We have been working on Tiles, a private and secure AI assistant for everyday use. To ensure its Python model server starts predictably on any machine, the runtime and dependencies must be deterministic and portable. This post walks through how we achieve that with layered venvstacks.
The Python Problem
Right now, we have a polyglot architecture where the control plane and CLI are written in Rust, while local model inference runs through a Python server as a daemon. Ideally, when we ship Tiles, we should also ship the required artifacts needed to run Python on the user’s system.
Since Python servers cannot be compiled into a single standalone binary, the user’s system must have a Python runtime available. More importantly, it must be a deterministic Python runtime so that the server runs exactly on the version developers expect.
In earlier releases of Tiles (before 0.4.0), we packed the server files into the final release tarball. During installation, we extracted them to the user’s system, downloaded uv (a Python package manager), installed Python 3.13 if it was not already present, and then ran the server as a daemon.
This approach had several issues:
- Downloading development-related tools such as
uvonto the user’s system - Relying on
uvat install time to manage dependencies and run the server - Increased chances of failures due to dependency or runtime non-determinism
- Requiring internet access to download all of the above tools
- Lack of a fully deterministic runtime across operating systems
One of the long-term goals of Tiles is complete portability. The previous approach did not align with that vision.
Portable Runtimes
To address these issues, we decided to ship the runtime along with the release tarball. We are now using venvstacks by LM Studio to achieve this.
Venvstacks allows us to build a layered Python environment with three layers:
- Runtimes
Defines the exact Python runtime version we need. - Frameworks
Specifies shared Python frameworks such as NumPy, MLX, and others. - Applications
Defines the actual server application and its specific dependencies.
Similar to Docker, each layer depends on the layer beneath it. A change in any layer requires rebuilding that layer and the ones above it.
All components within a layer share the layers beneath them. For example, every framework uses the same Python runtime defined in the runtimes layer. Likewise, if we have multiple servers in the applications layer and both depend on MLX, they will share the exact deterministic MLX dependency defined in frameworks, as well as the same Python runtime defined in runtimes.
How venvstacks is used in Tiles
We define everything inside a venvstacks.toml file. Here is the venvstacks.toml used in Tiles.
Because we pin dependency versions in the TOML file, we eliminate non-determinism.
Internally, venvstacks uses uv to manage dependencies. Once the TOML file is defined, we run:
venvstacks lock venvstacks.toml
venvstacks build venvstacks.toml
venvstacks publish venvstacks.tomlThese commands resolve dependencies, create the necessary folders, lock files, and metadata for each layer, build the Python runtime and environments based on the lock files, and produce reproducible tarballs that can be unpacked on external systems and run directly.
We bundle the venvstack runtime artifacts into the final installer using this bundler script. During installation, this installer script extracts the venvstack tarballs into a deterministic directory.
Our Rust CLI can then predictably start the Python server using:
stack_export_prod/app-server/bin/python -m server.mainWhat’s Next
We tested version 0.4.0 on clean macOS virtual machines to verify portability, and the approach worked well.
For now, we are focusing only on macOS. When we expand support to other operating systems, we will revisit this setup and adapt it as needed.
Packaging the runtime and dependencies increases the size of the final installer. We are exploring ways to reduce that footprint.
We also observed that changes in lock files can produce redundant application tarballs when running the publish command. More details are tracked in this issue.
Overall, we are satisfied with this approach for now.
Till then, keep on tiling.
