Repository navigation
serve: self-describing request traces for SFT export (prompt token sha, replay delta, build id, default cap, parser fidelity) - #2
Conversation
JunyiXu-nv
left a comment
There was a problem hiding this comment.
Requesting changes for two issues: _tail_frames now JSON-parses the whole stream (three times per response, on the event loop), and with capture on the raw records leave the directory analysis/raw_vs_parsed.py reads. Two questions and two nits inline. The commit also has no DCO Signed-off-by.
Record at capture time what the GLM-5.3 kernel-trace export (Zhaoran's
audit, 2026-10-06) had to reconstruct by forensic replay. All additive;
unset env vars keep previous behaviour byte-for-byte.
Request line: thread (client_metadata.{session,thread,parent_thread,turn,
root_turn}_id + agent/request kind), kf (x-kf-* campaign headers), gateway
(X-Gateway-Request-Id/Attempt), sampling as sent, body_sha256, build_id.
Group conversations on thread.thread_id -- `session` is the router key and
a Codex sub-agent shares it with its parent (F01).
Response line: prompt_tokens {count, sha256(int32le ids), source[, ids]}
from the engine promise (aggregated) or the context worker's response
(disaggregated, on_ctx_resp); input_replay -- what rendering changed vs the
body (ReplayLedger: namespaces restored, bare names replayed, reasoning
decoded, orphan tool results, unknown items, tool_calls_folded); response_id,
usage, effective max_output_tokens, output_call_ids, output_item_types,
finish_reasons (chat), gateway_injected_terminal; build_id. Error exits are
traced as well (_traced_error_response).
Raw output (now beside the traces when capture is on): generated token_ids,
num_output_tokens, the engine's finish_reason, build_id, and cheap text
flags (marker literals, repetition measure, dots-only reasoning).
Fix at source: TLLM_RESPONSES_DEFAULT_MAX_OUTPUT_TOKENS caps Responses
requests that set none (Codex never does; F04 loops ran uncapped) and the
response echoes the effective cap; parser fidelity events name_unresolved
and GLM argument_coerced/unparsed/repeated (F02); the gateway forwards a
request id and attempt counter and signs its rotation terminal.
serve.sh exports TRTLLM_BUILD_ID (= provenance py_manifest_sha256[:12]) and
TRTLLM_PROVENANCE_FILE to every worker, copies provenance.txt beside the
traces, and points TRTLLM_RAW_OUTPUT_DIR at the request-trace directory.
Reviewed twice adversarially before landing (context-only path now strips
the prompt ids with the handoff; unparsed vs coerced argument notes;
server faults traced as errors; gateway identity headers stripped from
clients; linear reasoning scan; ctx retry replaces the digest).
Found by the 1P1D smoke and fixed here: a context-only Responses reply
carried no prompt ids when post-processing workers are on (the reply
is built where the result has none), so the proxy relayed none, the
generation worker rendered the prompt a second time and the trace had
no digest; the handler now attaches them from the promise, as chat and
completions do.
Unit suites touched: 1232 passed in the serving image.
Verified on a real rollout: GLM-5.2-NVFP4 served to Codex CLI sessions;
every response line carries a prompt token sha that an offline re-render
reproduces, and every raw record carries the generated ids.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Lin Jia <linj@nvidia.com>
- _tail_frames: rsplit returns count+1 pieces and the first is the whole stream; it is no longer parsed (240 ms -> ~0 per response line). The tail is split once per record and shared by responses_outcome/responses_summary. - Drop gateway_injected_terminal: the gateway writes SSE_ROTATED after the backend is gone, so the frame never passes through this server. The way to find those requests is the join gateway.request_id -> gateway access log (status ends in "!"); documented where the constant was. - raw_vs_parsed.py reads raw dumps from either layout (raw_output/ without capture, request_trace/ beside the trace with capture on). - GLM-4/4.7 streaming path collects argument notes and attaches them to the streamed call, so a Codex (always streaming) call carries the same notes as the whole-text parse; normalization records dedupe per (call_id, event set) instead of per call_id so a later delivery that learns more is not lost. - body_sha256 hashes the memoized request bytes (no second serialization on the event loop); canonical JSON only as fallback. - SPDX header on test_gateway_request_identity.py. Tests: tail never parses the body (json.loads call count), digest from bytes, streamed-call notes for both parsers, raw_vs_parsed in both layouts. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Lin Jia <linj@nvidia.com>
dd28b03 to
622a850
Compare
|
Addressed the review in |
Summary
Make the request traces the fleets write self-describing for SFT export, so what Zhaoran's GLM-5.3 kernel-trace audit (2026-10-06) had to reconstruct by forensic replay is recorded at capture time. Everything is additive; with the new env vars unset, serving behaves byte-for-byte as before (one exception called out under "Behaviour changes").
Branch:
linj/trace-fidelityoffdev-junyix-feat-kimi-k3-serving@ e061773; merges cleanly onto the current tip (the three Helix/tripwire commits touch disjoint files).What a reader gets
Request line (
requests-<pid>.jsonl)thread—client_metadata.{session_id, thread_id, parent_thread_id, turn_id, root_turn_id}plus agent name / request kind fromx-codex-turn-metadata. Group conversations onthread.thread_id:sessionis the router key, and a Codex sub-agent shares its parent'sprompt_cache_key(the audit's F01).conversation_id.py's comment now says so.kf— thex-kf-*campaign headers without the prefix;gateway—X-Gateway-Request-Id/X-Gateway-Attempt(new, see gateway.py);samplingas sent (an absentmax_output_tokensstays absent);body_sha256;build_id.Response line (
responses-<pid>.jsonl)prompt_tokens={count, sha256, encoding: "int32le", source}of the token ids the engine was given —sha256(np.asarray(ids, "<i4").tobytes()). Aggregated: from the engine promise. Disaggregated: from the context worker's response inon_ctx_resp.TRTLLM_REQUEST_TRACE_PROMPT_TOKEN_IDS=1stores the ids too. An export re-renders the body, tokenizes, and must reproduce the sha; that is the oracle the smoke ran (below).input_replay— what rendering changed vs the body (ReplayLedgerin responses_utils: namespaces restored, bare names replayed, reasoning decoded from encrypted, orphan tool results, unknown items,tool_calls_folded, kimi_k3 flags). RidesResponsesRequest._input_replay(PrivateAttr) and, across the ctx→proxy hop,ResponsesResponse.input_replay, stripped with the handoff.response_id,usage(five counts),max_output_tokensas submitted (request's or server default),output_call_ids,output_item_types;finish_reasonson chat replies;build_id. Handler-rejected requests now leave a response line too (_traced_error_response, same vocabulary as the proxy's_trace_failure; server faults answered with 400 are traced aserror, notrejected_400).Raw output (
raw-<pid>.jsonl, now beside the traces whenserver.captureis on)token_ids,num_output_tokens, the engine's ownfinish_reason(threaded throughPostProcessorHookChunk.finish_reason),build_id, and cheapflags:marker_literals(<|...|>spellings with counts),repetition(suffix repeats + identical-line runs, texts ≥ 2000 chars),reasoning_dots_only. Signals for an export, not verdicts. Join key:disagg_request_id(disaggregated), else engine request id == traceclient_id.Fix at source (per audit F02/F04/F06, appendix)
TLLM_RESPONSES_DEFAULT_MAX_OUTPUT_TOKENS=<n>: default output cap for Responses requests that set none (Codex never does; the F04 loops ran uncapped). Applied where the request is rendered (workers;server.envreaches every process), echoed as the response'smax_output_tokens. Off unless set.call_id, inToolCallItem.normalizations/normalize-<pid>.jsonl:name_unresolved(call to an undeclared tool); GLMargument_coerced(literal changed:1e3→1000.0,True→true,'a'→"a"; whitespace outside strings ignored),argument_unparsed(text delivered JSON-quoted under a non-string schema),argument_repeated. Delivered arguments are byte-identical to before.X-Gateway-Request-Id/X-Gateway-Attemptto the backend (a retry's records join to one gateway line), strips client copies of the threeX-Gateway-*headers, andSSE_ROTATEDcarries"source": "gateway", "code": "backend_rotated"so a reader can tell a gateway-injected terminal from the model's.build_id = py_manifest_sha256[:12](only when 12 hex digits) andTRTLLM_PROVENANCE_FILEexported to every worker;provenance.txtcopied beside the traces;TRTLLM_RAW_OUTPUT_DIR= the request-trace directory when capture is on. FLEET.md gains a section on what an attempt leaves undertrace.request_root.Behaviour changes to know about
num_postprocess_workers > 0the context-only Responses reply setprompt_token_idsinside the postprocessor, where the result object has none, so the proxy relayed nothing — the generation worker rendered the prompt a second time and no digest could be recorded.OpenAIServer._attach_ctx_prompt_idsnow copies them from the promise, exactly as_create_chat_responseand the completions path already did. Net effect on the fleet: the generation worker consumes the relayedprompt_token_ids_b64for Responses requests (the path that already existed for it) instead of re-rendering. Exercised end to end on the smoke (two Codex sessions, 20 calls, all completed, 22/22 digests, re-render oracle 20/20)._drop_ctx_handoffnow also nullsprompt_token_ids/prompt_token_ids_b64on a context-only reply that is not handed off (EOS as first token), which used to reach the client (and the trace body) with the full id list.ResponsesResponse.input_replayis dumped asnullon every client-facing response and SSE snapshot, like the existingprompt_token_idsfields (~20 bytes; SDKs ignore unknown keys)./v1/completionsis still untraced in the aggregated server; the docstring now says so.Env knobs (all optional; unset = previous behaviour)
TRTLLM_BUILD_IDTRTLLM_PROVENANCE_FILETRTLLM_REQUEST_TRACE_PROMPT_TOKEN_IDS=1TLLM_RESPONSES_DEFAULT_MAX_OUTPUT_TOKENS=<n>For the data-generation fleets: leave the cap unset. Capture is additive on purpose -- a loop is marked at export time (raw
finish_reason, token counts, the repetition flag) rather than cut at serving time, because a cap set by us truncates real outputs (the A/B run saw GLM-5.2 complete 40-70k-token calls). The knob exists for deployments that want a ceiling; it is not part of the trace-quality fix. Leave the ids off unless the export wants them on disk (the sha verifies a re-render).Verification
test_request_trace_fidelity.py,test_raw_output_hook_fidelity.py,test_responses_input_replay_ledger.py,test_glm_argument_notes.py,large_scale_serving/tests/test_gateway_request_identity.py): 1232 passed, 61 skipped, 0 failed in the 26.08 image.execsessions + three probes (cap 16, no cap, chatmax_tokens: 1). 31/31 checks: digests self-consistent 22/22 andcount == usage.input_tokens20/20; rawtoken_ids+finish_reasonon 23/23 andlen == usage.output_tokens; build_id on all 69 lines = provenance; caps echoed (16, default 4096),incompleteiff raw sayslength; thread prefix chaining 18/18, FIFO 18/18. Offline re-render (_create_input_tokenson the recorded bodies) reproduces the recorded prompt ids exactly: 20/20.glm5.2_ctx_tep4+glm5.2_gen_tep4, two nodes, the K3 fleet's image and TCP-only UCX list): attempt-1 exposed change (1) above (0/25 digests); attempt-2 on the fix: 31/31 checks, digests 22/22 fromctx_response, raw joined ondisagg_request_id23/23, re-render oracle 20/20.Verifier and smoke configs:
/scratch/fsw/portfolios/coreai/users/linj/kernel_writer/tools/smoke/(verify_traces.pyis stdlib-only except the optional tokenizer/re-render part).Not in this MR
result.py: the in-proxy post-processing path (num_postprocess_workers: 0) never ends the raw hook on an error response, so a failed request's pending record lives until process exit (pre-existing; fleet configs run workers).linj, so the disaggregated smoke ran with the stock plugin.🤖 Generated with Claude Code