Skip to content

Observability and Telemetry

The observability: preset (nsys, OTEL, Tachometer) and the telemetry: providers (GPU and CPU power). Profiler settings: Profiling.

observability

Dynamo request-lifecycle OTel tracing is on by default, including when observability.enabled: false or the entire block is omitted. It applies only to the Dynamo stack (frontend.type: dynamo). No enable_otel, otel_endpoint, or DYN_LIFECYCLE_TRACE_ENABLED recipe setting is needed.

The implied otel-collector service starts on the head node before workers, receives OTLP/gRPC on port 4317, and saves <log_dir>/otel/traces.jsonl and logs.jsonl. Dynamo frontends and workers receive DYN_LIFECYCLE_TRACE_ENABLED=1, OTEL_EXPORT_ENABLED=1, the head-node OTLP endpoint, and a component service name. The collector stops after the serving processes so they can flush spans. This uses Dynamo's native dynamo.request_lifecycle instrumentation (Dynamo PR #14101); the installed Dynamo build must include it. The default lifecycle mode is Dynamo's core mode.

make setup ARCH=<compute_arch> installs the pinned, checksum-verified otelcol-contrib host binary. A missing binary fails startup with a setup hint. An air-gapped installation can set services[name: otel-collector].options.binary to a shared executable; options.port changes both its listener and the automatic endpoint. srtctl dry-run shows the implied collector regardless of the analytics level. DSight discovers the saved OTLP JSONL when building the report.

Explicit recipe environment still wins. The existing enable_otel: true plus otel_endpoint redirects traces to that external endpoint, so those traces are not saved locally. To opt out of automatic collection, declare the otel-collector service with enabled: false; this also removes its default lifecycle/export environment. Neither option changes other stacks' defaults.

Tachometer collection is on by default for every run (no configuration needed; observability.tachometer.enabled: false opts out). observability.enabled turns on the server metrics content (the TRT-LLM publish flag and engine statistics) and the trace surfaces:

observability:
  enabled: true

The capture window aligns with the load, the same window the benchmark client's own AIPERF_SERVER_METRICS_URLS polling covers: on benchmark runs the scraper starts once the server passes the health gate (bring-up produces only dead-endpoint noise while workers load) and is stopped gracefully when the client exits, with a configurable grace period for compacting final.parquet. Runs without a discrete load window (serve-only, manual, eval-only) capture the whole serve session as before. Signal handlers and the critical-process monitor still use the process registry's existing teardown budget; the benchmark shutdown grace does not override those paths.

Tachometer scrapes all configured worker, frontend, DCGM, and node-exporter endpoints, independently of the benchmark client's AIPERF_SERVER_METRICS_URLS polling. This keeps the raw capture complete even when the client also collects metrics. In a Dynamo sidecar job a worker process can have two targets, the sidecar's runtime metrics (backend_{mode}{index}_rank{rank}) and the native engine's own (..._engine); see Native sidecars.

The legacy in-job Python RAW scraper is retired: a recipe still carrying scrape_metrics, scrape_interval_seconds, or scrape_output fails validation at submit time.

Fields: ObservabilityConfig, NsysObservabilityConfig. tachometer.enabled: null (the default) collects on every run, independent of observability.enabled; explicit false opts out. See Profiling for the nsys settings.

observability.enabled: true also enables nsys with NVTX tracing and CPU sampling on every frontend and worker process/rank. Its default nsys.capture_window: measured_workload starts collection after warmup and stops it when the measured workload finishes. Supported bundled runners call the boundary hooks automatically; custom/manual clients must call them at their own phase boundaries. Set nsys.capture_window: including_startup to include startup and warmup through teardown, or nsys.enabled: false to opt out. An enabled top-level profiling mode takes precedence. The serving container must provide nsys and the required NVTX support. See Observability capture for timing, sampling, injection, and report-finalization settings.

SGLang workers always receive --enable-metrics unless the recipe sets it: native sglang.launch_server serves /metrics only with the flag, and dynamo.sglang merges the engine's sglang:* series into its system-port /metrics only when SGLang was started with it.

Tachometer collects every worker rank, frontend, DCGM, node, and process metrics by default (minus the client-polled complement described above); the exporters launch from pinned multi-arch registry images with no configuration. Air-gapped clusters override the images via the containers: alias map in srtslurm.yaml; default_exporters: false disables the built-ins:

observability:
  enabled: true
  tachometer:
    enabled: true
    collect_interval_ms: 1000
    sync_interval_secs: 120
    compaction_threads: 4
    storage_subdir: tachometer
    extra_metadata:
      cluster: production
    dcgm_exporter:
      container_image: /containers/dcgm-exporter.sqsh
      port: 9400
    node_exporter:
      container_image: /containers/node-exporter.sqsh
      port: 9100
    process_exporter:
      binary: /opt/srt/configs/process-exporter   # host-native (default mode); or set container_image instead
      container_image: ""
      port: 9256

Every Tachometer field: TachometerConfig. The ones whose behavior needs more than a line:

Tachometer field Type Default Description
collect_interval_ms int 1000 Milliseconds between scrapes of every endpoint; the single cadence knob. It also drives the launched DCGM exporter's --collect-interval (an explicit dcgm_exporter.command wins) and the host sampler. Values below 1000 speed up DCGM NVML sampling and are warned about at launch: 100ms sampling measured ~2% decode ITL overhead on GB300. Replaces the retired Hz-based default_frequency
shutdown_grace_secs float 120.0 Time the scraper gets after SIGTERM to flush and compact final.parquet before it is killed; compaction scales with the data accumulated since the last periodic sync
default_exporters bool true Imply the built-in DCGM + node + process exporters when no explicit blocks are set. The exporters are services (dcgm-exporter, node-exporter, process-exporter); declaring one under services: by that name overrides it, and srtctl dry-run lists them
dcgm_exporter object/null built-in Defaults to nvcr.io#nvidia/k8s/dcgm-exporter:3.3.9-3.6.1-ubuntu22.04 on port 9401; an explicit block overrides
node_exporter object/null built-in Defaults to quay.io#prometheus/node-exporter:v1.8.2 on port 9101 with the cpu, infiniband, meminfo, processes, stat, vmstat, pressure and meminfo_numa collectors on worker nodes. Includes major faults and page-reclaim counters; retains process-state and NUMA-node identity. An explicit block overrides
process_exporter object/null built-in Defaults to the host-native configs/process-exporter binary (ncabatoff/process-exporter 0.8.7, installed by make setup for the compute arch, like configs/nats-server and configs/etcd) on port 9256, launched with plain srun (no container) on every allocated node (the process-exporter service, placement.node: all). Reads the host /proc and publishes per-process-group CPU seconds by mode, thread count, per-thread-name CPU and count (-threads=true), context switches, RSS and open fds. PSS is not collected (-gather-smaps=false): reading smaps_rollup walks each process's page tables on every scrape, and on B300 nodes whose engine ranks keep ~175 GiB of KV-offload host memory resident that cost 7-8 CPU-seconds per scrape and about 11% of serving throughput. RSS comes from /proc/<pid>/stat, which the kernel serves from approximate per-CPU counters, so a small group's RSS can be off in either direction (even 0) on hosts with many CPUs. To also publish PSS (memtype="proportionalResident", counted exactly from the page tables), declare the service as {name: process-exporter, type: process-exporter, args: ["-gather-smaps=true"]}; the declared entry replaces the implied one, so a binary or port set in this block moves to the entry's options.binary / options.port. The default flags need process-exporter 0.7.0 or later. The passthrough filter retains metric names and labels, including summary sum/count suffixes, and attaches hostname and run metadata to raw rows. Groups come from <log_dir>/process-exporter.yml, written at launch: the Dynamo frontend, then the groups of every engine the recipe runs (each backend's process_exporter_groups: TRT-LLM's trtllm_llmapi_launch launcher, trtllm_engine children, dynamo_trtllm handler and trtllm_serve; SGLang's sglang_scheduler / sglang_dp_controller / sglang_detokenizer engine processes and dynamo_sglang; vLLM's vllm_engine_core / vllm_worker / vllm_api_server / vllm_dp_coordinator engine processes, vllm_serve and dynamo_vllm), then vllm_router, the benchmark clients and the infra daemons. SGLang and vLLM engine processes are matched on their retitled command lines (vLLM's default VLLM:: title prefix). If the binary is missing the leg is skipped with a warning (submit warns too). An explicit block may set binary (absolute, or relative to the srtctl checkout) or instead a container_image with binary unset to run it containerized; the upstream FROM scratch image is not used by default because pyxis/enroot on some clusters cannot start shell-less images

Every exporter block (TelemetryExporterConfig) accepts container_image, port, command and binary, and GPU exporters also kind (dcgm or custom), gpu_labels and gpu_metrics. binary selects host-native launch (the executable runs directly under srun, container_image is ignored and may be ""); without it the exporter runs from container_image. One of the two must be set. gpu_labels and gpu_metrics matter only to GPU power telemetry (telemetry.dcgm_exporter and the cluster default_gpu_exporter it inherits from) and default to DCGM; see GPU power telemetry.

make setup ARCH=<compute_arch> downloads and checksum-verifies the matching Tachometer binary from the latest srt-slurm release and installs the process-exporter binary for the same arch. The scraper and the process exporter run as native srun processes; the DCGM and node exporters remain containerized on worker nodes. Run make tachometer-scraper to build the scraper from source instead. The process-exporter passthrough filter and node process-state/NUMA-label preservation require a scraper built from this revision or a release containing it; rebuild the scraper when using an older downloaded binary.

The pressure collector reports PSI only when the host exposes the corresponding /proc/pressure files; missing metrics indicate unavailable data. NUMA memory and allocation metrics retain the exported node label as numa_node in raw metric names, separately from host metadata. With observability.enabled: true, the existing local host sampler also records cumulative PSI stall totals in microseconds in its psi JSONL field. That optional sampler covers the sweep/orchestrator host only; it does not extend exporter placement to dedicated frontend or client nodes. Collector overhead has not been measured for this change.

Tachometer writes its Parquet stream under <log_dir>/<storage_subdir>/raw/scrape/ (the leaf is created by the scraper itself; srtctl pre-creates only the parent, because the scraper refuses a pre-existing storage directory), compacting to final.parquet there on shutdown. Intermediate files remain in <log_dir>/<storage_subdir>/local until shutdown compaction completes. Rows carry an epoch timestamp_ns column, so they join directly with AIPerf records and Dynamo spans.

The scraper runs as a best-effort process: if it dies (or the binary is missing at runtime), the benchmark continues and the loss is visible in tachometer.out and the sweep log. srtctl validate-setup still fails fast at submit time when bin/tachometer-scraper is absent.

telemetry

telemetry is GPU power measurement (the dcgm-power artifact producer). It can run alongside observability.tachometer; it does not start Tachometer itself. The exporter that supplies the watts is not tied to DCGM: the exporter block's gpu_labels and gpu_metrics select the labels and metrics (see GPU power telemetry).

When both are enabled, telemetry.dcgm_exporter is shared with Tachometer. Do not also configure observability.tachometer.dcgm_exporter; Tachometer can still launch an optional node exporter from its own block.

telemetry:
  enabled: true
  collect_interval_ms: 1000
  storage_subdir: power
  required: true
  dcgm_exporter:
    container_image: /containers/dcgm-exporter.sqsh
    port: 9400
  cpu_power_exporter:
    port: 9405
    source: auto

Fields: TelemetryConfig. collect_interval_ms is shared by the DCGM and CPU legs and must be at most 3000.

telemetry requires a benchmark.type of sa-bench, custom, agentic, agentx, or manual (a manual job has no load window, so like serve-only it captures the whole serve session; use it when an external load generator drives the endpoint), and no dedicated node for the discovery plane (an etcd/nats service with placement.node: dedicated moves the head off the batch host the collector runs on). The benchmark client may run on any node: collector sample timestamps and benchmark window boundaries are both Unix wall-clock readings and are assumed NTP-synchronised across the allocation, which clock_sync_check verifies before servers start (see power-telemetry.md).

CPU power

telemetry.cpu_power_exporter is an independent, best-effort leg: its presence (not a separate enabled flag) turns CPU power collection on, and it can run with or without dcgm_exporter alongside it. On each worker node, srtctl launches a cpu-power-exporter process directly on the bare host (outside the model container, so it can read host power interfaces) and exposes it on cpu_power_exporter.port. It resolves the bundled Rust binary installed by make setup first, falling back to the ACPI-only Python stdlib exporter (srtctl.core.cpu_power_exporter) when that binary is absent. A head-node collector scrapes every worker's exporter on the shared collect_interval_ms/request_timeout_seconds cadence and writes per-sample rows plus a manifest under <log_dir>/<storage_subdir>/cpu/ (samples.csv, cpu_manifest.json).

Fields: CpuPowerExporterConfig. source: auto uses ACPI when at least one discovered socket-total sensor reads a positive value (the envelope that becomes power_w; a 0 W reading from any channel is filed as missing, never as a sample), else DCGM fields 1130+1132 (CPU rail + SysIO), else 1130 alone if this libdcgm refuses 1132 — each step down logged at WARN. DCGM has no envelope field, so its power_w is ~half of ACPI's. The setting has no effect on the Python fallback exporter, which is ACPI-only. See CPU power telemetry for the full ladder.

samples.csv carries one row per socket per scrape (schema v3), with columns schema_version, timestamp_unix, hostname, source, sensor, socket_id, power_w, cpu_rail_w, soc_w, dram_w, module_w, total_power_w. ACPI power_w is the socket envelope; DCGM power_w is field 1130, the CPU rail. ACPI fills available component rails; DCGM fills cpu_rail_w (1130) and soc_w (1132). NVML supplies module_w, a superchip reference rail that includes the GPUs. total_power_w sums only power_w over sockets. See CPU power telemetry for details.

Because collection is always best-effort, there is no required knob for the CPU leg: a node that fails to expose its exporter (or fails to publish readings) simply produces gaps in samples.csv, and the run continues.