- Add a Per-Job Environment section explaining that runtime.environment reaches only the GPUs declared in the same job YAML, with one-YAML-per-GPU-group examples for local, cross-PCIe workaround, and NVSwitch NVLink tuning setups - Replace the NCCL workaround pair in the runtime schema example with ASTR_LOG_LEVEL and ASTR_BACKEND and document value semantics (str() rendering, null exports empty, no host-shell passthrough) - Comment out the blanket NCCL exports in the get-started multi-GPU example so they are opt-in per docs/guides/distributed.md - Add a hard rule against copying NCCL workarounds into every training config
7.8 KiB
Containerized Training Deployment
AstrAI uses one training YAML as the declaration for both host-side container
runtime settings and in-container training settings. Do not invoke the trainer
with raw docker compose up; use scripts/train.sh so preflight validation,
checkpoint recovery, and graceful shutdown remain active.
Architecture
train.yaml
├── runtime parsed on the host before Docker starts
└── model/data/... parsed by train.py inside the container
│
scripts/train.sh preflight, Compose wrapper, lifecycle, timer
└── docker-compose.yml GPU passthrough, mounts, image, container limits
└── scripts/docker/train-entrypoint.sh process count, parallel mode, auto-resume
└── train.py --config /run/astrai/train.yaml
The two parsers deliberately own different sections. scripts/docker/train_runtime.py
reads only runtime; scripts/tools/train.py reads only
model/data/parallel/training/ckpt/log. Explicit trainer arguments after --
override training YAML values.
Runtime Schema
runtime:
job_name: astrai-train
paths:
data: ./data
model: ./params
checkpoints: ./checkpoints
gpu:
devices: all
parallel_mode: auto # one GPU: none; multiple GPUs: ddp
container:
cuda_tag: cu128
ipc: host
stop_grace_period: 10m
stop_timeout_seconds: 600
checkpoint_keep_last: 5
# max_duration_hours: 12
# Optional; entries are passed verbatim into the trainer container
# (see "Per-Job Environment"):
# environment:
# ASTR_LOG_LEVEL: DEBUG
# ASTR_BACKEND: torch_native
- Relative paths resolve from the YAML file's directory, not the current shell.
devicesis eitherallor a non-empty physical GPU index list. Compose passes all GPUs once;CUDA_VISIBLE_DEVICESperforms the only filtering.- The process count is derived from
devices. Withall, the entrypoint usestorch.cuda.device_count()after Docker starts. parallel_mode: autoselectsnonefor one GPU andddpfor multiple GPUs. Usefsdpexplicitly when model sharding is required.- To select specific physical GPUs, replace
allwith a list such asdevices: [0, 1]. environmententries apply only to the job defined by this YAML file, not to the host or to other jobs. Keep the section omitted unless this job's GPU selection needs it; see Per-Job Environment.max_duration_hoursstarts a detached host timer that calls the same gracefulstopcommand. A manual stop cancels the timer.
Per-Job Environment
runtime.environment is scoped to one job. start passes only the entries of
the config file it was given, so a variable reaches exactly the GPUs declared
in that file's runtime.gpu.devices and nothing else. Two jobs on the same
machine can therefore differ: a job whose GPUs have working peer-to-peer keeps
the section omitted, a job whose GPUs cross broken PCIe/NVLink paths declares
the NCCL workarounds, and a job on an NVSwitch fabric can pin the NVLink fast
path on.
Because of that scoping, the effective pattern is one YAML per GPU group rather than one shared YAML that gets edited whenever the device list changes:
# train-local.yaml: GPUs with working peer-to-peer; nothing to declare
runtime:
gpu:
devices: [0, 1]
# train-cross-pcie.yaml: this GPU set crosses broken paths, so only this job
# declares the workarounds (confirm first; see docs/guides/distributed.md)
runtime:
gpu:
devices: [4, 5, 6, 7]
environment:
NCCL_P2P_DISABLE: "1"
NCCL_NET_GDR_LEVEL: "0"
The same mechanism carries positive tuning, not just workarounds. On an NVSwitch node (Hopper-class GPUs with fabric manager running), NVLink SHARP multicast (NVLS) is the fast allreduce path and NCCL enables it automatically where supported. A job may pin it on explicitly and raise channel parallelism when benchmarks show the NVLink bandwidth is underused:
# train-nvlink.yaml: NVSwitch node; keep the disables OUT and pin the fast
# path on instead (verify support with NCCL_DEBUG=INFO first)
runtime:
gpu:
devices: [0, 1, 2, 3]
environment:
NCCL_NVLS_ENABLE: "1"
NCCL_MIN_NCHANNELS: "8"
# NCCL_ALGO: NVLS # force one algorithm; unsupported values fail loudly
NVLS requires NVSwitch multicast support; on plain NVLink bridges or PCIe-only
sets, keep the section omitted and let NCCL pick Ring/Tree with P2P. Newer
drivers list the actual interconnect and NVLS support directly in
nvidia-smi topo -m, so check that before assuming.
Confirm a variable is needed before adding it, and only in the YAML of the job that hits the problem:
nvidia-smi topo -m # check P2P support between exactly the selected GPUs
NCCL_DEBUG=INFO # confirm NCCL transport errors before disabling them
See docs/guides/distributed.md for what each troubleshooting variable
disables. The two directions are mutually exclusive: NCCL_P2P_DISABLE and
NCCL_NET_GDR_LEVEL remove bandwidth and must never appear in the same
environment as the NVLink entries above.
Semantics:
- Values must be scalars and are rendered with
str(), so quote them explicitly ("1","0") instead of relying on YAML booleans or numbers. - A
nullvalue exports the name with an empty value. - This section is the only path for extra host variables into the trainer container; variables exported in the host shell do not pass through Compose.
Fixed Container Paths
| Runtime path | Container path | Access |
|---|---|---|
runtime.paths.data |
/data |
read-only |
runtime.paths.model |
/models/base |
read-only |
runtime.paths.checkpoints |
/checkpoints |
read-write |
| the selected YAML | /run/astrai/train.yaml |
read-only |
Training configuration must therefore use data_root_path: /data. The source
code is baked into /app; start reuses the existing image, so run
bash scripts/train.sh build [CONFIG] after code changes.
Operations
The config argument defaults to ./train.yaml:
bash scripts/train.sh init [CONFIG]
bash scripts/train.sh preflight [CONFIG]
bash scripts/train.sh start [CONFIG]
bash scripts/train.sh start [CONFIG] --foreground -- --dry-run
bash scripts/train.sh logs [CONFIG]
bash scripts/train.sh status [CONFIG]
bash scripts/train.sh stop [CONFIG]
bash scripts/train.sh restart [CONFIG]
bash scripts/train.sh clean [CONFIG] --keep 5
bash scripts/train.sh clean [CONFIG] --keep 5 --force
init creates the declared runtime directories but does not generate or mutate
the YAML. preflight validates Docker, paths, base model files, checkpoint
writability, GPU configuration, and rendered Compose configuration.
Checkpoint Recovery
Checkpoints are stored below
runtime.paths.checkpoints/<job_name>/epoch_<N>_step_<N>. A checkpoint is
complete only when it contains:
meta.json
config.json
model.safetensors
optimizer.pt
scheduler.pt
start resumes the latest complete checkpoint and ignores partial writes. If no
complete checkpoint exists, /models/base/config.json and
/models/base/model.safetensors are required. stop sends SIGTERM; the
trainer finishes at a batch boundary and saves an emergency checkpoint before
the Docker timeout expires.
Hard Rules
- Keep Docker settings in
runtimeand trainer settings in the remaining YAML sections. - Filter GPUs once: Compose passes
count: all;devicesbecomesCUDA_VISIBLE_DEVICES. - Do not force DDP for a model that requires FSDP; declare the mode explicitly.
- Do not use
kill -9for routine shutdown; usescripts/train.sh stop CONFIG. - The image user is built with the host UID/GID so mounted checkpoints retain usable ownership.
- Scope
runtime.environmentto the job YAML that needs it; do not copy NCCL workarounds into every config.
Document Update Time: 2026-08-29