Skip to content

Docker quickstart

Run the SDK without installing Python or PyTorch on your host. For the CPU quickstart, allow 8 GB of RAM and 10 GB of free disk, with Docker Engine or Docker Desktop and Compose v2 or newer.

From the repository root:

docker compose run --build --rm laya

This builds the checkout, runs the sample request on CPU and prints JSON covering choice, score and noul. The first request downloads the selected public Hugging Face checkpoint; no account is needed. Allow several minutes for its first download. Weights stay in a named volume. Subsequent runs use docker compose run --rm laya.

Predictions and confidence still need evaluation on your workload. See the benchmark limits.

For ARM64 hosts, DGX Spark and Apple Silicon, see ARM64 and DGX Spark containers.

NVIDIA GPU / CUDA

Install a compatible NVIDIA driver and configure Docker with the NVIDIA Container Toolkit. The GPU image uses PyTorch CUDA 13.0 wheels. Check your GPU's compute capability and driver against PyTorch's supported builds; older cards may require a different build. Allow additional disk space for CUDA layers. VRAM needs depend on the checkpoint, batch size and input length.

docker compose -f compose.yaml -f compose.cuda.yaml run --build --rm laya

The override selects GPU 0 and defaults to LAYA_DEVICE=cuda. Set LAYA_GPU_ID to another host index or UUID. That GPU appears as device 0 inside the container. Check access without downloading weights:

docker compose -f compose.yaml -f compose.cuda.yaml run --rm laya python -c \
  'import torch; assert torch.cuda.is_available(); print(torch.cuda.get_device_name(0)); print(torch.ones(1, device="cuda").cpu())'

The sample rejects unavailable CUDA before loading a checkpoint. Laya can still fall back to CPU after a memory or inference error, so inspect its warnings. Rebuild when switching between CPU and CUDA configurations.

The image sets TORCH_DISABLE_NATIVE_JIT=1. PyTorch 2.14 otherwise replaces some eager CUDA ops with Triton kernels that it compiles on the first inference, which needs a C compiler the slim image does not carry: the container reports healthy and then fails every request (#365). The stock kernels give the same answers at the same latency. Set the same variable on a bare-metal install if predict fails with Failed to find C compiler.

This uses Compose GPU reservations. Windows requires Docker Desktop's supported WSL2 GPU setup. Apple MPS, AMD/ROCm and Intel GPU containers are outside this quickstart; use CPU unless you configure and validate another backend.

Configuration

Set Compose variables in your shell, a local .env file, or the service's environment block. Don't commit secrets in .env. Runtime variables also work with docker run -e; Compose-only settings are identified below.

Variable Default Purpose
LAYA_DEVICE cpu / cuda Device selected by the base / GPU configuration
LAYA_CUDA_AMP unset (checkpoint's amp_dtype) fp16/float16 or bf16/bfloat16 for the CUDA forward; anything else is ignored. Not cosmetic: the README's threshold section measures bf16 flipping 3 of 864 argmaxes on the parity set where fp16 flips none
LAYA_CPU_AMP unset bf16 or bfloat16 opts the CPU forward into bf16; anything else leaves it fp32. No fp16 spelling turns it on either: CPU autocast has no fp16 fast path that beats fp32, so bf16 is the only reduced precision core offers on this device
LAYA_MODEL auto Router alias: auto, english, multilingual, typed-decisions
LAYA_MODEL_PATH unset Compatible checkpoint path inside the container
LAYA_REVISION unset Hub commit, branch or tag used for every checkpoint download, or reviewed for the reviewed SHAs in laya/revisions.py; a revision= argument still wins
LAYA_SHA256_DIGESTS unset JSON digests verified before weights are parsed and loaded: {artifact: digest} for every checkpoint, or {model: {artifact: digest}} per checkpoint. See Security
LAYA_REQUEST_FILE bundled request JSON request path inside the container
OMP_NUM_THREADS 4 CPU threads; keep within available cores
HF_TOKEN / HF_TOKEN_FILE unset Optional Hugging Face credential
LAYA_API_KEY / LAYA_API_KEY_FILE unset laya-serve only: require Authorization: Bearer <key>
LAYA_PORT 8000 laya-serve only: container port, and the host port published for it
HF_HUB_OFFLINE 0 1 uses only cached checkpoints
HF_HOME /home/laya/.cache/huggingface Cache path; see mount requirement below
LAYA_CACHE_VOLUME project model cache Compose only: named cache volume
LAYA_GPU_ID 0 Compose only: NVIDIA device index or UUID
LAYA_TORCH_INDEX cu130 (CUDA) / cpu Compose build: PyTorch wheel index. cu126 also carries the pinned torch; cu128 does not
LAYA_TORCH_VERSION 2.14.0 Compose build: pinned PyTorch version

Compose forwards the runtime variables except HF_HOME, which stays aligned with its fixed cache mount, and except LAYA_MPS_AMP_MIN_ROWS, the MPS row gate, which no image here can reach because no container here can select MPS. If overriding HF_HOME in docker run or your own Compose file, provide a matching mount writable by UID 10001. Direct Docker builds select PyTorch with --build-arg TORCH_INDEX=cu130; runtime -e cannot change the installed wheel.

LAYA_MODEL=english OMP_NUM_THREADS=2 docker compose run --build --rm laya

docker build -t laya:local .
docker run --rm -e LAYA_MODEL=english -e OMP_NUM_THREADS=2 \
  -v laya-model-cache:/home/laya/.cache/huggingface laya:local

For your own request:

docker compose run --rm --volume "$PWD/request.json:/inputs/request.json:ro" \
  --env LAYA_REQUEST_FILE=/inputs/request.json laya

For a commented configuration with request, checkpoint and secret-file mounts, see compose.example.yml:

docker compose -f compose.yaml -f compose.example.yml run --build --rm laya

Add -f compose.cuda.yaml before run for NVIDIA GPUs. The example is an override of compose.yaml, so cache and image settings stay in one place.

Secret files

HF_TOKEN_FILE reads a mounted UTF-8 file at startup, trims surrounding whitespace and takes precedence over HF_TOKEN. Unreadable, empty or invalid files stop startup without printing their contents. The file must be readable by UID 10001. _FILE applies only to supported secrets, not every setting.

With HF_TOKEN_PATH pointing to an existing host file outside the checkout:

docker compose run --rm --volume "$HF_TOKEN_PATH:/run/secrets/hf_token:ro" \
  --env HF_TOKEN_FILE=/run/secrets/hf_token laya

Docker secrets or Kubernetes Secret volumes can supply the same file. Values are loaded into the process environment at startup; restart after changing a file. Never use tokens as build arguments or bake them into images. Public checkpoints need no token.

Fine-tuned checkpoints

This image runs inference. Fine-tuning happens outside it — the fine-tuning notebook runs the whole loop on Kaggle's free 2xT4 GPUs and exports a checkpoint this image can serve. Background and open questions about the training interface stay in #4 and #26.

Point LAYA_CHECKPOINT_PATH to an absolute host directory containing rl_agent_config.json, model.safetensors and matching tokenizer files:

docker compose run --rm --volume "$LAYA_CHECKPOINT_PATH:/models/custom" \
  --env LAYA_MODEL_PATH=/models/custom laya

Use a working copy writable by UID 10001 because the loader may update tokenizer configuration. A LoRA adapter alone is not a complete checkpoint. Leave LAYA_MODEL=auto when setting LAYA_MODEL_PATH; an explicit alias and local path are mutually exclusive. The local-path response comes from the Agent and has no Router routing metadata. These settings also work with the CUDA override. Evaluate fine-tuned checkpoints on held-out examples before relying on them.

Models from ModelScope

When a host cannot reach huggingface.co, the checkpoint can come from ModelScope and be baked into the image at build time. One argument selects which checkpoint, and it defaults to the multilingual one. The Compose override adds the prefetch arguments to both services and keeps the container off the Hub:

docker compose -f compose.yaml -f compose.http.yaml -f compose.modelscope.yaml up --build laya-serve

For NVIDIA, add -f compose.cuda.yaml before up; it repeats its own arguments for both services, so the ordering between the two overrides does not matter. Plain Docker takes the arguments directly:

docker build --build-arg MODELSCOPE_MODEL=multilingual \
  -t laya:local .
docker run --rm -e HF_HUB_OFFLINE=1 -p 127.0.0.1:8000:8000 laya:local laya-serve

One prerequisite on a host that has run this before. The baked weights land in $HF_HOME/hub inside the image, under the cache directory compose.yaml mounts the model-cache volume at (/home/laya/.cache/huggingface), and Docker seeds a named volume from the image only while that volume is empty. A volume left over from the Hub-based quickstart holds the older Hub snapshot, it is never re-seeded, and the baked weights stay invisible behind it: the loader resolves refs/main to the old Hub commit and the container answers with the weights that were already downloaded, as if the rebuild had changed nothing. Point the deployment at an empty cache volume -- docker compose down --volumes with the same Compose files and the same LAYA_CACHE_VOLUME, or LAYA_CACHE_VOLUME=<name> for a fresh one. With HF_HUB_OFFLINE=1 a missing or divergent ref is a load failure with no network to fall back to, but the prerequisite is the same.

docker/prefetch_modelscope.py lists the repository on modelscope.cn, downloads the checkpoint's own files -- the same set laya/agent.py asks the Hub for, so no sibling checkpoint is pulled -- and writes them into the image's hub cache the way snapshot_download lays out a snapshot. Nothing else changes: Agent, the Router that laya-serve builds, laya.cli and the integrations keep their repo ids and resolve them to the baked snapshot, so a container built this way needs no network at all. Each file's size is checked against what the repository reports before the snapshot is published, and its SHA-256 as well when the repository publishes one. A size or digest mismatch fails the build. A repository that publishes no digest leaves the check to size alone, which cannot detect a same-size substitution.

Variable Default Purpose
MODELSCOPE_MODEL multilingual (Compose); empty in the Dockerfile Checkpoint to bake: multilingual, english, typed-decisions or all. Empty means no prefetch and the unchanged image
MODELSCOPE_REVISION master ModelScope branch, tag or commit to bake
HF_HUB_OFFLINE 1 in the Compose override 1 never contacts the Hub, so the baked copy is served

A type expands to that checkpoint's path inside the bundled repository, which is what the Router and the one-shot quickstart load by default, so a build that names one type serves it with no further change:

MODELSCOPE_MODEL=english docker compose -f compose.yaml -f compose.http.yaml -f compose.modelscope.yaml up --build laya-serve
MODELSCOPE_MODEL=all docker compose -f compose.yaml -f compose.http.yaml -f compose.modelscope.yaml up --build laya-serve

all is the whole family, about 2.4 GB of weights. The override also sets LAYA_MODELS=multilingual, because LAYA_PRELOAD=1 with the default list would try to build every checkpoint and fail on the first one that was not baked; set LAYA_MODELS to the list you baked when you bake more, and MODELSCOPE_MODEL=all when a deployment really does serve the family.

Several types can be named at once -- MODELSCOPE_MODEL="english multilingual" bakes both, about 1.5 GB -- which is what a serving image usually wants: the Router chooses between the English and the multilingual checkpoint on its own, and one it was not given answers 500 inference failed with does not contain 'rl_agent_config.json' in the log. Checkpoints that share a repository always bake into one snapshot, because a cached revision resolves to a single directory; the root checkpoint and each subfolder are both in it.

Beyond the types the argument also takes repo[:subfolder] specs, comma- or space-separated, which is how the mirror's standalone repositories (laya, laya-multilingual, laya-typed-decisions) or a fine-tuned checkpoint is baked. A standalone repository is what Agent("convaiinnovations/laya-multilingual") loads directly; the Router's default is the bundled path, so a type is normally what a serving image wants.

Two details about pins and provenance. The build prints the commit the snapshot is keyed by, which is the tip of the revision that was baked -- pass that SHA as revision= or LAYA_REVISION to pin a load to exactly what was baked. A mirror repository can hold files from several uploads, so that tip is the one repository-wide key there is. Hub-side pins do not describe a mirror snapshot: reviewed names Hugging Face commits, and the SHA-256 digest map is keyed to Hub artifact hashes, so neither applies here, and no digest pin exists for a mirror snapshot either. The build already refuses a download that does not match the mirror's own reported size -- and its digest, when the mirror publishes one -- but this checks consistency with mirror metadata, not an independently pinned digest. The weights come from whichever mirror account the argument names, which is its own supply-chain decision for the deployment to make.

Development and cleanup

Open a Python prompt with docker compose run --rm laya python. To run the existing routing/criteria checks and secret-file tests against your checkout without downloading weights:

docker compose run --rm --volume "$PWD:/workspace:ro" --workdir /workspace laya \
  sh -ec 'python tests/test_router.py; python tests/test_criteria.py; python tests/test_docker_entrypoint.py'

Rebuild with --build after changing source or the bundled example. The image runs as UID/GID 10001. New named volumes inherit the image cache directory's ownership; host directories must be writable by that UID. Keep model caches writable for tokenizer compatibility updates.

--rm removes completed containers. docker compose down retains the cache. To delete downloaded weights, run docker compose down --volumes using the same Compose files and LAYA_CACHE_VOLUME setting. The next request downloads them again; don't remove a cache shared with another project.

HTTP serving

The image ships laya-serve, so the same build that runs the one-shot quickstart can serve the Jev-compatible API. compose.http.yaml adds it as a second service and leaves laya alone:

docker compose -f compose.yaml -f compose.http.yaml up --build laya-serve
curl -s localhost:8000/health
curl -s localhost:8000/v1/systemone -H 'content-type: application/json' \
  --data @examples/docker/request.json

For NVIDIA, add the CUDA override. It repeats the build args and the device reservation for laya-serve, because laya-serve is a separate service and overrides for laya never reach it:

docker compose -f compose.yaml -f compose.http.yaml -f compose.cuda.yaml up --build laya-serve

up keeps the service running in the foreground; -d detaches. Weights go to the same named model-cache volume as the quickstart, so serving after a quickstart run starts with the checkpoints already on disk. Stop with docker compose ... down, using the same Compose files.

The port is published on 127.0.0.1 only. The API has no authentication until LAYA_API_KEY is set, so set a key before exposing it with LAYA_BIND_ADDRESS=0.0.0.0, and put a TLS reverse proxy in front for remote clients. /health does not require authentication in either case, so the healthcheck below keeps working; with a key set it answers an unauthenticated caller {"status": "ok"} and withholds the checkpoint, revision and device fields, which need the bearer.

The service has a healthcheck on /health. docker compose ... up -d --wait laya-serve waits for HTTP liveness. With the Compose default LAYA_PRELOAD=0, it can return with no checkpoint loaded; the first valid inference request loads the checkpoint it needs. With LAYA_PRELOAD=1, the server builds the selected checkpoints before listening, but preloading does not run a model forward.

These are three separate checks: /health confirms the HTTP process responds, an authenticated /health response lists resident checkpoints, and a successful authenticated POST /v1/systemone confirms that the requested inference ran. A healthy container or a nonempty loaded list alone does not establish successful inference. The Docker workflow's HTTP job checks startup, loopback publication and authentication with preload disabled and invalid request bodies; it does not run a model forward.

/health reports device as the device a resident checkpoint actually computes on, which is not always what LAYA_DEVICE asked for: a checkpoint that wants a GPU it cannot get falls back to CPU silently and still answers correctly. checkpoint_devices names each loaded checkpoint, and device_is_preference is true only while nothing is resident, so a deployment that quietly lost its GPU says so instead of echoing its own configuration back.

Server configuration

These apply to the laya-serve service only.

variable default effect
LAYA_HOST 0.0.0.0 bind address inside the container
LAYA_PORT 8000 container port, and the host port published for it
LAYA_BIND_ADDRESS 127.0.0.1 host address the port is published on
LAYA_PRELOAD 0 1 builds every checkpoint at startup instead of on first request
LAYA_MODELS (all) comma list to preload: english,multilingual,typed-decisions
LAYA_THREADS OMP_NUM_THREADS caps torch intra-op threads; keep at or below physical cores
LAYA_AUTO_TASK 0 1 lets the router reach typed-decisions automatically
LAYA_DEFAULT_MODEL multilingual Checkpoint a state with no language evidence falls back to (no letters, or Latin text too short to identify). Set english for mostly English traffic; an unresolvable name stops the container at startup instead of serving a configuration nobody asked for
LAYA_EXTRA_MODELS (none) JSON object {name: source} registering extra checkpoints beside the bundled ones: a Hub repo id or local checkpoint directory as a string, or a ["repo", "subfolder"] pair
LAYA_IDLE_UNLOAD_SECONDS 0 unload resident checkpoints after this many idle seconds; the next request loads its checkpoint again. Zero disables unloading
LAYA_MAX_LOADED 2 Checkpoints kept resident; LAYA_AUTO_TASK makes a third reachable on demand, and a cap below what routing chooses rebuilds one per switch
LAYA_MAX_CONCURRENT 16 requests admitted at once; later ones get 503 (a value that does not parse, or is not positive, falls back to 16)
LAYA_LOG_LEVEL info uvicorn log level
LAYA_API_KEY (none) when set, requires Authorization: Bearer <key>
LAYA_ROOT_PATH (empty) public URL prefix for FastAPI when behind a reverse proxy; the proxy should strip it before forwarding
LAYA_MAX_TOKEN_BUDGET 8192 cap on per-request max_len and head_max_len overrides
LAYA_MAX_BATCH_TOKENS 131072 tokens one /v1/systemone/batch forward pass may collate (states × questions × row width); a larger batch is split into several passes, not refused
LAYA_JEV_STRICT 0 1 serves the strict Jev wire contract (no root routing, no per-answer action / answer_confidence, reduced usage) for strict Jev clients
LAYA_TOURNAMENT_GROUP 0 answer a choice with more options than this by elimination in groups of this size (2 to 100), on /v1/systemone, and raise its option cap from 100 to 255. See Large choices. Unset, or outside 2 to 100, is off
LAYA_SHA256_DIGESTS (none) JSON digests checked before a checkpoint is parsed: {artifact: digest} for every checkpoint, or {model: {artifact: digest}} per checkpoint. See Security

For example, set LAYA_ROOT_PATH=/laya when publishing the API under /laya. The proxy must strip that prefix before forwarding to the container; this setting updates FastAPI's generated URLs and does not change the internal /health or /v1/systemone routes.

LAYA_PRELOAD defaults to 0 here rather than the package default of 1, because preloading makes the first boot download all three checkpoints. Set it to 1 for a long-running deployment so the first request does not pay for the build.

LAYA_PORT sets both the published host port and the port the server binds, so the two cannot drift. Change one place to move the service:

LAYA_PORT=9000 docker compose -f compose.yaml -f compose.http.yaml up --build laya-serve

Bearer token from a file

LAYA_API_KEY_FILE is read once at startup, moved into LAYA_API_KEY, and the _FILE variable is removed before the server execs. Prefer this to putting the key in the environment:

docker compose -f compose.yaml -f compose.http.yaml run --rm \
  --volume "$PWD/laya_api_key:/run/secrets/laya_api_key:ro" \
  -e LAYA_API_KEY_FILE=/run/secrets/laya_api_key \
  --service-ports laya-serve