Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 1 addition & 31 deletions docs-site/scripts/prepare-versioned-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,33 +205,6 @@ function writeJson(filePath, value) {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
}

// In-page anchors that were already broken in a published snapshot. A released tag
// cannot be re-cut, so for frozen versions the only place left to repair them is
// here, at extraction time. Current docs are deliberately NOT covered: those are
// fixed at source in docs/.
//
// Each entry carries the heading it assumes, and the repair only fires when that
// heading is actually present. `main` is a branch, not a frozen tag -- once the
// source fix lands there its snapshot already has the corrected heading, and an
// unconditional rewrite would break the link a second time in the other direction.
const FROZEN_ANCHOR_REPAIRS = [
{
from: "#modular-inference-recipe-inference_x_v2",
to: "#inference_x_v2",
onlyWhenHeading: /^### inference_x_v2\s*$/m,
},
];

function repairFrozenAnchors(text) {
let out = text;
for (const repair of FROZEN_ANCHOR_REPAIRS) {
if (out.includes(repair.from) && repair.onlyWhenHeading.test(out)) {
out = out.replaceAll(repair.from, repair.to);
}
}
return out;
}

function rewriteVersionedDocsLinks(rootDir, versionLabel) {
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
for (const entry of entries) {
Expand All @@ -240,9 +213,7 @@ function rewriteVersionedDocsLinks(rootDir, versionLabel) {
rewriteVersionedDocsLinks(entryPath, versionLabel);
} else if (entry.isFile() && entry.name.endsWith(".md")) {
const text = fs.readFileSync(entryPath, "utf8");
const rewritten = repairFrozenAnchors(
text.replaceAll("](/docs/", `](/docs/${versionLabel}/`),
);
const rewritten = text.replaceAll("](/docs/", `](/docs/${versionLabel}/`);
if (rewritten !== text) {
fs.writeFileSync(entryPath, rewritten);
}
Expand Down Expand Up @@ -313,7 +284,6 @@ if (require.main === module) {

module.exports = {
buildDocVersionPlan,
repairFrozenAnchors,
currentDocsSource,
docsPaths,
isReleaseTag,
Expand Down
20 changes: 0 additions & 20 deletions docs-site/scripts/prepare-versioned-docs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const {
safeVersionDirName,
currentDocsSource,
prepareVersionedDocs,
repairFrozenAnchors,
} = require("./prepare-versioned-docs");

function git(cwd, args) {
Expand Down Expand Up @@ -165,22 +164,3 @@ test("prepareVersionedDocs materializes current docs, versioned docs, sidebars,
"See [SPP](/docs/v1.2.3/sflow_spp).\n",
);
});

test("repairFrozenAnchors fixes a stale anchor only while the old heading is present", () => {
const stale = [
"See [Modular inference recipe](#modular-inference-recipe-inference_x_v2).",
"",
"### inference_x_v2",
].join("\n");
assert.match(repairFrozenAnchors(stale), /\(#inference_x_v2\)/);

// The guard that matters: `main` is a branch, so once the source fix lands its
// snapshot already carries the long heading and the link is correct. Rewriting
// unconditionally would break it a second time, in the other direction.
const fixed = [
"See [Modular inference recipe](#modular-inference-recipe-inference_x_v2).",
"",
"### Modular inference recipe (inference_x_v2)",
].join("\n");
assert.equal(repairFrozenAnchors(fixed), fixed);
});
258 changes: 27 additions & 231 deletions docs-site/static/sflow_intro.html

Large diffs are not rendered by default.

255 changes: 0 additions & 255 deletions docs/release_notes/RELEASE_NOTES_v0.3.1.md

This file was deleted.

3 changes: 1 addition & 2 deletions docs/user/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,12 @@ stateDiagram-v2

| Command | Purpose | Key Options |
|---------|---------|-------------|
| **`sflow run`** | Execute a workflow | `--dry-run`, `--tui`, `--bulk-input/--row`, `--set/-s`, `--artifact/-a`, `--missable-tasks/-M`, `--skip-artifact-check`, `--wait-for-gpus`, `--extra-args/-e`, `--output-dir`, `--log-level`, `--enable-workflow-monitor`, `--enable-task-monitor`, `--include-nodes/--exclude-nodes`, `--kubeconfig`, `--kube-context`, `--kube-namespace`, `--kube-node-selector`, `--kube-compute-domain-channel`, `--kube-compute-domain-create`, `--extra-kubectl-args`, `--extra-kubectl-apply-args` |
| **`sflow run`** | Execute a workflow | `--dry-run`, `--tui`, `--bulk-input/--row`, `--set/-s`, `--artifact/-a`, `--missable-tasks/-M`, `--skip-artifact-check`, `--extra-args/-e`, `--output-dir`, `--log-level`, `--enable-workflow-monitor`, `--enable-task-monitor`, `--include-nodes/--exclude-nodes`, `--kubeconfig`, `--kube-context`, `--kube-namespace`, `--kube-node-selector`, `--kube-compute-domain-channel`, `--kube-compute-domain-create`, `--extra-kubectl-args` |
| **`sflow batch`** | Generate Slurm sbatch scripts | `--submit`, `--bulk-input` (CSV sweeps), `--bulk-submit` (YAML folders), `--row`, `--nodes`, `--partition`, `--account`, `--time`, `--resolve`, `--skip-artifact-check`, `--sflow-version`, `--enable-workflow-monitor`, `--enable-task-monitor`, `--include-nodes/--exclude-nodes` |
| **`sflow compose`** | Merge multiple YAMLs into one | `--resolve`, `--validate`, `--bulk-input`, `--row`, `--missable-tasks/-M`, `-o/--output` |
| **`sflow visualize`** | Render DAG as image/mermaid | `--format` (png/svg/pdf/mermaid/dot), `--show-variables`, `--set/-s`, `--artifact/-a`, `--missable-tasks/-M` |
| **`sflow sample`** | List/copy example workflows | `--list`, `--force`, `-o/--output` |
| **`sflow skill`** | Copy agent skills into project (merges into existing directory) | `--list`, `--force` (overwrite existing files), `-o/--output` |
| **`sflow upgrade`** (alias `sflow update`) | Reinstall sflow in place from git or an index | `--repo`, `--branch`, `--sflow-version`, `--sflow-index-url`, `--sflow-source-path`, `--force`, `--dry-run` |

### Multi-file Input

Expand Down
39 changes: 5 additions & 34 deletions docs/user/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,40 +174,6 @@ sflow batch -f workflow.yaml -e "--gpus-per-node=8" -e "--segment=2"
This is useful for quick adjustments or when testing different cluster configurations.
:::

### GPU placement inside the step

On a GRES partition `slurmstepd` overwrites the `CUDA_VISIBLE_DEVICES` sflow exports, so
every step sees the whole allocation and every rank picks device 0 — several tasks that
were planned onto different GPUs all pile onto the same one. sflow therefore no longer
trusts the inherited value. A prelude sourced inside each Slurm step probes the devices
the step can actually see, looks up the physical UUIDs the driver resolved this task's
plan to (`SFLOW_PLANNED_GPU_UUIDS`), and re-exports the indices those same cards have
*here*. Matching by UUID rather than by index is also what fixes pyxis/enroot containers,
which renumber devices from 0.

This is **on by default** for any Slurm task with a `resources.gpus` slice. The one thing
that opts out is srun `gpus_per_task`, because Slurm already carves GPUs per rank there.

- **Audit record.** Each task writes `<task>/sflow_gpus.log` (`sflow_gpus.<node>.log` per
node on multi-node tasks): planned indices and UUIDs, the inherited environment, the
visible index→UUID map, and the final selection. It is deliberately not dot-prefixed so
artifact browsers show it. The `GPU Assignment` section of `sflow_summary.log` is built
from it.
- **Hard failure `exit 97`.** If a planned card is not visible at all, the step holds fewer
GPUs than planned, or a planned slot is out of range, the step aborts with exit code 97
rather than silently running on the wrong device. When Slurm — not sflow — chose the
devices, sflow degrades to index arithmetic instead of failing.
- **Graceful skips.** If `CUDA_VISIBLE_DEVICES` is not a plain comma-separated list of
non-negative integers (for example a workflow variable of that name shadowing it), or the
placement script cannot be staged, sflow warns and falls back to the previous behavior.

:::note
`NVIDIA_VISIBLE_DEVICES` is no longer exported to srun steps. Containers therefore see all
of the node's GPUs — NVML consumers such as `nvidia-smi` and DCGM lose device isolation —
in exchange for the planned host-numbered slice being addressable at all. The Docker
backend is unaffected: it keeps isolation via `--gpus device=<uuid>`.
:::

## Selecting or excluding nodes (all backends)

Restrict which cluster nodes a run may use with two backend-agnostic controls that
Expand Down Expand Up @@ -366,6 +332,11 @@ Two things to know about `SFLOW_WAIT_FOR_GPUS`:

Reservation requires POSIX file locking and is inert on Windows.

To see it work on a real GPU host, `examples/gpu_reservation/` ships a runnable demo
(`demo.yaml`) plus two harnesses: `prove.sh` asserts the four guarantees above
(exact pinning, disjoint concurrent tasks, fail-fast, `--wait-for-gpus`) and exits
non-zero if any fails, and `stress.sh` fires many concurrent runs at one GPU pool.

> **Changed behavior.** Container names gained a driver-PID segment
> (`sflow-<task>-<node>` → `sflow-p<pid>-<task>-<node>`) so concurrent runs on one
> host never collide; scripts matching the old name must be updated. A task that
Expand Down
2 changes: 0 additions & 2 deletions docs/user/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,6 @@ Common options:
- `--account, -A <name>`: Slurm account (auto-detected if not specified)
- `--time <limit>`: time limit (e.g., `02:00:00`)
- `--nodes, -N <count>`: number of nodes. If omitted, single-job and bulk-submit modes derive it from the config's Slurm backend `nodes` field. Bulk-input mode requires either this flag or a CSV node-count column (`SLURM_NODES`, `NUM_SLURM_NODES`, or `NUM_NODES`).

**When the two node counts disagree.** `--nodes` sizes the *sbatch allocation*; the config's own number sizes the *workflow inside it* — the backend's node count and the `match_count` of readiness probes. The same number in both is the normal way to run, but two different numbers means the job is allocated one size and the recipe plans for another: probes wait on a node that was never allocated, or the job holds nodes nothing will ever use. `--bulk-input` **rejects** the run outright (the CSV states the size per row, so a disagreement is unambiguously a mistake) and names up to five offending rows; the config-driven single-job and `--bulk-submit` paths **warn**. A `--set` of the node variable counts as the row's value, since it overrides the CSV cell.
- `--gpus-per-node, -G <count>`: number of GPUs per node for cluster topology. Config `gpus_per_node` wins when present. Applied to sflow validation and planning only, not as a Slurm directive. Use `-e '--gpus-per-node=N'` for `sflow batch`, or backend `extra_args` for `sflow run`, if your cluster requires the Slurm allocation flag.
- `--job-name, -J <name>`: Slurm job name (default: `sflow`)
- `--set, -s KEY=VALUE`: override variables (repeatable)
Expand Down
2 changes: 1 addition & 1 deletion docs/user/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ Beyond `depends_on`, `resources`, `replicas`, and `probes`, a task supports:

- `operator` / `backend`: name (or inline override object) of the operator or backend for this task.
- `ports`: service ports the task exposes (each with `port` and an optional `name`).
- `timeout`: **accepted but not enforced.** Both the per-task and the workflow `timeout` are parsed and merged, but nothing reads them — a task that sets one runs unbounded, and `sflow run` logs a warning naming every task that does. Bound the run with the backend's own limit instead (Slurm `--time`, a Kubernetes `activeDeadlineSeconds`). Kept so existing recipes keep loading.
- `timeout`: per-task timeout (seconds or a string like `30m`); the workflow also has its own `timeout`.
- `fail_fast`: bool, **default depends on the backend** — `true` on Kubernetes (a failed command in a pod should fail the task), `false` on Slurm/local/docker (shell default: only the last command's exit code counts). Leave unset to take the backend default, or set explicitly (`true`/`false`) to override per task. When effective-true, sflow prepends `set -e` to shell-operator scripts so any failed command fails the task. Applies to shell operators only (never `python`, whose script is Python source).
- `variables`: task-scoped variables (same format as top-level `variables`).
- `retries`: retry policy (`count`, `interval`, `backoff`) for a failed task.
Expand Down
10 changes: 0 additions & 10 deletions docs/user/outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,10 @@ After a successful run, `sflow run` prints the output folder, summary path, and
- task duration timeline and task event timeline
- probe traces — the last attempt of every readiness/failure probe — when any task defines probes
- GPU and node usage charts when resource placement data exists
- **`GPU Assignment`** — per task, the physical GPUs it was planned onto next to the devices it actually saw, read back from the placement record the step wrote. When the backend re-indexes devices inside the container (pyxis/enroot renumbering from 0), the section says so rather than leaving you to reconcile two different numbering schemes.
- **`Node Topology`** — the CPU / NUMA / GPU probe each backend captured at reservation time, printed as one `[backend <name>]` block per backend.
- **`External Command Health`** — call counts, failures, timeouts, and mean/max latency for the external commands sflow shells out to (`kubectl`, `srun`, `docker`), plus a list of the slow and failed calls sharing the Timeline's `+elapsed` column. Written even when the run is cancelled or fails, which is when it matters most.
- command-log paths
- workflow DAG and dependency list
- failure hints with task name, attempts, reason, and task log path when a task fails or is cancelled

The task event Timeline also carries an `UNGATED` event for members of a merged Kubernetes pod, marking the moment their in-pod gate opened and the task was allowed to start.

Two companion files are written next to the summary, but only when there is something to say:

- **`command_trace.jsonl`** — one record per *notable* external command (non-zero exit, or slower than 5s). A healthy run, or a run with no notable external-command calls, leaves no file. This is the machine-readable form of the `External Command Health` section; the pre-existing `*_cmds.log` records *what* was issued, this records *how it went*.
- **`loop_stalls.txt`** — if sflow's own event loop stops being scheduled for 30s, sflow logs a warning and dumps every thread's Python stack here, then logs a recovery line with the worst observed lag. Previously such a freeze produced no diagnostic output at all, because sflow's logging runs on the thread that was stuck. Created only when a stall actually happens.

Example `sflow_summary.log`:

```text
Expand Down
13 changes: 5 additions & 8 deletions docs/user/quick-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ and `addressing_style` (`auto` / `virtual` / `path`).
|-------|----------|------|---------|-------------|
| `name` | Yes | string | — | Workflow name. |
| `tasks` | Yes | list | — | List of task definitions (must be non-empty). |
| `timeout` | | string / int | `null` | **Not enforced** — accepted and merged, but nothing reads it; a workflow that sets it runs unbounded and `sflow run` warns. Use the backend's own limit (Slurm `--time`). |
| `timeout` | | string / int | `null` | Workflow-level timeout (e.g. `1h`, `115m`). |
| `variables` | | dict / list | `null` | Workflow-scoped variables (same format as root `variables`). |
| `upload_all` | | object | `null` | Zip the whole workflow output dir and upload it to a `storage` target (see [Workflow Upload-All](#workflow-upload-all)). |
| `monitor` | | object | `null` | Workflow-level hardware monitor (see [Monitor](#monitor)). |
Expand All @@ -336,7 +336,7 @@ and `addressing_style` (`auto` / `virtual` / `path`).
| `backend` | | string / dict | `null` | Backend name, or inline backend override. |
| `depends_on` | | list[string] | `null` | Names of tasks this task depends on. |
| `required_by` | | list[string] | `null` | Reverse dependency: `A required_by: [B]` is folded into `B depends_on: [A]` at load (lets an optional fragment attach to a hub without editing it). |
| `timeout` | | int / string | `null` | **Not enforced** — see [Workflow](#workflow). Bound the task with the backend's own limit instead. |
| `timeout` | | int / string | `null` | Task-level timeout. |
| `variables` | | dict / list | `null` | Task-scoped variables. |
| `resources` | | object | `null` | Node / GPU resource requirements. |
| `replicas` | | object | `null` | Replication configuration. |
Expand All @@ -358,9 +358,8 @@ and `addressing_style` (`auto` / `virtual` / `path`).
| `nodes.indices` | | list[int / expr] | `null` | Specific node indices (e.g. `[0]`). |
| `nodes.count` | | int / expr | `null` | Number of nodes. |
| `nodes.exclude` | | int / list[int] / expr | `null` | Node indices to remove from the placement pool before `indices`, `count`, or GPU packing. |
| `nodes.release_after` | | string | `null` | When node reservations can be reused: `workflow_completion`, `task_ready`, or `task_completion`. sflow reserves nodes **only when this is set explicitly**; omitted, `nodes.indices`/`count` are non-exclusive placement constraints (see note below). |
| `gpus.count` | One of `count` / `indices` | int / expr | `null` | Number of GPUs (sets `CUDA_VISIBLE_DEVICES`). Index-agnostic on its own: the planner packs the task into any contiguous idle GPU run on one node. |
| `gpus.indices` | One of `count` / `indices` | list[int / expr] | `null` | Pin the task to specific **0-based, non-negative, unique** device ids. Combined with `count`, `count` is the total across nodes and `indices` the per-node slice, so the task fans out over `count / len(indices)` nodes. |
| `nodes.release_after` | | string | `workflow_completion` | When node reservations can be reused: `workflow_completion`, `task_ready`, or `task_completion`. sflow reserves nodes **only when this is set explicitly**; omitted, `nodes.indices`/`count` are non-exclusive placement constraints (see note below). |
| `gpus.count` | If `gpus` is set | int / expr | — | Number of GPUs (sets `CUDA_VISIBLE_DEVICES`). |
| `gpus.release_after` | | string | inferred | When GPU reservations can be reused: `workflow_completion`, `task_ready`, or `task_completion`. |

For nodes, `release_after` only creates an exclusive node reservation when explicitly set; omitted `nodes.indices` and `nodes.count` are placement constraints and may overlap with other planned tasks. For GPUs, omitted `release_after` is inferred: tasks without readiness probes release GPUs after task completion for downstream dependents, while tasks with readiness probes keep GPUs until workflow completion unless explicitly set to `task_ready`. `task_ready` releases after readiness succeeds. `task_completion` releases after any terminal task status (`COMPLETED`, `FAILED`, `TIMEOUT`, or `CANCELLED`). Dry-run rehearses these resource lifetimes across the DAG.
Expand Down Expand Up @@ -490,9 +489,7 @@ To publish a metric literally named `file`, use `patterns:`; a top-level `file:`
| `interval` | | int | `5000` | Sampling interval (ms) for built-in scopes without their own. |
| `scopes` | | object | `null` (all) | Which scopes to collect — `cpu`, `gpu`, `memory`, `disk`, `network`, `custom`; omit ⇒ all built-ins active. |
| `resources` | | object | `null` | Which hardware to target — `nodes` / `gpus` (like task resources) or `used_by_tasks: [names]`. |
| `report` | | object | `{enabled: true}` | Post-run report. `format`: any of `csv`, `svg`, `png` (`png` needs the `sflow[monitor]` extra). |
| `report.enabled` | | bool | `true` | **On by default** whenever `monitor:` is set. Set `false` to opt out — worth doing on large fan-outs, since report cost scales with samples × views. Raw samples and the overview are still written. |
| `window` | | object | `null` | `{start, end}` task-log markers bounding the reported window, so the report covers the benchmark rather than the whole task lifecycle. Task monitors only (rejected on `workflow.monitor`) and requires `report.enabled: true`. |
| `report` | | object | `null` (csv + svg) | Opt-in post-run report `format`: any of `csv`, `svg`, `png` (`png` needs the `sflow[monitor]` extra). |

## Expression Syntax

Expand Down
Loading
Loading