Skip to main content
Version: v0.2.1

sflow YAML Schema Reference

Complete field-by-field reference for sflow v0.1 YAML configuration.

For narrative docs with examples: configuration, variables, artifacts, backends, operators, probes, replicas, resources, modular-workflows, outputs.

Top-Level Fields

version: "0.1"          # Required. Only "0.1" is valid.
variables: {} # Optional. Dict or list of variable declarations.
artifacts: [] # Optional. List of named URI resources.
backends: [] # Optional. List of compute backends.
operators: [] # Optional. List of task execution methods.
workflow: {} # Required. Workflow definition with tasks.

Extra top-level keys are rejected (extra="forbid").


Variables

Variables can be declared as a dict or list. Dict form is more common:

variables:
MY_VAR:
description: "Human-readable description" # optional
type: integer # optional: integer, string
value: 42 # required
domain: [16, 32, 64] # optional: valid values for sweeps

List form:

variables:
- name: MY_VAR
value: 42

Field Reference

FieldTypeRequiredDescription
descriptionstringNoHuman-readable description
typeinteger/stringNoType hint; default inferred from value
valueanyYesDefault value (can be an expression)
domainlistNoAllowed values; value must be in domain

Expression Syntax

Use ${{ }} in YAML fields (resolved before execution):

value: "${{ variables.TP_SIZE * variables.DP_SIZE }}"
value: "${{ [variables.X, 1] | max }}"
value: "${{ 'yes' if variables.FLAG else 'no' }}"

Available expression contexts:

ContextAvailable InExample
variablesYAML fields${{ variables.TP_SIZE }}
backendsYAML fields${{ backends.slurm_cluster.nodes[0].ip_address }}
artifactsYAML fields${{ artifacts.MODEL.path }}
workflowYAML fields${{ workflow.name }}
taskScripts only${{ task.server.nodes[0].ip_address }}

Backend Node Properties

backends.<name>.nodes[i].name         # hostname
backends.<name>.nodes[i].ip_address # IP address
backends.<name>.nodes[i].index # 0-based index
backends.<name>.nodes[i].num_gpus # GPUs on that node

Task Context (Scripts Only)

task.<name>.nodes[i].name
task.<name>.nodes[i].ip_address
task.<name>.gpus # assigned GPU indices
task.<name>.backend # backend name
task.<name>.operator # operator name

For replicated tasks, use task.<name>_0 or task.<name>[0].

Reserved Environment Variables

VariableDescription
SFLOW_WORKSPACE_DIRWorkspace root directory
SFLOW_OUTPUT_DIROutput root directory
SFLOW_WORKFLOW_OUTPUT_DIRWorkflow-specific output directory
SFLOW_TASK_OUTPUT_DIRTask-specific output directory
SFLOW_REPLICA_INDEXReplica index (0-based)
SFLOW_TASK_ASSIGNED_NODE_NAMESComma-separated node hostnames
SFLOW_TASK_ASSIGNED_NODE_IPSComma-separated node IP addresses

Artifacts

artifacts:
- name: LOCAL_MODEL_PATH
uri: fs:///absolute/path/to/model
- name: CONFIG_FILE
uri: file://generated_config.yaml
content: |
setting: ${{ variables.SOME_VAR }}

Field Reference

FieldTypeRequiredDescription
namestringYesUnique identifier; becomes env var name
uristringYesURI with scheme (fs://, file://, http://, etc.)
contentstringNoInline content; only valid with file:// scheme

URI Schemes

SchemeBehavior
fs://Resolved to local path; validated in dry-run (must exist)
file://Generated at runtime; supports content field with expressions
OthersKept as string; no download or validation

Access patterns:

  • YAML: ${{ artifacts.LOCAL_MODEL_PATH.path }}
  • Scripts: ${LOCAL_MODEL_PATH} (env var)

Best Practice: Use file:// Artifacts for Helper Scripts

When a task needs a helper script (Python, shell, etc.), always use a file:// artifact with content instead of embedding multi-line code inline in the task script. Inline code via heredocs or python3 -c breaks due to YAML -> shell -> Python quoting conflicts:

artifacts:
- name: MY_PROXY_SCRIPT
uri: file://proxy.py
content: |
import uvicorn
from fastapi import FastAPI
app = FastAPI()
# ... full script with f-strings, quotes, etc. -- all safe here
uvicorn.run(app, host="0.0.0.0", port=8000)

workflow:
tasks:
- name: proxy
script:
- python3 ${{ artifacts.MY_PROXY_SCRIPT.path }}

Backends

backends:
- name: slurm_cluster
type: slurm # "slurm" or "local"
default: true # at most one backend can be default
nodes: 4
partition: my_partition
account: my_account
time: 120 # minutes
gpus_per_node: 8 # positive integer
extra_args: # additional sbatch/salloc flags
- --exclusive
- --gpus-per-node=8

Field Reference

FieldTypeRequiredDefaultDescription
namestringYesUnique backend identifier
typestringYes"slurm" or "local"
defaultboolNofalseMark as default (max one allowed)
nodesint/exprNo1Number of nodes to allocate
partitionstringNoSLURM partition name
accountstringNoSLURM account name
timeint/strNoSLURM time limit (minutes)
gpus_per_nodeintNoGPUs per node (must be positive)
extra_argslistNo[]Additional backend arguments

local backend: Creates synthetic nodes (localhost). Default operator is bash.

slurm backend: Uses existing allocation (SLURM_JOB_ID) if available; otherwise runs salloc. Default operator is srun.


Operators

operators:
- name: my_operator
type: srun # "srun" or "bash"
container_image: nvcr.io/nvidia/pytorch:24.07-py3
container_name: my_container # optional Pyxis container name
container_writable: true # writable container filesystem
mpi: pmix # MPI implementation
extra_args: # additional srun flags
- --container-image=${{ variables.IMAGE }}

Field Reference

FieldTypeRequiredDescription
namestringYesUnique operator identifier
typestringYes"srun" or "bash"
container_imagestringNoContainer image URI (Pyxis)
container_namestringNoNamed container for reuse
container_writableboolNoAllow writes to container filesystem
mpistringNoMPI type (pmix, pmi2, etc.)
extra_argslistNoAdditional srun/bash flags

Container Reuse with Named Containers

Combine --container-image in extra_args with container_name to import the image once and reuse the named container for all subsequent tasks. The first task (e.g. load_image) pays the import cost; all later tasks using the same container_name start instantly:

operators:
- name: my_runtime
type: srun
container_name: my_container # attach by name (skips import if exists)
container_writable: true
mpi: pmix
extra_args:
- --container-image=${{ variables.MY_IMAGE }} # imports only when name not found

Task-Level Operator Override

Tasks can override operator settings inline:

operator:
name: my_operator # reference named operator
ntasks: 4 # total MPI tasks
ntasks_per_node: 1 # tasks per node
extra_args: # additional flags for this task only
- --mem=64G

Workflow

workflow:
name: my_workflow # required
timeout: 120m # optional: workflow-level timeout
variables: # optional: workflow-scoped variables (override global)
HEAD_NODE_IP:
value: "${{ backends.slurm_cluster.nodes[0].ip_address }}"
tasks: [...] # required: list of task definitions

Field Reference

FieldTypeRequiredDescription
namestringYesWorkflow name
timeoutstringNoTimeout (e.g. "120m", "2h")
variablesdictNoWorkflow-scoped variables (override global)
taskslistYesNon-empty list of task definitions

Tasks

- name: my_task                 # required, unique within workflow
operator: operator_name # optional: string or inline dict
script: # required, non-empty list
- echo "hello"
- python run.py
depends_on: # optional
- other_task
resources: {} # optional
replicas: {} # optional
probes: {} # optional
retries: {} # optional
outputs: [] # optional

Task Field Reference

FieldTypeRequiredDescription
namestringYesUnique task name (no duplicates allowed)
operatorstring/dictNoOperator name or inline override
scriptlist[str]YesShell commands (non-empty)
depends_onlist[str]NoTask names this task depends on
resourcesdictNoNode and GPU requirements
replicasdictNoReplica count, policy, sweep variables
probesdictNoReadiness and failure probes
retriesdictNoRetry policy
outputslistNoOutput parsing patterns

Resources

resources:
gpus:
count: 4 # CUDA_VISIBLE_DEVICES slicing
nodes:
indices: [0] # pin to specific node indices
count: 2 # OR request N nodes (mutually exclusive with indices)

GPU allocation:

  • Each task/replica gets count GPUs via CUDA_VISIBLE_DEVICES
  • GPUs are sliced sequentially across replicas on each node
  • Total GPUs across all tasks must not exceed nodes * gpus_per_node

Replicas

replicas:
count: 4 # number of replicas
policy: "parallel" # "parallel" or "sequential"
variables: # sweep variable names (must have domain)
- CONCURRENCY
FieldTypeDefaultDescription
countint1Number of replicas
policystring"parallel""parallel" or "sequential"
variableslist[]Variable names with domain for sweeps

Naming: replicas are named task_0, task_1, etc. Sweep replicas use Cartesian product indices.

Probes

probes:
readiness:
log_watch:
regex_pattern: "Server ready"
match_count: 2 # wait for N matches (optional)
logger: other_task # watch another task's log (optional)
timeout: 600 # seconds
interval: 10 # seconds between checks
failure:
log_watch:
regex_pattern: "Traceback (most recent call last)"
interval: 10

Probe types (exactly one per probe):

TypeFieldsDescription
tcp_portportPort accepts TCP connections
log_watchregex_pattern, optional match_count, loggerPattern in task log

Retries

retries:
count: 3 # max retry attempts
interval: 30 # seconds between retries
backoff: 2 # multiplier for interval

Outputs

outputs:
- pattern: "accuracy: {accuracy:f}"

Parsed values written to outputs.json after task success.


Modular Composition

Multiple YAML files are merged with sflow compose or sflow run -f:

Merge Rules

  1. version must match across all files
  2. variables, artifacts, backends, operators merge by name (later file wins)
  3. workflow.name must match (if set in multiple files)
  4. workflow.tasks are concatenated in file order
  5. workflow.variables are merged by name

Missable Tasks

When composing configurations where some tasks may be absent (e.g., using agg.yaml without prefill.yaml):

sflow run -f base.yaml -f agg.yaml -f benchmark.yaml \
--missable-tasks prefill_server --missable-tasks decode_server

This removes prefill_server and decode_server from all depends_on lists and probe logger references.

Variable Overrides

sflow run -f config.yaml --set TP_SIZE=8 --set CONCURRENCY=[16,32,64]
sflow run -f config.yaml --artifact LOCAL_MODEL_PATH=fs:///new/path

For detailed explanations and more examples, see: