Repository navigation
Host the Qwen3.8-Flash CUDA-graph decode loop in the Engine's static path - #2728
Tianlei Wu (tianleiwu) wants to merge 9 commits into
Conversation
…th auxiliary sessions (#2725) ## Description Adds `session_options.custom_ops_libraries`, a list of custom-op libraries registered in order after the existing single `custom_ops_library`, and exports `InheritSessionOptionsFromParent` so components that create auxiliary sessions (for example MTP/draft heads) can inherit the decoder's providers, provider options, config entries and custom ops. Models whose graphs use several custom-op domains (fused attention, hyper-connection, GEMM and head ops shipped next to the model package) currently cannot name more than one library. This change is generic and has no dependency on any model type; it is the first step of the Qwen3.8-Flash static-graph backend series (see the end of this description), but stands alone and targets `main`. ## Summary of Changes | File | Change | |------|--------| | `src/config.h`, `src/config.cpp` | New `session_options.custom_ops_libraries` (JSON string array; scalar values such as `"custom_ops_libraries": "ops.so"` are rejected during parsing). Overlays append to `custom_ops_libraries` (and `[]` leaves existing entries unchanged). `InheritSessionOptionsFromParent` exposes the existing inheritance helper; `custom_ops_library` and `custom_ops_libraries` are inherited together when the child sets neither (note that `"custom_ops_libraries": []` leaves the vector empty and therefore still inherits from the parent unless `custom_ops_library` is set). | | `src/models/model.cpp` | `custom_ops_library` and then every `custom_ops_libraries` entry go through the same path-traversal and allow-list checks, with duplicate resolved paths registered once. All paths are validated against the allow-list before loading starts; validated libraries are then loaded and registered sequentially in order (so a later load/registration error can still leave earlier libraries registered on `session_options`). | | `test/CMakeLists.txt`, `test/cpp/engine/custom_ops_test_lib/custom_ops_test_lib.cpp`, `test/cpp/engine/custom_ops_libraries_tests.cpp` | Builds two small test custom-op shared libraries (`custom_ops_test_first` and `custom_ops_test_second`) and tests parsing/ordering, overlay rules, rejection of non-string entries and scalar values, inheritance (including `[]` and `CreateMtpDecoderConfig`), allow-list validation, and actual library loading, ordering, and deduplication. | ### Library Location Requirement Every entry in `custom_ops_library` and `custom_ops_libraries` must resolve to an existing file inside one of the allowed directories: 1. The model folder (alongside `genai_config.json`), 2. A registered Execution Provider (EP) library directory, or 3. The current working directory. Previously, an unresolved bare library name in `custom_ops_library` was passed directly to `RegisterCustomOpsLibrary` and could be resolved through the platform loader search path (`LD_LIBRARY_PATH` / `PATH`). Unresolved entries are now rejected before loading so the directory allow-list cannot be bypassed; users should place custom-op libraries in one of the allowed directories above (typically the model folder). ## Testing - CPU-only Release build (`-DUSE_CUDA=OFF`) on `main`: `engine_unit_tests` **929 passed, 0 failed, 2 skipped** (including all 9 tests in `CustomOpsLibrariesConfigTest`, which load real test custom-op shared libraries to verify registration, ordering, and deduplication). - `lintrunner`, `clang-format` (20.1.8) and `git diff --check` are clean. ## Motivation and Context Found while hosting a model that uses seven custom-op libraries. Registering them through one `custom_ops_library` entry was not possible, and auxiliary sessions did not inherit the decoder's custom ops, so head graphs failed to load without repeating the whole session configuration. This is the first PR of a stack that adds an Engine-hosted CUDA-graph MTP backend for Qwen3.8-Flash-Next (greedy only, one resident request, Linux/CUDA, no prefix cache). The follow-ups are drafts stacked on `kvaishnavi/qwen-38-flash-run`, so this commit is also carried as the first commit of the next PR (#2726) until this one merges. The stack: #2726 (CUDA data plane library) -> #2727 (model type and device engine) -> #2728 (Engine hosting) -> #2729 (qualification tools). ## Checklist - [x] Tests added/updated - [x] No breaking changes - [ ] Documentation updated (the option is described in the PR; no dedicated config documentation exists for `session_options` yet) --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
7463063 to
9a9a946
Compare
be1c6f9 to
4719a3f
Compare
…culative stats - Propagate unchecked device decode status errors in Qwen38GraphSession::Release() and Begin() (and capture deferred teardown errors) so turns ending via stop strings, cancellation, rewind, or replacement still check device validation bits before Reset(). - Feed the committed RequestStepResult back to Qwen38GraphSession::OnStepCommitted() after Request::CommitExternalTokens so tokens_emitted, tokens_discarded, and accepted/bonus counts reflect actual committed tokens. - Add unit tests and speculative-stats assertions for EOS, stop-string, and canceled turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
4719a3f to
17b0299
Compare
|
Rebased onto the new head of #2727 and force-pushed ( |
…culative stats - Propagate unchecked device decode status errors in Qwen38GraphSession::Release() and Begin() (and capture deferred teardown errors) so turns ending via stop strings, cancellation, rewind, or replacement still check device validation bits before Reset(). - Feed the committed RequestStepResult back to Qwen38GraphSession::OnStepCommitted() after Request::CommitExternalTokens so tokens_emitted, tokens_discarded, and accepted/bonus counts reflect actual committed tokens. - Add unit tests and speculative-stats assertions for EOS, stop-string, and canceled turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
17b0299 to
083043a
Compare
…culative stats - Propagate unchecked device decode status errors in Qwen38GraphSession::Release() and Begin() (and capture deferred teardown errors) so turns ending via stop strings, cancellation, rewind, or replacement still check device validation bits before Reset(). - Feed the committed RequestStepResult back to Qwen38GraphSession::OnStepCommitted() after Request::CommitExternalTokens so tokens_emitted, tokens_discarded, and accepted/bonus counts reflect actual committed tokens. - Add unit tests and speculative-stats assertions for EOS, stop-string, and canceled turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
083043a to
e864af9
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Pending look-ahead cycles can silently discard device errors after a stop string or cancellation, while multi-draft acceptance statistics also remain incorrect.
3 open issues
What changed in this PR
Hosts the qwen38_flash_graph device backend introduced in #2727 through the Engine’s static path, integrating GPU decode cycles with request lifecycle management.
Changes:
- Adds a single-slot executor and cache manager with chunked prefill, continuation, and rewind support.
- Adds token-wise commit handling for stop conditions and hooks for capability validation and speculative statistics.
- Documents the backend and adds scripted-device tests for lifecycle, decoding, and error handling.
| File | Description |
|---|---|
test/cpp/engine/qwen38_graph_test_doubles.h |
Adds a scripted decode device for Engine tests. |
test/cpp/engine/qwen38_graph_executor_tests.cpp |
Tests decoding, lifecycle, limits, and device failures. |
src/engine/scheduled_requests.h |
Declares executor-token staging. |
src/engine/scheduled_requests.cpp |
Routes staged tokens through request commit handling. |
src/engine/request.h |
Declares external-token commit support. |
src/engine/request.cpp |
Applies stop conditions to executor-selected tokens. |
src/engine/qwen38_graph_executor.h |
Defines the session, executor, and single-slot cache manager. |
src/engine/qwen38_graph_executor.cpp |
Implements device orchestration and speculative accounting. |
src/engine/model_executor.h |
Adds token-selection, validation, and statistics hooks. |
src/engine/engine.cpp |
Integrates backend creation, commit notifications, and capabilities. |
src/engine/cache_manager.h |
Adds static batch and session-length capability hooks. |
docs/paged_attention_engine.md |
Documents the backend’s behavior and restrictions. |
Review effort: Balanced
Akshay Sonawane (apsonawane)
left a comment
There was a problem hiding this comment.
Reviewed all 12 changed files at e864af9 plus the production device, request, cache and stats contracts. One new cross-Engine environment-state defect is recorded inline. The three existing unresolved threads on deferred status, evaluated-draft counts after truncation, and partial-accept accounting remain valid; I did not create duplicate findings. The scripted device's Drain behavior should not be taken as evidence that the production status path is fixed. No repository build, GPU execution or actual concurrent-Engine test was performed; this is source/control-flow review, not approval.
…culative stats - Propagate unchecked device decode status errors in Qwen38GraphSession::Release() and Begin() (and capture deferred teardown errors) so turns ending via stop strings, cancellation, rewind, or replacement still check device validation bits before Reset(). - Feed the committed RequestStepResult back to Qwen38GraphSession::OnStepCommitted() after Request::CommitExternalTokens so tokens_emitted, tokens_discarded, and accepted/bonus counts reflect actual committed tokens. - Add unit tests and speculative-stats assertions for EOS, stop-string, and canceled turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The wasted look-ahead cycle of a stop-string or canceled turn is never waited for, so its record stays unconsumed and only the device status bits can tell that it failed. The production device keeps that record after Drain() and, since #2727, CheckDecodeStatus() reads the accumulated bits also then; the Reset() that follows clears them. The executor already drains and checks the status before every Reset() (Begin, Release, end of turn). The scripted device now behaves like the production one so that this is actually exercised: Drain() completes the running cycle but leaves its record in flight, the cycle can accumulate error bits (fail_cycle), CheckDecodeStatus() reads them with a cycle in flight or unconsumed, and the device logs the drain/status/reset order. New tests: a failing look-ahead cycle of a stop-string turn surfaces when the turn is rewound and fails the next request instead of being reset away; the same for a canceled turn; a clean drained cycle lets the next request run. Document the Drain() + CheckDecodeStatus() contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e864af9 to
5c70b96
Compare
A cycle that the Engine ends inside its accepted prefix (turn or session limit, stop string, EOS) was reported with min(proposed, accepted + 1) evaluated drafts. With three accepted drafts but room for one token that is two evaluated drafts for one accepted one, a 50% acceptance rate where Engine::RecordSpeculativeCommit reports 100%. Count the draft positions the turn examined up to its terminal token, like Request::CommitAcceptedDraftsForTransaction does: every accepted draft it consumed (an EOS draft is examined but not accepted) plus the rejected position only when the turn reached the correction token. The scripted device gains multi-draft cycles (SetDrafts: 1..3 drafts per verify, any accepted prefix, reported through accepted_count/proposed_count like the production device). Tests cover limit, session limit, stop string and EOS truncation inside the accepted prefix, on the correction token and on the bonus token; five of them fail with the old formula. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A round that accepted some but not all of its drafts only recorded its histogram entry: the zero/full classification skipped it and partial_accept_rounds was never incremented, nor merged by AddStats. Classify every round like Engine::RecordSpeculativeCommit does, by the number of accepted drafts that were delivered (none: zero, all of the proposed ones: full, otherwise partial), independently of whether a correction token followed, and merge partial_accept_rounds in AddStats. Bonus and correction tokens keep being counted on their own. Tests cover a 0/1/2/3 accepted sequence of three-draft rounds (counters, histogram, correction/bonus, acceptance rate), a partial round whose correction does not fit the turn, a round truncated inside its accepted drafts, an EOS first draft (zero accept) and the sum over requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…l total The executor hands the prompt length of a turn to its own device (ExpectPrefillTokens, after Reset() and before the first chunk) and never publishes it through the process environment. Pin that with a scripted-device test: two Engines on two owner threads, each with its own model and device, are held at a rendezvous between the publication of the length and the first chunk of every turn, and each device must keep its own total across the chunks of a new turn, a continued turn that re-ingests the history after a Reset, and a canceled turn, while ORT_QMOE_PREFILL_TOTAL_TOKENS stays untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…path Serves `qwen38_flash_graph` through the existing Engine seams so requests, turns, cancellation, stop conditions and speculative statistics stay owned by the Engine. The backend is a single-slot static path: one resident request, greedy decoding, no prefix cache; further requests queue behind it in the static batch scheduler. - `Qwen38GraphCacheManager`: static-cache contract for one row. - `Qwen38GraphExecutor`: a `ModelExecutor` that selects its own tokens. One Engine step is one prompt chunk (the device's dense prefill chunk size) or one whole decode cycle (the folded draft/verify/commit cycle, whose one or two committed tokens are published through `Request::CommitExternalTokens`). Device error bits are checked when a turn ends; a continued or rewound turn re-ingests its history through the same chunked prefill. - `Qwen38GraphSession`: the platform- and CUDA-independent logic between the executor and a `Qwen38DecodeDevice`. - Engine hooks, all defaulting to the previous behaviour for other models: turn validation with capability errors (sampling, repetition penalty, minimum token counts, seeds and oversized sessions are rejected up front), static-path stop strings and limits, speculative-statistics reporting, and `Engine::CreateDependencies` returning early for this model type before any IndexShare or MTP setup. - `Request::CommitExternalTokens` applies EOS, length limits and stop strings per committed token. - Tests (scripted device, no GPU): 34 executor/session cases covering dense chunk streaming and hand-off, cancel during prefill and decode, stop strings, EOS handling, continuation and rewind, all-rejected and all-accepted cycles, device errors, and capability errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…culative stats - Propagate unchecked device decode status errors in Qwen38GraphSession::Release() and Begin() (and capture deferred teardown errors) so turns ending via stop strings, cancellation, rewind, or replacement still check device validation bits before Reset(). - Feed the committed RequestStepResult back to Qwen38GraphSession::OnStepCommitted() after Request::CommitExternalTokens so tokens_emitted, tokens_discarded, and accepted/bonus counts reflect actual committed tokens. - Add unit tests and speculative-stats assertions for EOS, stop-string, and canceled turns. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n Qwen38GraphSession - Extend Qwen38StepResult::tokens to 4 tokens and update PendingStep/OnStepCommitted speculative stats for multi-draft S=2..4 cycles while preserving committed-token clipping. - Export ORT_QMOE_PREFILL_TOTAL_TOKENS at the start of prompt prefill so QMoE memory lending can size the prefill budget. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…evice API The graph model now sizes its prompt chunks from model.qwen38_graph.prefill.chunk (model.qwen38_graph.prefill_chunk_tokens is removed) and receives the expected prompt length through the device instead of the process environment. - CreateQwen38GraphDependencies validates the chunk configuration with ValidateQwen38PrefillChunk and sizes the session from EffectiveQwen38PrefillChunkTokens; a device that names its own chunk still overrides it. - Qwen38GraphSession no longer writes ORT_QMOE_PREFILL_TOTAL_TOKENS: it calls Qwen38DecodeDevice::ExpectPrefillTokens(total) after Reset() and before the first PrefillChunk() of every turn. - The scripted device implements ExpectPrefillTokens (device-local, dropped by Reset()/BeginDecode()) and records the total every chunk saw; the tests use prefill.chunk, cover the default and invalid chunk configurations, the session's even rounding, and the announcement order. - docs/paged_attention_engine.md describes the chunk sizing and the announcement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The wasted look-ahead cycle of a stop-string or canceled turn is never waited for, so its record stays unconsumed and only the device status bits can tell that it failed. The production device keeps that record after Drain() and, since #2727, CheckDecodeStatus() reads the accumulated bits also then; the Reset() that follows clears them. The executor already drains and checks the status before every Reset() (Begin, Release, end of turn). The scripted device now behaves like the production one so that this is actually exercised: Drain() completes the running cycle but leaves its record in flight, the cycle can accumulate error bits (fail_cycle), CheckDecodeStatus() reads them with a cycle in flight or unconsumed, and the device logs the drain/status/reset order. New tests: a failing look-ahead cycle of a stop-string turn surfaces when the turn is rewound and fails the next request instead of being reset away; the same for a canceled turn; a clean drained cycle lets the next request run. Document the Drain() + CheckDecodeStatus() contract. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A cycle that the Engine ends inside its accepted prefix (turn or session limit, stop string, EOS) was reported with min(proposed, accepted + 1) evaluated drafts. With three accepted drafts but room for one token that is two evaluated drafts for one accepted one, a 50% acceptance rate where Engine::RecordSpeculativeCommit reports 100%. Count the draft positions the turn examined up to its terminal token, like Request::CommitAcceptedDraftsForTransaction does: every accepted draft it consumed (an EOS draft is examined but not accepted) plus the rejected position only when the turn reached the correction token. The scripted device gains multi-draft cycles (SetDrafts: 1..3 drafts per verify, any accepted prefix, reported through accepted_count/proposed_count like the production device). Tests cover limit, session limit, stop string and EOS truncation inside the accepted prefix, on the correction token and on the bonus token; five of them fail with the old formula. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A round that accepted some but not all of its drafts only recorded its histogram entry: the zero/full classification skipped it and partial_accept_rounds was never incremented, nor merged by AddStats. Classify every round like Engine::RecordSpeculativeCommit does, by the number of accepted drafts that were delivered (none: zero, all of the proposed ones: full, otherwise partial), independently of whether a correction token followed, and merge partial_accept_rounds in AddStats. Bonus and correction tokens keep being counted on their own. Tests cover a 0/1/2/3 accepted sequence of three-draft rounds (counters, histogram, correction/bonus, acceptance rate), a partial round whose correction does not fit the turn, a round truncated inside its accepted drafts, an EOS first draft (zero accept) and the sum over requests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…l total The executor hands the prompt length of a turn to its own device (ExpectPrefillTokens, after Reset() and before the first chunk) and never publishes it through the process environment. Pin that with a scripted-device test: two Engines on two owner threads, each with its own model and device, are held at a rendezvous between the publication of the length and the first chunk of every turn, and each device must keep its own total across the chunks of a new turn, a continued turn that re-ingests the history after a Reset, and a canceled turn, while ORT_QMOE_PREFILL_TOTAL_TOKENS stays untouched. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ot hosting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
5c70b96 to
6b248b7
Compare


Description
Serves the
qwen38_flash_graphmodel type (#2727) through the Engine's existing static path. The CUDA-graph decode loop (the MTP draft -> verify -> commit cycle, the folded cycle, the dense fast prefill and the shared weight pool) stays on the device; the Engine keeps ownership of requests, turns, cancellation, stop conditions and speculative statistics. It is a single-slot backend: one resident request, greedy decoding, no prefix cache; further requests queue behind it in the static batch scheduler. Multi-slot hosting comes with ragged batching later in the series.Multi-draft cycles of 2 to 4 tokens (S=2..4) are reported through
Qwen38StepResult::tokensand the speculative statistics. The prompt length of a turn is handed to the device withQwen38DecodeDevice::ExpectPrefillTokens(after the deviceReset(), before the first prompt chunk), so QMoE memory lending can size the prefill budget from state that belongs to that device; the executor itself neither reads nor writes the process environment (ORT_QMOE_PREFILL_TOTAL_TOKENSis published by the device only around a single run). The prompt chunk size comes fromEffectiveQwen38PrefillChunkTokensof the reworkedqwen38_flash_graphconfiguration (#2727).Summary of Changes
src/engine/qwen38_graph_executor.{h,cpp}Qwen38GraphCacheManager(static-cache contract for one row),Qwen38GraphExecutor(aModelExecutorthat selects its own tokens: one Engine step is one prompt chunk of the size the device asks for, or one whole decode cycle) andQwen38GraphSession(platform- and CUDA-independent logic between the executor and aQwen38DecodeDevice; checks the device error bits when a turn ends; continuation and rewind re-ingest the history through the same chunked prefill; the speculative statistics count the drafts that were actually evaluated and classify rounds by the drafts they delivered, asEngine::RecordSpeculativeCommitdoes).src/engine/engine.cpp,model_executor.h,cache_manager.hRequestmutation (sampling, repetition penalty, no-repeat n-grams, minimum token counts, seeds, guidance and oversized sessions are rejected with a clear error), speculative-statistics reporting, static batch and length limits, andCreateDependenciesreturning early for this model type before any IndexShare/MTP setup.src/engine/request.{h,cpp},scheduled_requests.{h,cpp}Request::CommitExternalTokensapplies EOS, length limits and stop strings per committed token (precedence: stop string, EOS, turn limit, context limit; later tokens of a cycle are dropped);ScheduledRequestsstages the executor's tokens. Stop strings are accepted on this static path only.docs/paged_attention_engine.mdExpectPrefillTokens, theDrain()+CheckDecodeStatus()contract and the speculative statistics.test/cpp/engine/qwen38_graph_executor_tests.cpp,qwen38_graph_test_doubles.hQwen38DecodeDeviceand the real Engine: dense chunk streaming (any size, shorter than or exactly the chunk, hand-off exactly once and only after the last chunk), hand-off skipped when the first token ends the turn, cancellation between chunks and during decode, stop strings, EOS, continuation and rewind, all-rejected and all-accepted cycles, device errors (including failures that only show afterDrain()), capability errors, evaluated-draft and round counts of truncated rounds, and the expected prefill total of two Engines on two owner threads. The scripted device completes cycles asynchronously like the real one (Drain()leaves the record in flight,CheckDecodeStatus()reports the accumulated status bits,Reset()clears them).The dynamic/paged Engine path and the MTP path added by #2714 are not changed: the new
Request/ScheduledRequestsentry points are only reached from this executor, which never attaches drafts (MaxDraftTokensPerStep() == 0).Review feedback incorporated
Qwen38GraphSession::Release()andBegin()propagate an unchecked device decode status error (and capture a deferred teardown error), so a turn that ends through a stop string, cancellation, rewind or replacement still checks the device validation bits beforeReset().RequestStepResultis fed back toQwen38GraphSession::OnStepCommitted()afterRequest::CommitExternalTokens, sotokens_emitted,tokens_discardedand the accepted and bonus counts reflect the tokens that were actually committed. Tests cover EOS, stop-string and canceled turns.Drain()(the look-ahead cycle that is completed when a turn is canceled, ends on a stop string or is rewound) fails the request instead of being cleared by the nextReset(). Every such path runsDrain()and thenCheckDecodeStatus(), which on the reworked device contract (Add the qwen38_flash_graph model type and its device decode engine #2727) reads the status bits accumulated since the lastReset()even when a finished cycle record is left unconsumed, so no executor change was needed; the scripted device now behaves like the real one andAFailureOfTheLookAheadCycleVisibleOnlyAfterDrain*/AFailureOfTheFirstCycleVisibleOnlyAfterDrainSurfacesAfterACancellationcover it.partial_accept_roundsis counted for every partial round, andAddStatsmerges it.ExpectPrefillTokens), not of the process: the executor no longer callssetenv.EnginesOnTwoOwnerThreadsKeepTheirOwnExpectedPrefillTotalsruns two Engines on two owner threads with their own scripted devices and checks that each device sees its own total in every chunk of a turn (first turn, continued turn, canceled turn, following request) and thatORT_QMOE_PREFILL_TOTAL_TOKENSis untouched.Testing
-DUSE_CUDA=ON -DENABLE_QWEN38_PLANE=ON, CUDA 13.0, H200/sm_90) on top of the reworked Add the qwen38_flash_graph model type and its device decode engine #2727 (85595784):engine_unit_tests --gtest_filter='*Qwen38*'135 passed, 0 failed. The full binary runs 1249 tests: 1209 passed, 22 skipped, 18 failed, and the 18 are exactly the failures of the unmodified base in this environment (theCuda*provider-bridge tests needGetProviderfrom the in-tree CUDA EP, which the rc5 plugin EP used here does not export; the threeModelPackage.*tests (TokenizerResolvesThroughSharedAsset,ExternalInitializersFolderResolvesThroughSharedAsset,FlatDirectoryRejectsSha256Reference) fail identically on unmodifiedmainand are unrelated to this change).-DUSE_CXX17=1(the Android configuration) and as C++20 with-Wall -Wextrawithout diagnostics.og.Engine(original head of this PR, H200, v12 package): token identity against the reference runner 6/6 (EOS mode) and 6/6 (fixed length) with equal cycle and accept counts, MTP stream equal to the no-MTP stream 6/6, andengine_behaviours.py27/27 in the bit-exact (--prefill steps) and the fast-prefill modes. The tip of the whole stack was re-qualified end to end on the rc5 package (see the final PR of the runtime series, [Qwen38] Adopt the SpeculativePrefixCommit contract of ORT #33474 and fix the conv-state replay orientation #2824).Motivation and Context
The Generator-hosted version of this loop could not share the Engine's request lifecycle (turns, cancellation, stop strings, statistics, queueing), which is what the serving stack uses. Hosting it as a static-path executor reuses all of that while keeping the single-slot device state (fixed in-place KV, recurrent-state bank ring) that cannot be paged. It is an alternative backend next to the paged Engine MTP path of #2714, not a replacement.
Limits of this PR: greedy only; one resident request; no prefix cache and no caller drafts; a continued turn or the replay after
RewindToStartOfTurnre-ingests the whole history (0.3 s at 2k, 3.2 s at 32k with the fast prefill); sessions up tokv_capacity - 4tokens; Linux + CUDA only. The GPU gates need an exported package, the CUDA EP and the contrib operators, which CI does not have, so CI covers the scripted-device tests only.Checklist