Services¶
The top-level services: block declares long-running processes that srtctl launches and tracks next
to the inference workers, the frontend, and the benchmark client. One list, one shape, for everything
that is not a worker or the frontend: the discovery plane (etcd, NATS), the Mooncake master, the
metrics exporters tachometer scrapes, an experimental router built from an unmerged PR, a standalone
Mooncake Store per worker node, a debugging HTTP server. The built-in ones are implied by the rest of
the recipe and need no entry; declaring one by name takes it over. Adding a new one is a recipe
change, not a code change.
Table of Contents¶
- Quick Start
- Configuration Reference
- Implicit Services
- Placement
- Start Order and Readiness
- Environment
- Building From Source
- Service Types
- Example: a router from a PR
- Example: standalone Mooncake stores
- Validation
- Limitations
Quick Start¶
services:
- name: my-sidecar
command:
- python3
- -m
- my_package.my_sidecar
- --port
- "9000"
readiness:
port: 9000
name and command are the only required fields for the default generic type. The service runs in
the job container on the head node, starts once workers and the frontend are healthy, and the job
waits until port 9000 answers before moving on. Its log is service_my-sidecar.out in the job's log
directory. examples/features/services.yaml is a runnable version of this.
Configuration Reference¶
services:
- name: my-sidecar # required, unique across the list
type: generic # generic (default) | etcd | nats | mooncake-master | dcgm-exporter | node-exporter | mooncake-store
enabled: true # false drops the service (the way to switch an implied one off)
external: null # typed kinds only: use an instance that already runs at this address
command: # argv, not shell-interpreted; required for generic
- python3
- -m
- pkg
args: # appended to command
- --flag
container: my-image # image or srtslurm.yaml alias; default: job container
env: # environment for the service process
MY_FLAG: "1"
options: # kind-specific knobs (nats: max_payload_mb; exporters: port, collect_interval_ms)
max_payload_mb: 24
placement:
node: head # head | infra | dedicated | prefill | decode | agg | workers
start: after_frontend # infra | before_workers | after_frontend
readiness: # optional probe, checked on every service node; typed kinds have default ports
port: 9000 # or tcp: {port} / http: {port, path, status} / log: {pattern}
timeout_seconds: 120
interval_seconds: 2
inherit_discovery_env: true # inject ETCD_ENDPOINTS / NATS_SERVER
critical: false # a crash fails the run when true
preamble: | # shell run before command, inside the container
ulimit -n 1048576
cpus_per_task: 8 # srun --cpus-per-task
cpu_bind: none # srun --cpu-bind
srun_options: # extra srun options for this service only
exclusive: ""
source: # clone before build/launch; single-node placements only
git: https://github.com/org/repo
rev: <commit-sha, tag, or refs/pull/N/head>
path: subdir
build_command: # run once from the clone, inside the container
- bash
- -lc
- pip install -e .
| Field | Default | Notes |
|---|---|---|
name |
required | Unique. Names service_<name>.out and the tracked process. |
type |
generic |
Selects a service type that supplies defaults, and for the typed kinds the command. |
enabled |
true |
false drops the service. Declaring an implied name with enabled: false switches it off. |
external |
none | etcd, nats, mooncake-master only: an address of an already-running instance. Nothing launches; the address is injected where the job's own would have been. |
options |
{} |
Kind-specific knobs; unknown keys are rejected. nats: max_payload_mb. dcgm-exporter, node-exporter: port, collect_interval_ms. mooncake-master: store_config (vLLM). |
command |
type default | Argv passed directly to the process. generic has no default, so it is required there. |
args |
[] |
Appended to command. Handy with typed services that supply the command. |
container |
type fallback, then job container | Aliases resolve through srtslurm.yaml like every other container key. |
env |
{} |
Merged over the type's defaults; see Environment. |
placement.node |
type default | generic: head. See Placement. |
start |
type default | etcd, nats: infra. mooncake-master, mooncake-store: before_workers. generic and the exporters: after_frontend. |
readiness |
type default | One probe per node: port / tcp, http, or log, plus timeout_seconds and interval_seconds. The typed kinds gate on their well-known ports when no probe is written. See Start Order and Readiness. Timing out terminates what this stage started and fails the job. |
inherit_discovery_env |
true |
Inject the same ETCD_ENDPOINTS / NATS_SERVER the Dynamo frontend gets. |
critical |
type default | generic: false. mooncake-store: true. |
preamble |
none | Shell run after the environment is exported and before command. |
cpus_per_task, cpu_bind, srun_options |
none | Pass-through srun knobs for this service's launches. |
source, build_command |
none | See Building From Source. |
build_timeout_seconds |
1800 |
build_command is killed when this runs out, so a hung build cannot hold the allocation. |
command, args, env values, and preamble may use these placeholders: {node}, {node_ip},
{node_id} (position in the worker list), {index} (instance index within the service), {role}
(the placement.node value), {head_node}, {head_ip}, {infra_node}, {infra_ip},
{master_port}, {metadata_port}. Only those names are substituted; other braces (JSON in an env
value) are left alone.
Implicit Services¶
Things the recipe asks for elsewhere are services the job runs without an entry; the Mooncake master is the one built-in kind that is always declared. NATS is not implied by the Dynamo frontend alone: the default request plane is tcp and KV events travel over direct ZMQ, so a plain Dynamo job runs etcd only. Declare a nats service to run one regardless.
| Implied by | Services | Where |
|---|---|---|
frontend.type: dynamo |
etcd |
the infra node, phase infra |
dynamo.request_plane: nats, dynamo.event_plane: nats, or a nats_max_payload_mb knob |
nats |
the infra node, phase infra |
a declared mooncake-master entry (see Mooncake KV Store) |
mooncake-master |
the infra node, phase before_workers |
tachometer on (the default; observability.tachometer.enabled) |
dcgm-exporter, node-exporter |
every worker node, phase after_frontend |
srtctl dry-run lists them next to the declared ones, marked implied by:. A declared entry with
the same name replaces the implied one, so the recipe only says what differs:
services:
- name: etcd
type: etcd
placement:
node: dedicated # reserve a node for the discovery plane
- name: nats
type: nats
placement:
node: dedicated # etcd and nats share the infra node: both or neither
options:
max_payload_mb: 24
- name: dcgm-exporter
type: dcgm-exporter
container: mirror/dcgm-exporter:3.3.9-3.6.1-ubuntu22.04 # air-gapped cluster
- name: node-exporter
type: node-exporter
enabled: false # drop it
external uses an instance that already runs: nothing launches, and the workers and frontend get its
address in ETCD_ENDPOINTS / NATS_SERVER (or MOONCAKE_MASTER) instead of the infra node's.
services:
- name: etcd
type: etcd
external: http://etcd.shared.example:2379
The power-telemetry path (telemetry.enabled) launches and owns its own DCGM exporter; the implied
dcgm-exporter steps aside when it is on.
The v1 layout (infra: and backend.mooncake_kv_store) is documented in legacy-v1.md; srtctl migrate rewrites it into the entries above.
Placement¶
placement.node picks the physical nodes. head, infra, and dedicated launch one instance;
dedicated reserves a node for the infra services (the job asks Slurm for one more node) and is
accepted by etcd, nats, and mooncake-master. prefill, decode, and agg launch one instance
per distinct node that role's workers use, so two TP1 decode workers on one node share one service.
workers launches one instance per worker node; all one per allocated node (head, infra, benchmark client, and workers).
When a service launches on more than one node its processes and logs get a node suffix:
service_<name>_<node>. Two services that declare the same readiness.port and land on the same
node are rejected before anything launches; give them disjoint placements or ports.
Start Order and Readiness¶
Services launch in three phases; within a phase, implied services first, then declared ones in declaration order:
infra: the discovery plane (etcd, NATS). Nothing else should need this phase.before_workers: after the discovery plane, before any worker. The Mooncake master, standalone stores, anything workers connect to at startup.after_frontend: once workers and the frontend are healthy, before the scraper. The exporters, sidecars that register into a running job.
Within a phase, a service with readiness blocks until its probe passes on each of its nodes. The
typed kinds gate on their well-known ports by default (etcd 2379, NATS 4222, the Mooncake master
8700, 8701, and 8702, the exporters none); a generic service without a probe is considered started
when its srun is launched. Three probes are available, and a readiness block names exactly one:
readiness:
port: 9000 # shorthand for tcp
timeout_seconds: 120 # per node; the job fails when it runs out
interval_seconds: 2 # between attempts
readiness:
tcp:
port: 9000 # a TCP connection is accepted
readiness:
http:
port: 8000
path: /ready # GET http://<node>:8000/ready
status: 200 # returns this status
readiness:
log:
pattern: 'Uvicorn running on .*:\d+' # a regex matched against service_<name>.out
The probe is re-run every interval_seconds until it passes. Between attempts the stage checks that
the process is still alive, so a service that crashes fails the job at once with its exit code rather
than after the full timeout. Ongoing health is the shared
ProcessRegistry monitor, the same as every other process in the job. It tears the run down only for
critical: true services. Anything other components register under or route through should be
critical: a router that dies mid-run otherwise leaves the frontend silently talking to the raw
backend and the benchmark measuring something other than what it claims.
Environment¶
The service process environment is built in layers, later ones winning:
- Discovery env, when
inherit_discovery_envis true:ETCD_ENDPOINTS=http://<infra>:2379,NATS_SERVER=nats://<infra>:4222(or theexternaladdresses). etcd and NATS themselves never get it. - The type's defaults (
mooncake-storesetsMOONCAKE_LOCAL_HOSTNAMEto the node's IP). - The recipe's
env, with placeholders substituted. - Values srtctl owns for the type (
mooncake-store:MOONCAKE_MASTER,MOONCAKE_TE_META_DATA_SERVER). A recipe value for these is ignored.
Building From Source¶
source plus build_command clone and build once before launch. The clone runs on the bare host of
the service node (git and network access are host concerns, and the job container may lack git) into
<log_dir>/services/<name>/src, which every container sees at /logs/services/<name>/src.
build_command and command then run inside the service container from that directory (or
source.path under it).
rev must be immutable: a commit SHA, a tag, or refs/pull/<n>/head while iterating on an open
PR. main, master, and HEAD are rejected at load time. Because the build installs into one
container instance, source is only allowed with single-node placements (head, infra).
source is the same shape dynamo.source uses. At submit time srtctl apply resolves a non-commit
rev with git ls-remote and records the commit as source.sha in the submitted config.yaml
(the recipe on disk is untouched), so the job checks out exactly the commit the lockfile names even
if the PR is pushed to again while the job waits in the queue. --json output lists what was pinned
under pinned_sources. srtctl dry-run never touches the network.
Service Types¶
type selects a registered ServiceKind (src/srtctl/services/). A kind supplies defaults and the
environment its process needs; the launch path is shared by every kind. Register a new one with
@register_service("<name>"), the same pattern as @register_benchmark.
| Type | Default command | Start | Critical | Notes |
|---|---|---|---|---|
generic |
none (required) | after_frontend |
false |
Launches exactly what you wrote. |
etcd |
/configs/etcd from the job container, advertising the node's IP |
infra |
true |
Implied by the Dynamo frontend. Placement head, infra, or dedicated; supports external. Fresh data dir on node-local /tmp each job. |
nats |
/configs/nats-server -js from the job container |
infra |
true |
Implied by the Dynamo frontend. options.max_payload_mb writes a server config. Same placements as etcd; supports external. |
mooncake-master |
mooncake_master with the RPC, HTTP metadata, and metrics ports srtctl owns |
before_workers |
true |
Declared by name; see Mooncake KV Store. args are appended; options.store_config is the vLLM connector JSON. Container falls back to the job container. Supports dedicated and external. |
dcgm-exporter |
dcgm-exporter --collect-interval=<ms> --address :9401 in nvcr.io/nvidia/k8s/dcgm-exporter |
after_frontend |
false |
Implied on worker nodes while tachometer runs. Shell-less (distroless image). options: port, collect_interval_ms. |
node-exporter |
/bin/node_exporter with the cpu, infiniband, and meminfo collectors on 9101 in quay.io/prometheus/node-exporter |
after_frontend |
false |
Implied on worker nodes while tachometer runs. Shell-less. options: port. |
process-exporter |
configs/process-exporter -config.path <log_dir>/process-exporter.yml -web.listen-address=:9256 -threads=true ... on the bare node |
after_frontend |
false |
Implied on every allocated node (placement.node: all) while tachometer runs. Host-native from the static binary make setup installs; skipped with a warning when it is missing. A declared container switches to the image's /bin/process-exporter with the group file under /logs. options: port, binary. |
mooncake-store |
python -m mooncake.mooncake_store_service |
before_workers |
true |
Requires a mooncake-master entry. Container falls back to the master's. Injects the master's address. |
The bespoke launch paths these replace (start_head_infrastructure with its own readiness loop, a
Mooncake-master stage, exporter launches inside the tachometer stage) are gone; every one of these is
a ManagedProcess from the same stage, with the same registry, cleanup, and dry-run output.
Example: a router from a PR¶
frontend:
type: dynamo
services:
- name: thunderagent-router
source:
git: https://github.com/ai-dynamo/dynamo
rev: refs/pull/14000/head # switch to a commit SHA once merged
build_command:
- bash
- -lc
- "cd lib/bindings/python && maturin develop --uv && cd ../../.. && pip install -e ."
command:
- python3
- -m
- dynamo.thunderagent_router
- --endpoint
- dyn://namespace.component.endpoint
- --model-name
- my-model
# The router is the thing under test: other components register under its
# endpoint, so running without it must fail the run, not degrade it.
critical: true
inherit_discovery_env defaults to true, so the router sees the same etcd/NATS as the Dynamo
frontend and workers with no extra configuration.
Example: standalone Mooncake stores¶
Inference workers run embedded Mooncake clients with MOONCAKE_GLOBAL_SEGMENT_SIZE=0 while dedicated
per-node stores own the DRAM segments. Decode nodes contribute host memory without an in-process
HiCache pool. One entry per role gives each role its own segment size:
engine: sglang
roles:
prefill:
env:
MOONCAKE_PROTOCOL: rdma
MOONCAKE_DEVICE: "mlx5_0,mlx5_1"
MOONCAKE_GLOBAL_SEGMENT_SIZE: "0"
args:
disaggregation-transfer-backend: mooncake
decode:
env:
MOONCAKE_PROTOCOL: rdma
MOONCAKE_DEVICE: "mlx5_0,mlx5_1"
MOONCAKE_GLOBAL_SEGMENT_SIZE: "0"
args:
disaggregation-transfer-backend: mooncake
services:
- name: mooncake-master
type: mooncake-master
container: mooncake # the master; also the stores' default container
- name: store-prefill
type: mooncake-store
placement:
node: prefill
args:
- --port
- "8800"
env:
MOONCAKE_PROTOCOL: rdma
MOONCAKE_DEVICE: "mlx5_0,mlx5_1"
MOONCAKE_GLOBAL_SEGMENT_SIZE: 100gb
preamble: |
ulimit -n 1048576
ulimit -l unlimited
cpus_per_task: 8
readiness:
port: 8800
- name: store-decode
type: mooncake-store
placement:
node: decode
args:
- --port
- "8800"
env:
MOONCAKE_PROTOCOL: rdma
MOONCAKE_DEVICE: "mlx5_0,mlx5_1"
MOONCAKE_GLOBAL_SEGMENT_SIZE: 400gb
preamble: |
ulimit -n 1048576
ulimit -l unlimited
cpus_per_task: 8
readiness:
port: 8800
Stores start after the master is healthy and before workers. Each store gets MOONCAKE_MASTER,
MOONCAKE_TE_META_DATA_SERVER, and MOONCAKE_LOCAL_HOSTNAME from the runtime. If prefill and decode
share a node, the two entries above collide on port 8800 and the job fails before launching; use one
entry with placement.node: workers and a single segment size instead. See
Mooncake KV Store for the worker side.
Validation¶
Rejected at load time, so srtctl dry-run catches them:
- Empty or duplicate
name; unknowntype. genericwithoutcommand; acommand/argsentry that is blank;build_command: [].placement.nodeorstartoutside their vocabularies;dedicatedorexternalon a kind that does not support them; anoptionskey the kind does not know.sourcewith a movingrev, or with a multi-node placement.type: mooncake-storewithout amooncake-masterentry; twomooncake-masterentries.etcdandnatsdisagreeing ondedicated(they share the infra node).
Rejected at launch, before any service starts: two services listening on the same port on one node.
srtctl dry-run prints every service's type, placement, start phase, criticality, command,
container, source, readiness, and env.
Cleanup¶
Nothing a service launches outlives the job:
- Every long-running service is a named Slurm step (
service_<name>), so cleanup delivers SIGTERM throughscancel --signal=TERMand the process gets 30 seconds to flush (etcd its WAL, a scraper its parquet) before the step is killed. Signalling thesrunclient directly would have killed the task outright. - Every
srunthe stage starts, including the one-shot clone and build steps, is registered with the job'sProcessRegistrythe moment it exists, not when the stage returns. The registry's cleanup runs on normal completion, on any failed stage, from the SIGTERM handler (scancel), and from the crash monitor when a critical process dies, and it terminates then kills each trackedsrun. Slurm cancels the step, which kills the whole step cgroup inside the container, so forked or daemonized children of the service go with it. - A readiness gate that fails, or a signal that arrives during one, terminates everything the stage already launched before the error propagates.
- A service whose process exits before its readiness port answers fails immediately with its exit code instead of waiting out the readiness timeout.
- The clone step is bounded by the
timeouton each git command (600s each) and the build step bybuild_timeout_seconds; a step that overruns is killed and the job fails with a pointer to its log. - When the batch script exits, Slurm releases the allocation and reaps any remaining step, so even a cleanup path srtctl never reaches cannot leave a service running on a compute node.
Limitations¶
- Declared order is launch order, and
readinessis the only wait. A service that needs another service to be ready polls for it in its owncommand. sourcebuilds are single-node only.- Services run on the sbatch/SLURM path only; there is no local dev mode in 2.0.