Skip to content

Port upstream #197 (ignore_eos) and #221 (MTP graph topology class) - #101

Merged
ashalliants merged 4 commits into
masterfrom
feat/upstream-prs-197-221
Sep 14, 2026
Merged

ashalliants merged 4 commits into
masterfrom
feat/upstream-prs-197-221

Conversation

@ashalliants

Copy link
Copy Markdown
Owner

Brings in two open upstream Neroued/ninfer PRs, keeping their original authorship, plus the changes our Windows build needed.

Neroued#197: honour ignore_eos on Chat Completions

Until now ninfer-serve accepted ignore_eos and ignored it. A request stopped at the checkpoint's own EOS instead of running to max_tokens, so throughput comparisons against SGLang or vLLM measured decode runs of different lengths. It now sets stop.include_model_defaults = !ignore_eos. Stop strings and stop token ids from the caller still apply. Only /v1/chat/completions is affected.

Neroued#221: give MTP graph profiles a topology class

mtp_graph_profiles put every profile in class 0. At a draft window of 6 or more, one CUDA graph executable would then cover both the prompt and the chunked small-T attention routes, and startup fails with cudaErrorGraphExecUpdateFailure. Our MTP cap is 5, so we can't hit this today. At widths up to 5 the profiles are unchanged, because the route-flip boundary only applies from width 7. The 35B route table in our fork matches the predicate the fix mirrors.

Fork-specific follow-up

  • The upstream test linked CUDA::cudart, which collides on MSVC with the cudart_static that ninfer_core already links (LNK2005). It now links ${NINFER_CUDART_TARGET} instead.
  • Added ignore_eos coverage to test_openai_schema.cpp, since upstream's PR had no test.

Verification

  • Full build-ninja build passes on MSVC 14.44 / CUDA 12.8.
  • ctest -R "qwen3_6_35b_a3b|openai|anthropic|serve_options" -E real: 6 passed, 1 skipped (the DFlash load-plan test needs the artifact).
  • Checked that the new MTP test catches the bug: built against the old variant.cpp, it fails at k=6 ("class 0 carries both prompt and chunked_small_t"). With the fix it passes.
  • Not done: end-to-end ignore_eos run against a live server on the 3090.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GGPoSVoQSK7bMu5SdmNfBT

Thireus and others added 3 commits September 14, 2026 14:25
ninfer-serve accepts the ignore_eos field and ignores it, so a request generates the checkpoint's own number of tokens rather than max_tokens. With max_tokens 512 and ignore_eos true, a chat completion that stops naturally after 267 tokens should run to 512, which is what SGLang returns for the same request; ninfer returned 267 with finish_reason "stop". Any cross-engine throughput comparison then measures decode rates over windows of different lengths.

StopPolicy already carries include_model_defaults (include/ninfer/types.h:234), and merge_stop_policy (src/targets/qwen3_6/impl/frontend/frontend.cpp:389) appends the tokenizer's default stop tokens only when it is set. The serve layer had no way to clear it.

Add a bool ignore_eos to GenerationRequest, parse it with the existing get_bool helper in parse_stop, and map it in to_request_options as options.stop.include_model_defaults = !request.ignore_eos. Caller-supplied stop strings and stop token ids are unaffected: only the checkpoint's own defaults are suppressed, which is the documented meaning of the field in vLLM, SGLang and llama.cpp.

The default is false, so a request that does not send the field behaves exactly as before, and only the OpenAI Chat Completions path is touched (/v1/messages and /v1/responses are not).

Verified against the same server and prompt: ignore_eos=true gives completion_tokens=512 and finish_reason length; ignore_eos=false gives 267 and stop, unchanged.
instantiate_graph_family builds one cudaGraphExec_t per topology class and
installs every other profile of that class through cudaGraphExecUpdate, which
cannot cross a change of node count. dflash_graph_profiles honours that twice:
it breaks the frontier where the attention route flips, and derives
topology_class from the same predicate. mtp_graph_profiles did neither, so at a
draft window of six or more one executable served both the prompt and the
chunked small-T route.

Today the MTP draft cap of five hides it. With the cap raised - four constants,
no other change - draft windows 6 through 15 fail at startup with
cudaErrorGraphExecUpdateFailure; with this change all of them run.

The shared predicate is renamed verify_uses_chunked_small_t because both
planners now read it, and gains the width bound the real route has:
causal_attention_resolve_route returns Prompt for every envelope past
kMaximumVerifyTokens. verify_route_flip_target locates the boundary by bisecting
that predicate rather than restating the route table, which is how the planner
and the route could drift apart.

At the shipped cap nothing moves: every width up to six resolves to SmallT, so
no class changes and the added boundary sits behind a zero check. Adds
tests/targets/qwen3_6_35b_a3b/test_mtp_graph_profiles.cpp, which asserts the
contract over four capacities, draft windows one to sixteen and all five KV
storages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e_eos

The upstream test linked CUDA::cudart, which on MSVC collides with the cudart_static that
ninfer_core already brings in (LNK2005 on cudaMemGetInfo and friends). NINFER_CUDART_TARGET
is how this tree picks the runtime; linking it also restores the CUDA include path the test
needs for ninfer/ops/sampling.h.

Upstream's ignore_eos change shipped without a test. Pin that an absent field keeps the
checkpoint's stop tokens, that ignore_eos drops them while keeping caller stop strings, and
that it parses on a request with no stop field.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGPoSVoQSK7bMu5SdmNfBT

@codepulse-review codepulse-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 1 should-fix — across 1 file.

Jump to the ⚠️ should-fix items first — they block merge.

Correctness: The request translation and MTP topology logic are consistent; no correctness, security, concurrency, lifecycle, or performance defects found.

Readability: Names and comments clearly explain the route boundary.

Architecture: The new public request control is not documented in docs/serving.md; see the inline comment.

Tests: CI reports all four checks passed. Added schema and MTP contract coverage is relevant; local execution was not performed per the static-review constraint.

⚠️ Outstanding — 1 blocking item

Still unresolved on this PR. Any coding agent — CodePulse or your own AI tool — can work this list; check items off as they land.

  • src/serve/openai_chat_request.cpp:760 — New chat-completions control is missing from serving docs (Important)

Reviewed by CodePulse · AI C++ review with a senior engineer's eye · codepulse.review →

}

void parse_stop(const Json& body, GenerationRequest& output) {
output.ignore_eos = get_bool(body, "ignore_eos", false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Important · Architecture — New chat-completions control is missing from serving docs
Fix: Add ignore_eos to docs/serving.md with its default and stop-policy semantics.
Caution: Keep src/serve/translate.cpp and tests/test_openai_schema.cpp aligned with the documented behavior.

The serving capability table documents stop strings but does not mention ignore_eos, and the repository currently has no other documentation for it. Because unknown top-level fields are otherwise ignored, users cannot discover this extension or understand that it suppresses only checkpoint defaults while preserving caller-supplied stops. The public serving contract should be updated alongside this parser change.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added ignore_eos to the Chat Completions list in docs/serving.md: boolean, default false, drops only the checkpoint's stop tokens (caller stop strings still apply), non-boolean rejected with 400, not honored by /v1/responses or /v1/messages. The 400 case is now pinned in tests/test_openai_schema.cpp.

Review of #101: the field was otherwise undiscoverable, since unknown top-level fields are
ignored. State its default, that it drops only the checkpoint's stop tokens while caller stop
strings still apply, the 400 on a non-boolean (now pinned in the schema test), and that the
Responses and Messages endpoints do not honor it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GGPoSVoQSK7bMu5SdmNfBT

@codepulse-review codepulse-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No action needed — ship it.

Correctness: No actionable defects found. The stop-policy mapping is isolated to Chat Completions, and graph topology classes match the runtime route predicate and envelope boundaries.

Readability/Architecture: Clear and consistent with existing patterns.

Security/Performance: No issues identified.

Tests: Added coverage is appropriate; authoritative CI reports all four checks passing. Local execution was skipped per the static-review requirement.


Reviewed by CodePulse · AI C++ review with a senior engineer's eye · codepulse.review →

@ashalliants
ashalliants merged commit 7ff4196 into master Sep 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants