Repository navigation
Consult ignore_eos when classifying a finish reason at the output limit - #102
Conversation
5dba4f1 to
ea7e971
Compare
|
@mark-yong GitHub reports that this PR has merge conflicts with Conflicting files:
Please update the branch against |
ea7e971 to
f1cccd6
Compare
|
The runtime fix is correct and still needed. The branch head sits on 1. The new termination also changes parser behavior, which the description does not state. The body says "only the reporting moves". The changed
All three follow from EOS being non-terminating, and they match the documented rule that truncated envelopes are omitted ( 2. Commit text.
3. The first commit fails its own test. 8d61d89 adds only the test, which fails on that tree, so a rebase-merge would put a failing commit on 4. The
Tests run, CPU only: the full lib suite gives 1281 passed, 4 failed and 25 ignored on the branch head, and 1280 passed and 5 failed on |
f1cccd6 to
8aaf734
Compare
8aaf734 to
01db7c7
Compare
A request with ignore_eos suppresses end-of-sequence termination, so its last sampled token can be an EOS id while it finishes on the output limit. The terminal event classified that as a stop with Eos termination, which tells clients a truncated generation completed normally. Use the request's ignore_eos setting in the finish-reason classification, the same way the emitting and logprob paths already do, and document the finish reason each protocol reports for each termination cause. The new termination also reaches the decoders before encoding: at the output limit a completed tool call now reports the limit reason instead of tool_calls, open tagged, DSML, and XTML envelopes are dropped instead of raising an incomplete-call error, and the recipient parser no longer closes a final complete tool frame that end-of-sequence termination would have closed, so with required or named tool choice and no earlier completed call the request now ends in an error. These follow from end-of-sequence being non-terminating and match the documented truncated-envelope rule. Validation: the regression phase test fails on c65024d with only the test applied, at the terminal-event assertion (reason "stop", termination Eos), and passes with the fix. Release Helios library suite on the build host (CPU): 1276 passed, 9 failed, 25 ignored with the fix; the same suite on c65024d with only the test added is 1275 passed and 10 failed, the new case being the only difference. The nine shared failures are environmental: four engram staging memory-limit cases, four missing FLARE_TEST_TEMPLATE_DIR fixtures, and one package-cache staging case. Separately, builds at 0ce6de8 served one prepared single-GPU package side by side with an explicit seed at the clamped prepared defaults: a request that reaches its output limit on an EOS token reported stop on the unmodified build and length on this one in all 30 requests, while requests that honour EOS, stop before it, or ignore it without ending on it reported the same reason on both builds. rustfmt drift on the touched file is unchanged. Material limits: the regression drives the simulated worker phases, the served check used /v1/chat/completions on one prepared package, and the sampled-EOS suppression itself is unchanged. Design-Signed-Off-By: Hermes Agent (GLM-5.3-Flash); design-review=a38f Signed-off-by: Mark Yong <mark-yong@users.noreply.github.com>
01db7c7 to
449b870
Compare
|
Thanks for the detailed review. All four points have been addressed on the branch, now a single commit (
The speculative-round case is also covered by the same classification in Final-tree validation: 1,276 passed, 9 failed, 25 ignored. The same nine environmental failures occur on The served A/B was not rerun after the rebase; the runtime fix is unchanged. The earlier comparison covered Chat Completions only. Messages and Responses have not been exercised over the wire. |
|
thanks for the updates. this looks good to me now. @lukealonso good to go. |
…op-16 (numerics change on re-export) Above eight rows the recipe left the route exchange entirely: the general path's latent all-reduce (rows x 3584 BF16), router all-gather, two copies and the native route top-k. MAX_ROWS goes 8 -> 20 so decode rounds of nine to twenty rows (two concurrent speculative streams verifying up to 2 x (depth + 1) rows, or four-stream rounds up to twenty) stay on the route exchange: the paired gather plus the standalone top-16 selects their experts; the fused single-exchange select stays at <= 8 rows. The decoder routes every 9-20-row round onto this path unconditionally; the recipe prepares 1-20 rows or raises at preparation, with no pin detection and no fallback to the general path. Transport and speed: the route path is not push-gated -- it runs on whatever paired-gather transport the b12x plan selects. The per-layer speed evidence (30.75 / 38.35 / 48.57 / 59.63 us saved at 9 / 12 / 16 / 20 rows; slowest-rank medians, >= 200 graph replays x 5; general path 47.05-80.52 us vs route path 16.30-20.89 us per MoE layer) was measured on a push-planned exchange. The pull transport at nine to twenty rows is UNMEASURED -- that is the path a default upstream deployment takes at these widths. The push path carries the evidence, no speed claim is made for the pull transport, and measuring it at 9/12/16/20 rows is a follow-up before this path is relied on by default. Numerics change on re-export at 9-20-row rounds (flagged in the subject): the latent arrives by gather instead of the BF16 all-reduce, so the hidden-shard sums regroup -- 2.63e-3 relative RMS against the general path at every width, one BF16 unit roundoff of the general path's partial sums. The route path is strictly closer to the FP32 product (1.66e-3 vs 2.36e-3 rel RMS). Selected ids are identical to the general path at 9/10/12/16 rows over 6 banks x 8 ranks (weights within 5 fp32 ULP), with one near-tie 16th-slot swap row in 960 at 20 rows (near-tie gate re-run: 3.2M rows at 9/12/16/20, every set difference a 1-for-1 swap within 2 fp32 ULP, set rate <= 5e-5, 0 violations). Existing captured packages are unaffected; every re-exported Kimi package changes arithmetic at those widths on two- and four-stream decode, and the maintainer must accept that trade. Pin dependency: rows 17-20 need the FlashInfer standalone top-16 that accepts the full range (the pin declares KIMI_TOPK16_MAX_ROWS = 20). Their route-gather pull request NVIDIA#101 covers 9-16 rows; this change extends the recipe to 20 rows and needs NVIDIA#102's standalone select. The FlashInfer pin bumps when NVIDIA#102 merges; until then, preparation for 9-20-row rounds requires it -- on an older pin the top-16's row check raises at preparation of those widths, loudly, by design. Tests: tests/recipes/test_kimi31.py 33 passed (1/2/8/9/12/16/20-row route-gather surface selections -- one row fuses the select, two to twenty rows pair the exchange with the standalone top-16 -- plus the 21-row envelope rejection and the 25 existing tests); with test_kimi3_preparation.py 57 passed. The Flare test mocks the b12x plan surfaces: it proves which surfaces are planned, not results. The GPU evidence is the flashinfer side's CUDA tests for the top-16 row-range change; no Flare-level GPU route test was run on this tree. Earlier end-to-end round numbers were measured with a full settings bundle and are dropped rather than misattributed. Validation: tests/recipes/test_kimi31.py + test_kimi3_preparation.py 57 passed on this branch against the release-lane base's 49 (the 8 new items; the remaining tests are identical and pass at base -- base-failure parity holds, no test that passes at base fails here). Material limitations: no Flare-level GPU route test and no pull-transport measurement (above); the route-vs-general arithmetic comparison is from the recorded measurements (6 banks x 8 ranks), not re-run here. Numerics class: NUMERICS-LIGHT, BF16-structural -- ids identical except the quantified near-tie swaps; the latent regrouping above is the documented one-BF16-roundoff difference, closer to FP32 than the general path. Design-Signed-Off-By: Hermes Agent (hermes-agent); design-review=a38f
Request
ignore_eosrequests finish on their output limit, but the terminal event classified the finish reason as if an end-of-sequence token had ended the generation.docs/TODO.txtcarries the gap as a roadmap item: "Respect ignore_eos when classifying EOS at the output limit; see helios/src/server/runtime.rs."ignore_eossuppresses end-of-sequence termination: the admission flag is packed into the sampling graph asEosCount = 0(helios/src/model/packing.rs), so the request keeps generating past EOS tokens and stops at its output limit. The terminal event still decided the reason from the last sampled token alone:The two neighbouring EOS checks in the same function already consult the setting (the terminating-EOS check and the text-stop finish), so this site was the inconsistent one. When an
ignore_eosrequest's last sampled token happens to be an EOS id, the client is told the generation ended normally although it was cut at its output limit. That reaches every encoder: OpenAIfinish_reason: "stop", Anthropicstop_reason: "end_turn", and Responsesincomplete: false, andTermination::Eoson the terminal event. A harness that trusts those fields treats truncated output as a complete answer.It fires when the sampling draw lands on an EOS token in the final position of a request that carried
ignore_eos: intermittent by sampling, deterministic once the draw occurs.Change
helios/src/server/runtime.rs: the finish-reason classification uses the request'signore_eossetting, the same way the emitting and logprob paths already do. An ignored EOS no longer counts as a stop, so the request reportslengthandTermination::OutputLimit. User stops are unchanged, and so is every non-ignore_eosrequest. The changedterminationalso reaches the decoders before encoding, which changes three behaviors for anignore_eosrequest that ends at its output limit on an end-of-sequence token: a completed tool call now reports the output-limit reason instead oftool_calls(Chat Completions; raw Completions rejects tool parameters) ortool_use(Messages), so a client that dispatches only on those reasons stops dispatching; an open tagged, DSML, or XTML tool envelope is now dropped silently where end-of-sequence termination raised an "incomplete tool call" error; and the recipient parser no longer closes a final complete tool frame that end-of-sequence termination would have closed, so with required or named tool choice and no earlier completed call the request now ends in an error. All three follow from end-of-sequence being non-terminating and match the documented rule that truncated envelopes are omitted (docs/api.md).docs/api.md: maps each termination cause to each protocol's finish reason. An output limit reportslength(Chat Completions and Completions),max_tokens(Messages), andincompletewithmax_output_tokens(Responses) even when parsed calls completed the answer; otherwise completed calls reporttool_callsandtool_use(raw Completions rejects tool parameters, sotool_callsis Chat Completions only); any other cause reportsstop(Chat Completions and Completions), with Messages returningstop_sequencefor a matched string andend_turnfor every other cause, including an honored end-of-sequence token and an explicit token stop. The cause is decided at the terminal token, so an honored end-of-sequence token or a matched string stop landing on the final allowed token reports the stop reason. A request admitted withignore_eosreports the limit reason when it exhausts its output limit even when its last generated token is end-of-sequence, and a page-capacity early finish reports the limit reason before the declared limit.docs/TODO.txt: drops the roadmap item this closes.ignored_eos_at_the_output_limit_finishes_for_length(helios/src/server/runtime/phase_tests.rs): a one-token prompt withignore_eos: trueand a two-token limit, EOS sampled on both generated tokens, driving Init → Prefill → Snapshot → Decode → Snapshot and asserting the terminal event is(generated tokens 2, reason "length", Termination::OutputLimit).Runtime-only: no package format, prepared contract, graph, kernel, or recipe change, so existing packages need no re-export.
Validation
CPU, release Helios library suite on the build machine (no GPU), commit
449b8708:c65024dbwith only the test applied, at the terminal-event assertion (left: [(2, "stop", Eos)],right: [(2, "length", OutputLimit)]), and passes with the fix.--lib ignored_eos): 3 passed, the new case plus the two existingserver::logprobsignore_eoscases.c65024dbwith only the test added, the new case being the only difference. The nine shared failures are environmental on both trees: fourengram::readerstaging allocation cases (the tests request 8 MiBRLIMIT_MEMLOCK, the host caps it lower), four missingFLARE_TEST_TEMPLATE_DIRfixture cases, and onepackage::cachestaging case.rustfmt --checkon the touched file: no new drift.Separately, builds at
0ce6de86served one prepared package side by side on one GPU (unsloth/gemma-4-E4B-it-NVFP4TP1 on SM120, no MTP draft,--max-concurrency 2) with the same request to both:/v1/chat/completions, prompt "Reply with exactly the word: ok", which stops naturally after two tokens (ok+ end-of-sequence). Requests carried an explicitseedrather thantemperature: 0. Flare has no greedy mode to request: the decoder clamps client sampling values to the package's declared range before validation (helios/src/server/request.rs,sampling_number), sotemperature: 0is served as the declared minimum or the prepared default, and the model-layer rejection inSamplingParameters::validateis never reached over HTTP, asdocs/api.md(Clamping and rejection) documents. Six requests per cell, cells interleaved between the servers, except the defect cell at thirty:max_tokensignore_eosstop/ 2 tok ×6stop/ 2 tok ×6stop/ 2 tok ×30length/ 2 tok ×30stop/ 10 tok ×6length/ 10 tok ×6stop/ 2 tok ×6stop/ 2 tok ×6length/ 1 tok ×6length/ 1 tok ×6length/ 1 tok ×6length/ 1 tok ×6Row 2 is the defect: the limit sits exactly at the natural stop, so the last sampled token is the end-of-sequence token, and the unmodified build reports
stopfor a request that ran to its output limit. Row 3 is the same request with more room. Rows 1, 4, 5 and 6 show the unchanged paths: honouring EOS, and truncation before the EOS. Token counts and returned text matched between the builds in every cell.Limitations
seed, no MTP, and the/v1/chat/completionspath. The Anthropic and Responses surfaces derive from the sameTermination, but were not exercised over the wire.seedmakes a cell repeat, and every cell above produced a single distinct token stream on each build, but it is not a bit-identical guarantee across requests: in one pass atmax_tokens=32the unmodified build produced two different streams in six requests. The classification rule held in every request observed on both builds: anignore_eosrequest must not reportstopbecause its last token was an ignored end-of-sequence token.EosCount = 0) is unchanged and was not re-validated. The runtime change is limited to termination classification, but the resultingTerminationalso affects decoder finalization and tool-call handling as disclosed above.lengthmatches vLLM for the same request.ignore_eoschange behavior; the other termination paths are untouched, and their encoders read the sameTerminationvalues as before.Written with AI assistance; reviewed before opening.