Repository navigation
Conversation
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
TokenCountSource (client | server) says where a token count comes from. basis_key(base, basis, *, primary) is the single naming rule: the primary basis keeps the base name and the other basis gets a _<basis> suffix (osl_server beside a client primary, osl_client after a flip). Server counts ride QueryResult.metadata under PROMPT_TOKENS_KEY and COMPLETION_TOKENS_KEY. well_formed_count and server_usage_count validate a raw JSON value and warn on a malformed one; put_server_usage writes only well-formed counts, so a bad server value never fails a frame. EventRecord gains prompt_tokens and completion_tokens (UNSET when absent, as in archived logs) for the COMPLETE event. core/record.py adds is_complete_line, the byte-level filter that post-run readers use to find COMPLETE lines in events.jsonl, and EventLogFormatError for a log that is not in the compact form. Part of #540. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
settings.token_count_source (--token-count-source) picks the primary basis for ISL/OSL/TPS/TPOT. It defaults to client, so every unsuffixed number matches main; flipping the default to server is a one-line change here plus the regenerated templates. settings.client.stream_include_usage (--no-stream-include-usage to opt out) asks streaming OpenAI-compatible endpoints for a usage frame. It is on by default so the server basis is always reported beside the client one; turning it off restores main's request bytes. Part of #540. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Request side: every adapter takes encode_query(query, *, include_usage).
The worker passes the run's stream_include_usage. The OpenAI chat and
/v1/completions adapters send stream_options: {"include_usage": true} only
on streaming requests with at most one choice; SGLang (meta_info) and
videogen ignore the flag. Query.data keeps main's shape.
Response side: usage is read from any SSE frame (the last well-formed value
per field wins) and from non-streaming bodies. Count fields decode broadly
and are validated at capture, so a malformed count warns and never drops
the frame's content or finish_reason. Each SSE frame decodes in its own try,
so one bad frame no longer drops the frames after it in the same read.
SGLang accepts the current meta_info shape (null finish_reason,
num_retractions, optional e2e_latency/output_ids) and validates
prompt_tokens once per changed value rather than once per frame.
session.py lifts the counts from QueryResult.metadata onto the COMPLETE
EventRecord. It trusts put_server_usage, the single writer, and adds no
validation to the per-request path.
Hot path: the OpenAI SSE fast path keeps main's cost plus one usage test per
frame (decode micro-benchmark within +0.7..+2.4% of main).
Part of #540.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The echo, max-throughput and variable-throughput servers follow vLLM: - a streaming response carries a usage frame only when the request sets stream_options.include_usage, after the finish frame and before [DONE]; - a non-streaming body always carries usage. Roofline numbers are compared across versions, so the stream layout is pinned byte for byte by tests. The echo server also serves /v1/completions and rejects stream_options without stream. Fixtures: mock_http_echo_server_factory(report_usage=False) models a server that sends no usage, and usage_warning_unlatched resets the once-per-process malformed-usage warning. tests/test_helpers.py adds parse_sse_frames, make_report and DEFAULT_SOURCE, which the later layers' tests use. Part of #540. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
metrics/token_count_basis.py holds the one usage rule. classify_server_usage decides, once per sample, whether the server's counts are usable: usage is missing when there is none, or when completion_tokens is 0 beside non-empty output. read_server_usage returns the usable counts. The live aggregator and every post-run tool use these two functions. The module also reads a finished run's basis back (token_count_source_for_run, replay_basis). Aggregator: - Token series are registered from one table (TOKEN_SERIES) and named per basis with basis_key. Primary series keep main's names and registration order, so a default snapshot is a strict superset of main's. - ServerUsageTrigger classifies each COMPLETE once and records server ISL/OSL/TPOT synchronously, with no tokenizer and nothing added to n_pending_tasks. Server TPOT uses LoadGen's rule, (complete - first chunk) / (completion_tokens - 1), and skips completion_tokens <= 1 and non-positive windows. - Missing usage is counted on either primary (tracked_samples_missing_usage[_empty_output]). osl_paired_* counters total both OSLs over the samples that were counted on both bases. - Early stopping and steady state attach to the primary basis only. --no-metrics-isl drops ISL on both bases. - __main__ takes --token-count-source. Part of #540. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he primary Report.from_snapshot(snapshot, token_count_source=) takes the basis as a required argument; the snapshot wire format is unchanged. Unsuffixed fields come from the primary basis, so under the client default result_summary.json and report.txt keep every main key, value and line. The other basis is reported beside it: - result_summary.json: _<basis> blocks (input/output_sequence_lengths, tpot, tps), with "partial": true when that basis covered fewer samples, plus osl_paired and the missing-usage counts; - report.txt: main's rows unchanged, then an "Other token basis (<basis>):" block, with a " (partial)" suffix on short sections. Completeness follows the primary. A client primary keeps main's rule (n_pending_tasks == 0) and only marks server blocks partial. A server primary requires every tracked successful sample to carry usable usage; otherwise run_benchmark exits 4 with the cause-aware message from Report.incomplete_reason(). pipeline.resolve_token_count_source is the only reader of the setting; videogen resolves to client. Under a server primary, setup fails before any load on a streaming OpenAI run that opts out of usage, and execute.py fails on rows with n > 1 (usage sums all choices). Under a client primary, n > 1 rows only warn. Part of #540. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Full-run OSL (the agentic accuracy-gate input), the accuracy OSL rollup, the scorer, the steady-state replay CLI and scripts/early_stopping_estimate_from_events.py now share the live usage rule (read_server_usage). Each writes the primary basis unsuffixed with the other basis's _<basis> keys beside it. A block that covers fewer turns than were completed carries "partial": true. Under a server primary, report.txt also says ", N missing usage"; under the client default, the full-run line keeps main's wording. A replay takes its basis from replay_basis(run_dir, *, override): the summary's top-level token_count_source, else config.yaml, else client for archived runs. --token-count-source overrides it, and the early-stopping script's --summary names the summary to read. Readers of events.jsonl stop silently dropping data: a non-compact log raises EventLogFormatError, and the replays count and warn on undecodable COMPLETE lines. The archived pre-usage run test moves to the new call signatures; every pinned number is unchanged. Part of #540. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Design docs (core, endpoint client, OpenAI, metrics aggregator, metrics, report, steady state, early stopping, testing), the CLI quick reference, AGENTS.md (architecture notes and module map) and the agentic examples now describe: - the primary/other basis; - the _<basis> naming rule; - when usage is requested; - the missing-usage rule; - how to flip the default. Part of #540. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
--roofline-no-stream-include-usage passes --no-stream-include-usage to the roofline benchmark client, so the stub servers send no usage frame and the streaming rows can be compared with and without the usage request. The low-QPS correctness test is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅ |
| ] | ||
| ) | ||
| assert snapshot_series_count(registry, MetricSeriesKey.ISL.value) == 0 | ||
| for basis in TokenCountSource: |
| ] | ||
| ) | ||
| assert not registry.has_series(MetricSeriesKey.ISL.value) | ||
| for basis in TokenCountSource: |
Comment on lines
+1727
to
+1733
| totals = { | ||
| (key, basis): snapshot_series_total( | ||
| reg, _name(key, basis, primary=primary) | ||
| ) | ||
| for key in _TOKEN_KEYS | ||
| for basis in TokenCountSource | ||
| } |
Comment on lines
+1552
to
+1553
| " OSL per-turn mean (accuracy, all turns): 6.0 tokens over 2 turns " | ||
| "(1 empty, 0 errored, 0 undecodable, 0 missing)", |
| class _WordTokenizer: | ||
| """BatchTokenizer stand-in that counts whitespace-delimited words.""" | ||
|
|
||
| def __init__(self, *a: object, **k: object) -> None: ... |
| def __enter__(self) -> _WordTokenizer: | ||
| return self | ||
|
|
||
| def __exit__(self, *exc: object) -> None: ... |
| global _malformed_usage_warned | ||
| if _malformed_usage_warned: | ||
| return | ||
| _malformed_usage_warned = True |
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| import inference_endpoint.commands.benchmark.full_run_osl as full_run_osl_mod |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Implements #540. Records the endpoint's reported token usage
(
usage.prompt_tokens/usage.completion_tokens, or SGLangmeta_info)beside the client tokenizer's counts on every run, and lets one setting choose
which basis is the headline. The default stays on the client tokenizer, so
every existing number is unchanged; the server counts and a paired per-sample
comparison (
osl_paired) make the #540 gap (inline<think>…</think>vsstructured
reasoning_content) visible on every run.settings.token_count_source: client(--token-count-source client). Every unsuffixed number, pass/fail decision and exit code matchesmainfor a well-formed endpoint._serversuffix (isl_server,osl_server,tpot_ns_server;input_sequence_lengths_server,output_sequence_lengths_server,tpot_server,tps_server). They never decide pass/fail under the default.--token-count-source servermakes server the primary (unsuffixed, gates the run) and names the other basis*_client. The flip is one schema default (config/schema.py); no key name or code path changes./v1/completionsrequests sendstream_options: {"include_usage": true}by default.--no-stream-include-usage(YAMLsettings.client.stream_include_usage: false) opts out; request bytes then equalmain's.--token-count-source servertogether with the opt-out on a streaming OpenAI run raisesSetupError(every streamed sample would miss usage and the run would exit 4 after doing all the work).meta_infoand non-streamingusagealready carry counts.api_type: videogenalways resolves toclient(INFO log).n > 1rows are rejected only under a server primary.Changes from the earlier draft (server default)
server; client counts as*_clientclient(identical tomain); server counts as*_serverTokenCountSourceeverywhere, keyword-only; names derived by onebasis_keyruleclassify_server_usage/read_server_usage) shared by the aggregator, post-run OSL, scorer and replaysrun_config.token_count_sourcetoken_count_sourceinresult_summary.json, read by one reader (token_count_source_for_run)Out of scope: a live (TUI) display of the other basis, early stopping and
steady-state detection on the other basis, and the RECV_FIRST vs COMPLETE clock
mismatch.
How to review
Size
src/,scripts/)docs/,*.md, examples)About 2.5 lines of test code per line of implementation. The stub servers
(
src/inference_endpoint/testing/, about 320 added lines) count asimplementation here but are test infrastructure.
¹
openai/,sglang/,load_generator/, plusworker.py,adapter_protocol.pyandmetrics_table.py.Commits, in reading order
The history is ten commits, one per layer, oldest first (the order of the
Commits tab). Each commit holds its layer's final code and tests, so no
intermediate rework appears.
040d48c8maingit diff 040d48c8 e9f2db57 -- tests/unit/metrics/test_archived_run_compat.py.fdb91a44basis_keyis the only naming rule. A server count is kept only if it is a non-bool int in[0, TOKEN_HDR_HIGH].EventRecordcounts default toUNSET, so a record without them encodes byte-identically to an archived line.df28e3ebclient; flipping it is this one line plus the regenerated templates.28cf4286usage is Nonetest.stream_optionsgoes only on streaming requests withn ≤ 1. A malformed count never drops a frame. SGLang validatesprompt_tokensonly when it changes.session.pycopies counts without re-validating.87e7dfd50bbccc22metrics_table.py)ServerUsageTriggerruns once per COMPLETE and does no tokenizer work.classify_server_usageis the only definition of missing usage. Primary series keepmain's names and registration order. Server TPOT rule and its skips.dff68380main's andreport.txtkeepsmain's rows, with the other basis after them. Completeness follows the primary basis. Setup checks:resolve_token_count_source,_check_multi_choice_rows.e9f2db57read_server_usageandreplay_basis. Basis resolution order: summary, thenconfig.yaml, then client. A malformed log fails loudly.36265fa7f07b0d2bEvery commit imports cleanly, but only the head is tested as a whole:
tests/test_helpers.py(commit 5) imports names that commits 6 and 7 add.If time is short
metrics_table.pyin commit 6: the hot path.Skim or skip: the JSON fixtures (commits 1 and 6), the generated templates
(commit 3), the stub servers (commit 5) and the docs (commit 9).
Checking it locally
For the full suite run
uv run pytest.Naming rule
The primary basis keeps the base name; the other basis appends
_<basis.value>(pure append, so seriestpot_nsbecomestpot_ns_serverandreport key
tpotbecomestpot_server).Never suffixed (basis-independent):
osl_paired, theosl_paired_*counters,n_samples_missing_usage,n_samples_missing_usage_empty_output,n_missing_usage, the full-runn_undecodable/n_missing,client_token_tasks_pending,token_count_source.n_errorscounts tokenizer failures, so it exists only on the client basis(
n_errors_clientunder a server primary).What a default run writes, versus
mainAdditions only. Every key
mainwrites is still there, and under a clientprimary each unsuffixed value equals
main's on the same inputs (pinned bytests that rebuild reports and replays from identical snapshots and event
logs).
performance/result_summary.jsontoken_count_source,n_samples_missing_usage,n_samples_missing_usage_empty_output,client_token_tasks_pending,osl_paired, and the other basis asinput_sequence_lengths_server,output_sequence_lengths_server,tpot_server,tps_server(a block isnullwhen no usable sample was recorded;"partial": trueinside it when some samples lacked usage;tps_serverisnullwhile partial)final_snapshot.jsonisl_server,osl_server,tpot_ns_serverseries,osl_paired_*and missing-usage counters, registered aftermain's series so a default snapshot is a strict superset; wire schema unchangedevents.jsonlprompt_tokens/completion_tokensonsample.completerecords (omitted when the endpoint sent none)report.txtOther token basis (server):block aftermain's summary rows (token count source,TPS (server),OSL paired, a partial-reason sentence), and(server)sections aftermain's latency-breakdown sections, titled(server) (partial)when some samples lacked usage.main's rows keep their order and wording under a client primaryoutput_sequence_lengths_full_run)output_sequence_lengths_server,n_turns_counted_server,n_empty_server; its top-levelpartialstill reflects only the primary basisaccuracy/accuracy_results.jsontoken_count_source; per datasetoutput_sequence_lengths_server,n_osl_samples_server,n_missing_usageServer TPOT uses LoadGen's rule,
(complete - first chunk) / (completion_tokens - 1).No server series records a value below 1.
Deliberate divergences from
mainunder the defaultstream_optionssent by default on streaming OpenAI requests_servermetrics.--no-stream-include-usagerestoresmain's request bytes.StreamChunk, so TTFT andchunk_deltasample counts are unchanged. A live A/B cannot match bit for bit; parity is tested on identical inputs.usageobject no longer fails a non-streaming sample[0, TOKEN_HDR_HIGH]; one warning per worker process names the first bad type./v1/completionsbody with nousagekey no longer fails the sample (TextCompletionResponse.usagegains a default)parse_sse_chunkdrops only the malformed frame, not the rest of the readMetaInfoaccepts the currentmeta_infoshape (nullfinish_reason, etc.)Report.incomplete_reason()(the auditExecutionError, the pipeline "Report is incomplete: ..." log) instead ofmain's fixed textconfig.yaml, and on a non-compact event log; they report undecodablesample.completelinesclientwith a stderr note, and--token-count-sourceoverrides.COMPLETElines by their leading bytes instead of decoding them, and fails loudly (EventLogFormatError) on an event log that is not compactmsgspecJSONJSONLWriteralways writes the compact form the filter needs; a log in another encoding (for example spacedjson.dumps) would otherwise score nothing silently.*_serverblocks andreport.txtlines listed aboven > 1under a client primary: the adapter omitsstream_optionsandnon-streaming decode copies
usageonly for a single choice, so_serverispartial for those rows. A server that forces usage on every stream (for example
vLLM
--enable-force-include-usage) still returns summed counts.Measured
macOS (arm64, 10 cores, Python 3.12). These numbers are indicative only; a
Linux idle-host run is the authoritative check and is pending.
OpenAIMsgspecAdapter.decode_sse_message(test_decode_sse)Branch vs
main(4b385eb5), 5 interleaved runs, median of the per-runmedians. Budget: within +/-3%.
mainNote: macOS's timer ticks in 41.7 ns steps, so the empty case flips between 209
and 250 ns. Its minimum is 165.8 ns on both trees. A many-iteration
timeitofthe same input (3 interleaved runs, medians) gives empty 184.6 vs 188.7 ns
(+2.2%), 100 chars +1.8%, 1k +1.9%, 8k +0.9%, 32k +0.9%. The per-frame cost on the
default path is one
usage is Nonetest.CLI roofline (
test_e2e_perf.py, streaming rows, stub servers)Stub server 3 workers, client 4 workers, all on the same host;
mainruns itsown stub, so each side uses its own tree end to end. The client runs without
CPU pinning (
pin_loadgenis Linux-only). QPS, higher is better. "Opt-out" isthe branch with
--roofline-no-stream-include-usage, which makes the stub sendno usage frame; "default" is the branch with the usage request on.
mainmedianmain)main)concurrency c=1000, stream (n=6)concurrency c=4000, stream (n=6)max_throughput2M burst, stream (n=1)Turning the usage request off recovers
main's rate within noise on every row,and the default path shows no measurable cost on this host. The host is a shared
laptop:
mainalone varies 32.0k to 38.7k QPS across the sixc=1000runs, sodifferences under about 5% are not resolvable here, and the 0.99x opt-out bar
(opt-out QPS versus
main) is below this host's noise floor. The concurrency rows are six interleaved rounds, eacha separate pytest session, in rotating order (
main, opt-out, default). Theburst row ran once per mode (about three minutes each), in the same session as
a first pair of concurrency runs; those first-session concurrency runs were
noisier (
c=1000: opt-out 0.80x, default 1.02x;c=4000: 0.93x and 0.90x) andare not in the medians above. A Linux idle-host run is the authoritative check
and is pending.
Follow-ups (outside this change)
AgenticInferenceDataset.load: a JSONLstreamcolumn with missing valuesloads as float64, so requests carry
"stream": 1.0/0.0; coerce to bool.Type of change
Related issues
Implements #540.
🤖 Generated with Claude Code