Standalone OpenTelemetry request tracing#

This experimental, opt-in implementation propagates W3C trace context from the standalone HTTP server into the engine scheduler and exports request phases over OTLP/HTTP. It is disabled by default. Validation results, performance measurements and trace examples are retained in PR #2644.

Enable in an isolated service#

Install the optional dependencies in the same Python environment as ATOM:

python -m pip install -e '.[tracing]'
export ATOM_ENABLE_OTEL_TRACE=1
export OTEL_SERVICE_NAME=atom
export ATOM_TRACE_VERSION='<exact-ATOM-build-version>'
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT='http://127.0.0.1:43180/v1/traces'

Start the standalone server with its normal model arguments. The PoC uses an environment switch; it does not add a --enable-trace command-line option. The explicit endpoint must accept OTLP/HTTP protobuf traces, including the /v1/traces path. Container deployments must pass these variables into the container. Changing them requires restarting that isolated process.

For a local receiver and a request with known upstream context:

python scripts/observability/otlp_sink.py --port 43180 --output /tmp/atom-spans.jsonl
# In another terminal, with an isolated server already listening on port 30380:
curl http://127.0.0.1:30380/v1/completions \
  -H 'Content-Type: application/json' \
  -H 'traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01' \
  -H 'tracestate: nsdye=1' \
  -d '{"model":"atom-otel-poc","prompt":[100,200,300],"max_tokens":8,"temperature":0,"stream":true}'

Use a fresh receiver output path on each run. The receiver binds only to loopback and is a test utility, not a production collector. The benchmark client also accepts only loopback endpoints.

Trace contract#

upstream span
└── atom.request                    HTTP lifetime, including SSE teardown
    └── engine.request              one per engine sequence
        ├── prefill_waiting         engine receipt → first actual forward
        ├── prefill_forward         first forward → final prompt-chunk postprocess
        └── decode_forward          prefill completion → engine termination

Only primitive carrier, model and request-ID values cross the engine IPC boundary; SDK spans and contexts remain process-local. The API’s public response ID is gen_ai.request.id; atom.sequence.id distinguishes sibling sequences. The HTTP span is a server span. The scheduler emits completed spans using the recorded timestamps, once per sequence, rather than keeping SDK spans in the scheduler loop. Stages are visible after completion, not as live progress.

These phases measure wall time and include scheduling, chunk interleaving, preemption and applicable KV waits. They are not device-event measurements or pure GPU execution time. Engine first retained token time is recorded separately from final prompt-chunk completion. Single-output-token requests omit decode; requests cancelled while waiting omit phases that never executed.

The engine.request span holds the response model, normalized finish reason, prompt/output/cached tokens, queue/first-token/prefill/decode/end-to-end latency and errors. Prompt text, generated text and raw exception messages are not exported. Phase spans carry only request/sequence/model correlation and, on the phase active at an abnormal termination, error.type and ERROR status. Completed phases keep their normal status; usage and aggregate latency attributes are not copied to children. An omitted phase also has no corresponding root phase latency attribute. Normal SSE final-body completion is distinguished from early disconnect. On atom.request, 5xx responses, exceptions and early disconnects set ERROR; 4xx responses keep the status unset, as the OpenTelemetry HTTP server conventions require, and are still visible through http.response.status_code. Repeated tracestate request headers are combined into one list before extraction.

Export and sampling behavior#

With the flag off, the module does not import the OpenTelemetry SDK. With it on, the API lifespan initializes its provider on a startup worker thread, and each engine initializes its provider before READY. Request and scheduler paths only read a cached process-local tracer. Each emitting process owns a provider with ALWAYS_ON sampling, including when the incoming parent is sampled=0. The original trace ID and tracestate are preserved; newly recorded children may have the sampled bit set. This deliberate policy allows a downstream collector to calculate full phase spanmetrics before applying its own retention sampling.

Export uses BatchSpanProcessor with an 8192-span queue, up to 512 spans per batch, a 1-second schedule delay and a 1-second HTTP exporter timeout. There is no per-token export or new GPU synchronization. Telemetry initialization failures warn and allow inference to continue; the SDK’s bounded queue can drop spans under pressure. This does not promise lossless tracing on collector failure. API and engine shutdown hooks make a bounded best-effort flush.

A deployment that requires tracing must separately verify receiver-observed spans. The environment flag alone is not an acceptance check. Collector endpoint, network reachability, header forwarding through proxies, service/version recognition, spanmetrics and retention rules need integration validation.

Tests and benchmark tools#

The three tests/test_otel_*.py modules cover context propagation, real scheduler hooks, SSE lifecycle, errors/cancellation, sibling isolation and a spawned process sending real OTLP to a loopback receiver. Install .[tracing] before running them; otherwise tracing tests may skip for missing SDK dependencies. The CPU CI job installs the tracing extra so these tests run there.

bench_trace_overhead.py generates immutable, independently namespaced prompts per input-length/concurrency point and reuses the same manifest across arms. It checks streaming termination, response ID consistency, token counts and usage, records output hashes without text, and measures the complete closed-loop drain. check_traces.py validates the successful cold-input benchmark’s five-span trees, public IDs, usage, zero cached tokens, phase continuity and dye/parent propagation. Do not apply that cold-input check to a cache-hit experiment.

Both connector-based and same-node P/D are explicitly marked atom.trace.unsupported=pd on the HTTP span and omit engine phase spans. Multi-GPU TP/DP, PP, MTP, other models and additional HTTP APIs are not GPU-qualified by the MI325 validation recorded in the PR.

The review fixes after 072ef555 have CPU regression coverage, including PP phase accounting, but have not been rerun on a GPU. The MI325 performance measurements linked above apply to the earlier measured implementation; they are not a performance result for this revision. PP remains GPU-unqualified.