Repository navigation
Batched-decode engine runtime (attention-mask path) + benchmark - #335
Merged
Merged
Conversation
stikves
force-pushed
the
sukru/public-batched-stage2-engine
branch
from
October 19, 2026 21:37
34ab73d to
085c80b
Compare
Add the engine runtime for batched LLM bundles that take an additive attention mask, with llm-benchmark as the first consumer. - CoreAISequentialEngine: decodeBatch runs one forward step over N rows, and also handles rows at different cache positions during decode when the graph has an attn_mask input. lockstepBatchedGenerate generates from equal-length prompts together. prefillRow prefills one prompt and copies its KV prefix into one row of a batched cache. The engine accepts an optional attn_mask input and fills a causal mask for each step. - Per-request state (in-flight token, prefix-hit count, logits buffer, input handler) moves to GenerationSessionState, so separate sessions no longer cancel each other or share buffers. - Handlers: builders for batched token IDs, position IDs and masks; a KV cache that holds N rows, with copyRegion and copyRowPrefix for per-row copies; and the BatchedDecodeStep protocol. The per-row copies move raw bytes, so 8-bit KV caches work as well. - BatchedSampler samples one token per row with per-row configurations. The single-row iterator now goes through it with one row. - Bundle: LanguageConfig and LanguageModelBundle read max_batch_size, which defaults to 1. ModelBundle accepts metadata_version 0.3 as well as 0.2. - llm-benchmark: new --batch-size and --inference-engine-variant flags. Batches larger than 1 need --inference-engine-variant coreai-sequential. Existing single-sequence bundles run as before. prefillRow, maxBatchCapacity and the ragged decode path are there for a future scheduler and have no caller yet.
stikves
force-pushed
the
sukru/public-batched-stage2-engine
branch
from
October 21, 2026 03:00
085c80b to
9a307a6
Compare
stikves
marked this pull request as ready for review
October 21, 2026 03:01
stikves
requested review from
DawerG,
Lewis300,
alejandro-isaza,
kevchengcodes,
srjoglekar246 and
tjia1818
October 21, 2026 19:27
srjoglekar246
approved these changes
Oct 21, 2026
14 of 22 tasks
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.
This PR adds the engine runtime for batched LLM bundles that take an additive attention mask, and
uses
llm-benchmarkas the first consumer. The scheduler and server that would drive continuousbatching are not part of this PR.
It builds on the batched export from #334, the KV-state fix from #354 and the 8-bit KV-cache support
from #356, all now on main.
What this adds
CoreAISequentialEnginedecodeBatchruns one forward step over N rows and returns each row's last-token logits.When the rows sit at different cache positions (decode only, and only on graphs with an
attn_maskinput), every row writes its new token at the longest row's slot, and the shorterrows are copied back to their own position afterward.
lockstepBatchedGenerategenerates from N equal-length prompts together, with per-row samplingconfigurations. The benchmark uses it.
prefillRowprefills one prompt in a batch-1 scratch session and copies its KV prefix into onerow of a batched cache. When the bundle has a prefill graph, the prompt runs through that chunked
executable, so the decode executable's heap is not resized for each new row.
attn_maskinput and fills a causal mask for each step.now live on
GenerationSessionState, so separate sessions no longer cancel each other or sharebuffers. The engine's
lastPrefixHitCountreports the most recently driven session.BatchedInputBuilderbuilds the batched token IDs, position IDs and additive mask, includingthe per-row variants for ragged decode.
GrowingNDArrayStatecan hold a batch of rows.copyRegionmoves one row's slice betweensequence slots, and
copyRowPrefixcopies a batch-1 prefix into one row. Both copy raw bytes, so theywork for 8-bit KV caches as well as fp16, bf16 and fp32. The sequence dimension
is now always taken as the second-to-last dimension, so a dynamic batch dimension is not
mistaken for it.
BatchedDecodeStepprotocol describes the batched step and row admission, with ausage example.
BatchedSamplersamples one token per row, each with its own configuration. The single-rowpath in
SequentialIteratornow calls it with one row.LanguageConfigandLanguageModelBundlereadmax_batch_size, which defaults to 1when absent.
ModelBundleacceptsmetadata_version0.3 as well as 0.2.llm-benchmark: new--batch-sizeand--inference-engine-variantflags. Batches largerthan 1 need
--inference-engine-variant coreai-sequential, and a batch larger than the bundle'smax_batch_sizeis rejected.Scope and behavior
--batch-size, the benchmark reads thebatch size from the graph's fixed leading dimension, which is 1 for these bundles and for
dynamic-batch graphs.
prefillRow,maxBatchCapacityand the ragged path indecodeBatchare there for a futurescheduler. Nothing in this PR calls them.
Public API
InputLayout.attnMaskName, next to the existing input-name fields.package(read byllm-benchmark) or internal: the batched enginemethods,
BatchedSampler,BatchedDecodeStep, the per-row KV copies, andmaxBatchSize.LanguageConfiginitializer is unchanged.max_batch_sizeis read only when decodingbundle metadata.
Validation
swift format lint --strictis clean on the changed files.min/max, NDArray allocation cost) can fail when the whole suite runs in parallel; they pass on their
own, and this PR does not touch the code they time.
shape resolution, the per-row copies (fp16, fp32 and int8, with leading and non-leading batch dims), per-session generation state, the additive causal mask,
and parsing of
metadata_version0.3 andmax_batch_size.coreai-sequentialandcoreai-pipelinedengines, for:bundles on main either.
--attention-mask batched --dynamic-batch-size 4(metadata 0.3) runs through
llm-benchmarkat about 99 tok/s at batch 1 and about 220 tok/saggregate at batch 4 (debug build, M2 Max).