Skip to content

feat(profiling): nsight-slurm profiling type (Nsight Cloud for Slurm) - #419

Draft
nv-yna wants to merge 23 commits into
NVIDIA:mainfrom
nv-yna:yna/nsys-nsight-slurm
Draft

nv-yna wants to merge 23 commits into
NVIDIA:mainfrom
nv-yna:yna/nsys-nsight-slurm

Conversation

@nv-yna

@nv-yna nv-yna commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds profiling.type: nsight-slurm, which launches every worker srun step through the
Nsight Cloud for Slurm wrapper
(nsight-slurm srun ... -- <cmd>) instead of prefixing the worker command with nsys profile.
One coordinator per job collects time-aligned per-rank Nsight Systems reports into a shared report
workspace (<log_dir>/nsight-slurm-reports, with manifest and common collection ids).

Companion to #418 (playbook-flag nsys path). Both are independent of observability.enabled,
so the profiling overhead can be measured against the default-mode baseline (job 4 / PR #417).

Stacked on #417 (yna/observability-combined); the diff vs that branch is the last commit.

What changes

  • schema (src/srtctl/core/schema.py): ProfilingType.NSIGHT_SLURM; fields
    nsight_slurm_home, nsight_slurm_tool_path (default /usr/local/bin/nsys),
    nsight_slurm_profiling_mode (at-launch | manual | cuda-api, default cuda-api),
    nsight_slurm_tool_options (default = the TRT-LLM profiling playbook set minus connector-owned
    flags), nvtx_injection_path. Worker env carries the TLLM_PROFILE_START_STOP window plus
    TLLM_PROFILE_LOG_RANKS=all, DYN_ENABLE_RUST_NVTX=1, NVTX_INJECTION64_PATH.
    Validation: absolute home, known mode, connector-owned flags (-o, --force-overwrite,
    -c/--capture-range*, --start-later) rejected, cuda-api requires backend.type: trtllm.
  • start_srun_process (src/srtctl/core/slurm.py): optional srun_launcher /
    launcher_env. With a launcher, native srun options are forwarded unchanged and terminated by
    --; --export=... is omitted (the wrapper appends its own --export=ALL) and the export vars
    ride in the wrapper's process environment.
  • NsightSlurmStageMixin (src/srtctl/cli/mixins/nsight_slurm_stage.py): before the first
    worker step, once: configure tool-path / tool-command profile / profiling-mode / tool-options / report-output, enable pyxis, coordinator start; coordinator stop --force in sweep cleanup.
    All wrapper calls logged to <log_dir>/nsight-slurm.out; SLURM_SUBMIT_DIR redirected to the
    log dir so the wrapper's job state lives under the run.
  • worker_stage: both srun launch paths take the launcher kwargs.
  • docs/profiling.md: nsight-slurm section.
  • dry-run (src/srtctl/cli/submit.py): Profiling panel, incl. the nsight-slurm launcher rows.
  • teardown (NsightSlurmStageMixin.flush_nsight_slurm, a ProcessRegistry pre-cleanup hook): end the nsys
    sessions gracefully and wait for the reports, since cuda-api mode only writes them when the session ends.
  • mounts (start_srun_process(extra_container_mounts=...), ProfilingConfig.nsight_slurm_container_mounts):
    the connector's mounts ride in srtctl's single --container-mounts; disable pyxis on the wrapper.

Tests

  • tests/test_profiling_nsight_slurm.py (13): config helpers, env, validation (incl. cuda-api
    backend gate), launcher argv/env through a mocked Popen, stage sequencing through a fake
    <home>/bin/nsight-slurm that records argv/env/cwd, idempotence, missing-install error.
  • Full suite: 1801 passed, 2 skipped (19 nsight-slurm tests incl. the registry hook race and the growing-report wait, + 3 dry-run tests). ruff check / ruff format --check clean on src/srtctl.
    (pre-commit's pinned ruff v0.8.4 flags two pre-existing UP038 lines in do_sweep.py:239 /
    schema.py:2045 that predate this branch; left untouched.)

Status / open items

  • End-to-end run on hecate. Tool installed for aarch64 on lustre (nsight-slurm 0.13.3, superproject
    d9a7d30, coordinator df7e496, nsight-cloud-storage 57a1570; job 571136). First profiling job 571147
    configured the wrapper, started the coordinator and ran the connector inside every worker container, but
    the 4 ranks on the coordinator's own node hit [Nsight ERROR] Timeout connecting to Coordinator while all
    28 remote ranks joined; the step exited 1 and srtctl aborted the sweep.
    Root cause (verified on a compute node with a pyzmq probe): the wrapper publishes
    tcp://<SLURMD_NODENAME>:<port>; broker and client set zmq.IPV6=1, so ZMQ resolves with
    AF_INET6|AI_V4MAPPED and connects to the first result only. On hecate a node's own name resolves to
    link-local fe80::… addresses (no v4-mapped entry at all), so tcp://<own name>:<port> times out
    while tcp://<ipv4>:<port> and remote names work. Fix 24aa66d: srtctl sets
    SLURMD_NODENAME=<head_node_ip> for the wrapper CLI calls so the coordinator address is an IPv4 literal.
    (Upstream note for nsight-cloud-slurm: publish an IP literal, or iterate over resolved addresses.)
    Rerun 571265: all 32 ranks joined the coordinator, but the workers never came up: the wrapper's
    enable pyxis mode appends a second --container-mounts, and pyxis applies only the last one
    (verified on a 1-node allocation), so /model, /logs and /configs were gone inside the containers and the
    dynamo install preamble failed. Fix 62df1ef: srtctl no longer uses enable pyxis; it mounts the install
    root (ro), the connector onto /usr/local/bin/nsight-slurm-connector, and the log dir at its host path
    inside its own single --container-mounts (new start_srun_process(extra_container_mounts=...)).
    Verified on 1 node: connector starts, /configs visible, app runs.
    Run 3 (571904): all 32 ranks joined, workers came up, both capture windows ran (connectors logged the
    nsys RangeCollection state), but (a) prefill worker 1 hit the same TRT-LLM hang as PR feat(profiling): adopt the Dynamo Benchmark Playbook nsys recipe for TRT-LLM workers #418's runs, at the
    same prefill iteration 2324 (deterministic in the warmup stream with nsys attached), and (b) 0 reports:
    in cuda-api mode the wrapper keeps the nsys session open after the range (--capture-range-end repeat), so
    reports only exist when the session ends, and srtctl's teardown SIGKILLed the steps. Fix 58477a8:
    flush_nsight_slurm() before registry.cleanup() (nsight-slurm stop --job, then SIGTERM to the wrapper
    processes of the worker steps, then wait for *.nsys-rep to appear and settle).
    Run 4 (572233): same hang (prefill iteration 2319), and the flush ran too late: the critical-process
    monitor thread calls registry.cleanup() itself. Also, the connector keeps nsys output in its runtime
    workspace (container-local /tmp) and only copies it out on a collection-stop it never sees for the
    nsys 2026.3 range states (RangeCollection/RangeGeneration are unknown to connector 1.5.0).
    Fix b009d49: ProcessRegistry.add_pre_cleanup_hook() (flush runs before any teardown path kills steps)
    and the runtime workspace bind-mounted from <log_dir>/nsight-slurm-runtime at /nsrt (AF_UNIX socket
    path limit) with rescue-copy of *.nsys-rep into <report_root>/rescued/.
    Run 5 (572590): decode hang at iteration 7624 (= PR feat(profiling): adopt the Dynamo Benchmark Playbook nsys recipe for TRT-LLM workers #418's run 1), and two more findings: (a) the finally
    block and the monitor thread raced into cleanup(), the second caller skipped the hook and killed the steps;
    (b) the lustre-backed runtime workspaces (32 rank dirs, ipc socket fine) stayed empty through
    RangeCollection/RangeGeneration: without -o, cuda-api nsys writes nothing until the session ends.
    Fix 5261265: hooks run under a dedicated lock (concurrent cleanup() waits), and the tool-options carry
    -o <report_root>/direct/%q{SLURM_JOB_ID}_%q{SLURMD_NODENAME}_rank%q{SLURM_PROCID} so every rank writes
    straight to the shared filesystem at session end / SIGTERM.
    Runs 6-9 (572866, 572948, 572964, 572999) never reached the workers: hecate jobs had lost their Kerberos
    credential on the compute nodes. After re-auth:
    • 575849: the wrapper refused a non-empty report workspace (our pre-created direct/) -> 576229e moves the
      direct output next to it.
    • 576022: flush now runs first, but SIGTERM to the wrapper process is relayed to srun, which SIGKILLs the
      step -> c1e3725 signals the connector tasks (nsight-slurm-connector steps) with --signal=TERM.
    • 576684: connector tasks got SIGTERM, workers exited, but nsys waited for re-parented children -> ad4d2a2
      adds --wait=primary.
    • 576975: nsys now writes reports into <log_dir>/nsight-slurm-direct/ (11 files appeared, progress bars in
      the logs), but the flush judged the file COUNT settled after 20 s and cleanup killed nsys mid-write (all
      0 bytes) -> 89c0353 settles on bytes, requires non-empty reports, settle 60 s / timeout 600 s.
      Validation run of 89c0353 pending (needs a fresh cluster login). Every run so far also hit the TRT-LLM hang
      from PR feat(profiling): adopt the Dynamo Benchmark Playbook nsys recipe for TRT-LLM workers #418 (prefill iteration ~2324 or decode 7624), which is what triggers the teardown flush.
  • srtctl dry-run shows a Profiling panel (type, windows, launcher, tool-options, report-output) — dc42fa8.

🤖 Generated with Claude Code

nv-yna and others added 23 commits August 27, 2026 12:34
…n metrics

Extends the /proc host sampler from orchestrator-node-only to every
allocated node, and adds the scheduler-level fields needed to attribute
host-CPU interference to a remedy from a single baseline run:

- per-process run_delay_ns (/proc/pid/schedstat): cumulative run-queue
  wait — the direct contention signal that CPU pinning remedies
- per-process nr_migrations (/proc/pid/sched): cross-core churn,
  near-zero when pinned
- per-process affinity_ncpus (sched_getaffinity): direct pinning-state
  observable (144 = floating, 36 = pinned rank on GB200/GB300)
- host procs_running/procs_blocked and a t_mono companion timestamp
  (cross-node wall clocks have been observed seconds apart)

Collection: host_sampler.py gains a stdlib-only standalone CLI mode
(one file per node: host_samples_<node>.jsonl); benchmark_stage launches
it on all non-orchestrator nodes via one srun --overlap per het group,
gated by the new observability.host_sampler_all_nodes knob (default
true, follows observability.enabled). This closes the gaps where worker
nodes had no per-process host telemetry and a dedicated frontend node
had none at all.

Ingest: host_series.json gains a per-node hosts map plus
run_delay_ms_per_s / migrations_rate / affinity_ncpus / procs_runnable
series; rate denominators prefer the monotonic clock so NTP steps can't
skew them. docs/host-attribution-metrics.md documents the metric set
and the pinning-vs-placement attribution logic.

Teardown hardening from adversarial review: benchmark proc creation
moved inside the try so sampler sruns can't leak when placement/launch
raises; sampler teardown escalates via terminate_and_reap and logs
early-exited samplers; the standalone mode exits nonzero if its sampler
thread dies; the sampler never samples itself.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…idation matrix

Replaces the draft rubric with measured thresholds from 7 validation runs
(2x baseline, 2x pinned, 3x dedicated-frontend at DSV4 c1010 on GB300):
clean-node run-delay floor 0.00-0.01 ms/s; shared-node asymmetry 140-350x
(persisting at 190x under pinning, collapsing to 1x in all three
dedicated-frontend runs); affinity_ncpus as the direct pinning observable
(full-width vs CPUs-per-GPU), unchanged by placement. Adds the operator
three-step decision flow and expected-gain guidance (+4.0% pinning on
288-CPU nodes / +1.1% on 144-CPU; +0.5-0.85% placement).

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…ault

The Prometheus surface Tachometer captures says nothing about the processes
underneath it: Dynamo publishes no process_* or thread metrics and node_exporter
only sees the machine. The AgentX/VR200 frontend spin (fastokens 0.3.1 sizing its
BPE rayon pool to all 352 CPUs -> 352 idle workers burning ~110 cores) was
invisible in every scraped family and was found with sacct plus an out-of-band
per-thread /proc sampler.

Add ncabatoff/process-exporter (pinned multi-arch image, port 9256) as a third
built-in exporter, launched on every node that hosts a backend rank OR a frontend
replica -- the frontend node is the one the per-node exporters skip when the
frontend is head-placed or dedicated, and it is where frontend CPU pathologies
live. Groups come from <log_dir>/process-exporter.yml written at launch
(frontend, dynamo_trtllm/sglang/vllm handlers + ranks, the MPI launcher, the
benchmark client, infra daemons; first match wins, no catch-all). -threads=true
publishes per-thread-name CPU and thread counts, so a runaway pool shows up as a
step in thread_count and a CPU cluster on one thread name. Endpoints are scraped
unfiltered so groupname/threadname/mode labels pass through to the parquet and
to server_metrics_export.jsonl unchanged.

node_exporter additionally gets --collector.processes (host-wide thread total,
process states, procs_running/blocked).

An explicit process_exporter block overrides the image/port/command, and
default_exporters: false still disables all built-ins. Dry-run lists the new
exporter; docs/config-reference.md documents it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
The tachometer node_exporter launch enabled only cpu/infiniband/meminfo, and
the host sampler read /proc/stat + /proc/meminfo but no PSI. The result: none
of the scheduler-pressure signal the retired steady_probe.sh sampler carried
(PSI stall time, procs_running/blocked, context-switch and page-fault/reclaim
counters, per-NUMA free memory) was collected anywhere -- exactly the class of
signal that distinguishes a busy box from one whose work is blocked waiting.

Two complementary changes, both cheap procfs/sysfs reads with no measured
decode-latency cost (unlike dense NVML sampling):

- node_exporter (all worker nodes): add the stat, vmstat, pressure, and
  meminfo_numa collectors. The vendored NodeExporterFilter passes the new
  families through its default arm, so no scraper change is needed. Extracted
  into tachometer_node_exporter_command_template() mirroring the DCGM template;
  an explicit recipe node_exporter.command still wins.
- host_sampler (orchestrator/head node, which gets no exporter): add a
  /proc/pressure/{cpu,memory,io} PSI read to each sample. This is that node's
  only PSI source and covers the frontend node the exporters never reach.

Tests: node-exporter template asserts the four new collectors and
explicit-command override; host_sampler PSI parse + CONFIG_PSI-absent cases.

Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…-setup binary

The upstream ncabatoff/process-exporter image is FROM scratch: no shell,
no /root. pyxis/enroot on hecate refuses to start it
(`enroot-switchroot: failed to change directory: /root`, then
`/bin/sh: No such file or directory` once the home is mounted), so the
first run from this branch (hecate 565854) had to relaunch the exporter
by hand as a bare binary. The exporter is a static Go executable that
needs neither a container nor privileges, so ship it the way nats-server
and etcd are shipped: `make setup ARCH=<compute_arch>` downloads the
release tarball into configs/process-exporter and start_tachometer runs
it under plain srun with host paths for the binary and its group file.

- TelemetryExporterConfig.binary: host-native launch mode. Relative
  paths resolve against SRTCTL_SOURCE_DIR / the checkout root, where
  make setup installs host binaries; container_image is ignored.
- DEFAULT_PROCESS_EXPORTER now points at configs/process-exporter with
  an empty container_image. Validation requires binary or
  container_image for every exporter, process_exporter included.
- Missing binary (checkout whose make setup predates this) skips the
  process-exporter leg with a warning instead of failing the run;
  `srtctl apply` warns at submit time as well.
- An explicit process_exporter.container_image with binary unset keeps
  the container launch (group file via the /logs mount).
- Makefile setup step (PROCESS_EXPORTER_VERSION ?= 0.8.7), .gitignore,
  dry-run row shows "host binary configs/process-exporter :9256",
  docs/config-reference.md, tests for both launch modes, the skip path,
  binary resolution and validation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
# Conflicts:
#	src/srtctl/cli/mixins/telemetry_stage.py
…xporterFilter

Two node_exporter families enabled by the combined observability branch lost
their only distinguishing label in the vendored scraper filter, so every series
of the family collapsed into one metric name in the parquet:

- node_memory_numa_*{node="N"} (--collector.meminfo_numa, NVIDIA#415): the generic
  `memory_` arm emitted the bare metric name, folding all NUMA nodes together.
  Now `memory_numa_<x>{numa_node=N}`; host-wide meminfo stays label-free.
- node_processes_state{state="R"|"S"|"D"|...} and
  node_processes_threads_state{thread_state=...} (--collector.processes, NVIDIA#413):
  the default arm keeps only a fixed label whitelist that had neither key.
  `state` and `thread_state` are added to the whitelist.

Label-free stat/vmstat/pressure families were already passed through unchanged;
a test pins that too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…docs/comments for the collector union

Post-merge fixups from the three-PR integration review:

- benchmark_stage._start_remote_host_samplers hardcoded `--interval 2` (NVIDIA#356
  predates b83dc4a, which made the in-process sampler follow
  observability.tachometer.collect_interval_ms). Derive the remote interval
  from the same knob, clamped to >= 1 s like HostSampler, so head-node and
  remote host_samples rows share one cadence. Test covers default, 2 s, 5 s,
  sub-second clamp and 1.5 s.
- docs/config-reference.md node_exporter row lists the eight collectors of the
  union and the widened vmstat field filter (it still named NVIDIA#413's four).
- telemetry_stage.py NODE_EXPORTER_COLLECTORS comment no longer claims "no
  scraper change is needed": the filter keeps numa_node / state / thread_state
  since the previous commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…urm)

Launch every worker srun step through the nsight-slurm wrapper instead of
prefixing the command with `nsys profile`, so one coordinator per job
collects time-aligned per-rank Nsight Systems reports into a shared report
workspace. Independent of `observability.enabled`.

- schema: `ProfilingType.NSIGHT_SLURM`, fields `nsight_slurm_home`,
  `nsight_slurm_tool_path`, `nsight_slurm_profiling_mode`
  (at-launch | manual | cuda-api), `nsight_slurm_tool_options`,
  `nvtx_injection_path`; default tool options = the TRT-LLM profiling
  playbook set minus connector-owned flags. Validation: absolute home,
  known mode, connector-owned flags rejected, cuda-api requires the
  TRT-LLM backend (TLLM_PROFILE_START_STOP trigger). Worker env gets the
  iteration window plus TLLM_PROFILE_LOG_RANKS, DYN_ENABLE_RUST_NVTX and
  NVTX_INJECTION64_PATH.
- slurm.start_srun_process: optional `srun_launcher` / `launcher_env`.
  With a launcher the native srun options are forwarded unchanged and
  terminated by `--`; `--export` is omitted (the wrapper appends its own
  `--export=ALL`) and the export vars ride in the wrapper's process env.
- NsightSlurmStageMixin: idempotent `configure tool-path / tool-command /
  profiling-mode / tool-options / report-output`, `enable pyxis`,
  `coordinator start` before the first worker step; `coordinator stop
  --force` in sweep cleanup; all calls logged to <log_dir>/nsight-slurm.out
  with SLURM_SUBMIT_DIR redirected to the log dir.
- worker_stage: both srun launch paths pick up the launcher kwargs.
- docs/profiling.md: nsight-slurm section. Tests: tests/test_profiling_nsight_slurm.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…wrapper

`srtctl dry-run` rendered nothing about `profiling`, although it changes how every
worker step is launched (nsys prefix, or with `type: nsight-slurm` the wrapper
`<home>/bin/nsight-slurm srun ... -- <cmd>` plus its pyxis mounts and --export=ALL).
Add a Profiling panel: type, per-phase step windows, NVTX injection path, and for
nsight-slurm the launcher, install root, tool-path, profiling-mode, effective
tool-options and report-output. Tests in tests/test_dry_run.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…teral

The wrapper derives the coordinator address from SLURMD_NODENAME (or the fqdn),
so connectors resolve tcp://<head hostname>:<port> themselves. Both the broker
and the client run their ZMQ sockets with IPV6=1, and ZMQ connects only to the
first address a name resolves to (glibc returns v4-mapped addresses only when
the name has no IPv6 at all). On hecate (job 571147) the 4 ranks that ran on
the coordinator's own node resolved its name to a link-local IPv6 and timed out
("Timeout connecting to Coordinator"), while all 28 remote ranks joined; the
step exited 1 and srtctl aborted the sweep.

Set SLURMD_NODENAME=<runtime.head_node_ip> for the wrapper CLI calls (configure,
enable pyxis, coordinator start/stop) only, so the published command/announce
addresses are IPv4 literals. The wrapper-launched srun steps keep their own
environment. Test asserts the override reaches every wrapper call and does not
leak into launcher_env.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…yxis`

pyxis applies only the last --container-mounts flag it receives (SPANK options reach
slurmstepd as one environment variable per option). `nsight-slurm enable pyxis` appends
its own --container-mounts after the caller's, so every wrapper-launched worker lost the
job's /model, /logs and /configs mounts: on hecate (jobs 571147, 571265) the dynamo
install preamble found no wheel cache under /configs, tried to clone dynamo and exited 1.
Verified on a 1-node allocation: two --container-mounts flags -> only the second one is
visible in the container; wrapper with pyxis disabled and caller-supplied mounts -> the
connector starts, /configs is visible and the application runs.

- start_srun_process(extra_container_mounts=...): raw "src:dst[:flags]" specs merged into
  srtctl's single --container-mounts value.
- ProfilingConfig.nsight_slurm_container_mounts(log_dir): install root read-only, the
  connector file onto /usr/local/bin/nsight-slurm-connector (the wrapper prefixes the bare
  command name without pyxis management; /usr/local/bin is on the image PATH), and the
  log dir at its host path (wrapper config dir and report root live under it).
- NsightSlurmStageMixin: `disable pyxis` instead of `enable pyxis`; launch kwargs carry the
  extra mounts. Docs and tests updated.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…steps

In cuda-api mode a capture range closes at cudaProfilerStop, but the wrapper keeps
the nsys session open for the next range (--capture-range-end repeat), so a report
only exists once the session ends. srtctl's teardown cancelled the steps with
SIGKILL (hecate job 571904: capture ranges observed on all 32 ranks, zero reports
on disk).

Add NsightSlurmStageMixin.flush_nsight_slurm(registry), called from the sweep's
finally block right before registry.cleanup(): `nsight-slurm stop --job <id>`
(coordinator -> connectors -> nsys stop), and if no *.nsys-rep appears within
first_wait_s, a plain SIGTERM to the wrapper processes of the worker steps
(wrapper -> srun -> connector -> nsys writes its report on SIGTERM while the
application keeps running). Then wait for the report files to appear and settle
(bounded by timeout_s). Best effort, never raises. Tests cover both paths and the
no-op without a coordinator.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
… reports on lustre

Run 572233 showed two more gaps:

1. The critical-process monitor thread (and the signal handler) call
   registry.cleanup() directly, so the worker steps were SIGKILLed before the
   sweep's finally block reached flush_nsight_slurm(). Add
   ProcessRegistry.add_pre_cleanup_hook(): hooks run once, before the first
   cleanup() terminates anything, on every teardown path. do_sweep registers
   the nsight-slurm flush there.

2. In cuda-api mode the connector writes nsys output into its runtime workspace
   and only copies it to the report root when its state machine sees a
   collection stop; with the nsys 2026.3 agent the range states
   (RangeCollection / RangeGeneration) are unknown to connector 1.5.0, so the
   per-range reports stayed in the container-local /tmp and died with the step.
   Bind-mount <log_dir>/nsight-slurm-runtime at the short in-container path
   /nsrt (the connector binds an AF_UNIX callback socket under it; sockaddr_un
   caps the path at 107 bytes, verified on a hecate node) and hand it to the
   connector via NSIGHT_SLURM_RUNTIME_DIR in the launcher environment. The flush
   rescue-copies *.nsys-rep from that workspace into <report_root>/rescued/
   before signalling the connectors (their cleanup deletes the workspace) and
   again at the end.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…d teardown flush

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…se the pre-cleanup hook

Run 572590: the pre-cleanup hook ran, but the finally block and the critical-process
monitor thread called registry.cleanup() within the same second; the second caller saw
the hook as "done" and killed the steps while the flush was still waiting. Hold a
dedicated lock for the whole hook run so concurrent cleanup() callers wait.

The connector runtime workspace on lustre (/nsrt) held 32 rank dirs but their reports/
stayed empty through RangeCollection/RangeGeneration: in cuda-api mode nsys gets no -o
and writes nothing until the session ends. Pass
`-o <report_root>/direct/%q{SLURM_JOB_ID}_%q{SLURMD_NODENAME}_rank%q{SLURM_PROCID}`
in the wrapper tool-options (neither the wrapper CLI nor the connector rejects -o; only
--start-later is checked), so each rank's report lands on the shared filesystem at
session end or on SIGTERM, independent of the connector's scratch/upload state machine.

flush_nsight_slurm(): rescue-copy scratch reports on every poll; send SIGTERM to the
worker wrappers unless NEW reports appeared after `nsight-slurm stop` (pre-existing or
rescued files do not prove the sessions ended). Tests updated; registry race covered.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…'s report workspace

`nsight-slurm srun` refuses to start when its report-output directory is non-empty
without its own .nsight-cloud-storage.conf marker; pre-creating <report_root>/direct/
for the `-o` target made every worker step exit 2 (hecate job 575849). Write the
direct reports to the sibling <log_dir>/nsight-slurm-direct/ instead; the flush counts
reports in both places.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…tasks, not the wrapper

Hecate job 576022: the flush now ran before cleanup, but SIGTERM to the wrapper
process is relayed to its srun, and srun cancels the step with SIGKILL
("STEP ... CANCELLED ... DUE to SIGNAL Killed"), so nsys never wrote a report.

The wrapper-launched worker steps are named after their task command,
`nsight-slurm-connector`. The flush now lists them with `squeue -s -j <job>` and
sends the Slurm signal command with `--signal=TERM <job>.<step>`, which reaches
only the tasks: the connectors terminate nsys gracefully (`--kill none` leaves the
application running) and nsys writes its `-o` report. Tests cover the step
selection and that wrapper processes are never signalled.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
…the worker exits

Hecate job 576684: the teardown flush now reaches the connector tasks (step
"CANCELLED ... DUE to SIGNAL Terminated"), the worker shuts down gracefully, and
nsys then prints "The target application terminated. One or more process it
created re-parented. Waiting for termination of re-parented processes." and never
finalises before cleanup kills it. Add --wait=primary to the tool options so nsys
processes its events and writes the -o report as soon as the primary process exits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
Hecate job 576975: with --wait=primary the SIGTERM flush finally made nsys write
reports into the direct output dir, but nsys creates every .nsys-rep as an empty
placeholder first and fills it over minutes ("Processing events... [5%]"). The
flush saw 11 files appear at once, judged the count stable after 20 s, and cleanup
killed nsys mid-write.

Settle on (file count, non-empty count, total bytes) instead, require every report
to be non-empty, and raise the defaults to settle 60 s / timeout 600 s. Test with a
report that grows after the signal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Yuewei Na <nv-yna@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant