feat(deepseek): expand continuous batching across fixed kernels - #145
feat(deepseek): expand continuous batching across fixed kernels#145hashiqiqixian wants to merge 4 commits into
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughDeepSeek V4 serving now supports four-slot rank-local prefill, capacity-aware decode and prefill dispatch, mixed decode/prefill scheduling, and request-local MTP tail-state paging. Runtime configuration and documentation expose the new limits. ChangesDeepSeek V4 serving
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Scheduler
participant ReplicaEngineCore
participant serving_worker
participant DeepSeekV4ModelRunner
Scheduler->>ReplicaEngineCore: select mixed decode and prefill work
ReplicaEngineCore->>serving_worker: execute step
serving_worker->>serving_worker: partition decode before prefill
serving_worker->>DeepSeekV4ModelRunner: dispatch fixed-capacity batches
DeepSeekV4ModelRunner->>DeepSeekV4ModelRunner: page MTP tail state and run slots
DeepSeekV4ModelRunner-->>serving_worker: return logits and generated tokens
Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (4)
pypto_serving/model/deepseek/npu_runner.py (4)
4546-4570: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider sharing the local-row assignment logic with
_decode_assignment.
_prefill_assignmentand_decode_assignmenthave the same body: dense per-rank local-row assignment with a capacity guard. They differ only in the capacity field (prefill_batchversusdecode_batch), the error text, and decode's assignment cache. A shared private helper that takesranks, the capacity, and a label would remove the duplication.The return type is also
_DeepSeekV4DecodeAssignment, and the prefill caller at Line 1769 uses onlylocal_rows. Renaming the dataclass to a mode-neutral name would make the shared use clear.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pypto_serving/model/deepseek/npu_runner.py` around lines 4546 - 4570, Extract the duplicated dense per-rank local-row assignment from _prefill_assignment and _decode_assignment into a shared private helper accepting ranks, capacity, and a mode label, while preserving each method’s capacity validation and error wording. Reuse the helper from both assignment paths, retain decode’s assignment cache, and rename _DeepSeekV4DecodeAssignment to a mode-neutral type name with all references updated.
2744-2782: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider caching
_prefill_fwd_argsper slot.The staged buffers are fixed shared tensors, so the argument tuple for a given
slotis stable across prefill steps. This method now runs once per active slot, so it rebuilds the weight dict and re-slices 21 tensors up to four times per step._decode_fwd_argsalready caches its tuple in_decode_fwd_args_cache. Adict[int, tuple[Any, ...]]keyed byslotwould give the same benefit here.The transposed
(prefill_batch, ranks, ...)storage that makes[:, slot]contiguous is correct and worth keeping.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pypto_serving/model/deepseek/npu_runner.py` around lines 2744 - 2782, Cache the tuple produced by the prefill argument builder per slot, following the existing _decode_fwd_args_cache pattern. In the method containing _PREFILL_FWD_RANK_SLOT_NAMES and _ordered_layer_args, return the cached tuple for a valid slot, and store the newly built result under that slot after constructing and resident-marking the values. Preserve the existing [:, slot] slicing and contiguity checks.
908-934: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider writing
prefill_x_hcdirectly into the shared buffer.
prefill_batchgrew from 1 to 4, so this path now allocates and fillsranks × prefill_batch × token_rows × hc_mult × hiddenfloat32 twice per prefill step: once for theclone()at Line 915 and once for thecontiguous()at Line 933. The result is then copied again intobuffers.x_hcby_stage_prefill_fwd_inputs. With the production layout this is roughly a 4x increase over the previous shape.
_pack_decode_x_hcalready supports anout=destination for exactly this reason. An equivalentout=parameter here would letprepare_prefill_inputsfill the shared prefill buffer in place and remove both temporaries.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pypto_serving/model/deepseek/npu_runner.py` around lines 908 - 934, Update the prefill packing helper around `rank_rows` to accept an `out` destination, write `prefill_x_hc` directly into it, and avoid both the intermediate `clone()` and final `contiguous()` allocations. Update `prepare_prefill_inputs` and `_stage_prefill_fwd_inputs` to pass and reuse the shared `buffers.x_hc` buffer, preserving the existing rank/local-row placement and output layout.
3110-3166: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider batching the tail paging transfers per rank.
Both methods issue one
copy_to/copy_fromper request. With the full decode set this is up to 32 small device transfers in each direction per decode step, on the request thread.
buffers.tail_init_hidden[rank]is contiguous over the whole(decode_batch, hc_mult, hidden)block, and the pool shard has the same layout. Staging all rows for a rank first and then issuing one transfer per rank would reduce this torankstransfers per direction.The tradeoff is that a whole-rank transfer also moves rows for inactive local slots. Confirm that paging those rows in and out is harmless before making the change.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@pypto_serving/model/deepseek/npu_runner.py` around lines 3110 - 3166, Batch MTP tail paging in _page_in_mtp_tail_hidden and _page_out_mtp_tail_hidden by staging all request rows for each rank, then issuing one contiguous transfer per rank instead of one transfer per request. Preserve the existing request-state validation and copy updated rows back to their corresponding request state; confirm that transferring inactive local slots within each rank is harmless before applying whole-rank transfers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@pypto_serving/model/deepseek/npu_runner.py`:
- Around line 4546-4570: Extract the duplicated dense per-rank local-row
assignment from _prefill_assignment and _decode_assignment into a shared private
helper accepting ranks, capacity, and a mode label, while preserving each
method’s capacity validation and error wording. Reuse the helper from both
assignment paths, retain decode’s assignment cache, and rename
_DeepSeekV4DecodeAssignment to a mode-neutral type name with all references
updated.
- Around line 2744-2782: Cache the tuple produced by the prefill argument
builder per slot, following the existing _decode_fwd_args_cache pattern. In the
method containing _PREFILL_FWD_RANK_SLOT_NAMES and _ordered_layer_args, return
the cached tuple for a valid slot, and store the newly built result under that
slot after constructing and resident-marking the values. Preserve the existing
[:, slot] slicing and contiguity checks.
- Around line 908-934: Update the prefill packing helper around `rank_rows` to
accept an `out` destination, write `prefill_x_hc` directly into it, and avoid
both the intermediate `clone()` and final `contiguous()` allocations. Update
`prepare_prefill_inputs` and `_stage_prefill_fwd_inputs` to pass and reuse the
shared `buffers.x_hc` buffer, preserving the existing rank/local-row placement
and output layout.
- Around line 3110-3166: Batch MTP tail paging in _page_in_mtp_tail_hidden and
_page_out_mtp_tail_hidden by staging all request rows for each rank, then
issuing one contiguous transfer per rank instead of one transfer per request.
Preserve the existing request-state validation and copy updated rows back to
their corresponding request state; confirm that transferring inactive local
slots within each rank is harmless before applying whole-rank transfers.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 545e5608-d9fd-4c19-aa83-0c4f65b2cb87
📒 Files selected for processing (15)
docs/dev/model/deepseek-v4.mdpypto_serving/cli/main.pypypto_serving/config/types.pypypto_serving/model/common/executor/executor.pypypto_serving/model/deepseek/npu_executor.pypypto_serving/model/deepseek/npu_runner.pypypto_serving/model/deepseek/offline.pypypto_serving/serving/engine/async_engine.pypypto_serving/serving/sched/scheduler.pypypto_serving/serving/server/serving_worker.pytests/test_deepseek_v4_accuracy.pytests/unit/model/deepseek/test_model_components.pytests/unit/model/deepseek/test_offline.pytests/unit/serving/sched/test_async_scheduler.pytests/unit/serving/server/test_worker_step_protocol.py
ee27f73 to
ee5f21f
Compare
ee5f21f to
d126f55
Compare
8963c1d to
f2b6fc8
Compare
c7d2879 to
e232ed9
Compare
e232ed9 to
137a21a
Compare
hw-native-sys#163 step 7, as much of it as is safe to do while several PRs are open against the files the rest of it would touch. `RuntimeModel.layers` is now defaulted and empty for every loader in the tree, and `LayerWeights` is documented as deprecated with a pointer to what replaced it. Both are kept rather than deleted on purpose: hw-native-sys#168, hw-native-sys#114, hw-native-sys#152, hw-native-sys#145, hw-native-sys#144 and hw-native-sys#132 are open, and some construct a `RuntimeModel`. Removing the field would break them for no gain that cannot wait — it goes when they have landed. `--num-layers-override` in the Qwen example loses its `runtime_model.layers[:n]` slice, which had quietly become a no-op: staging reads `config.num_hidden_layers` and pulls exactly that many layers from the checkpoint, so replacing the config *is* the override now. The knob still works; there is simply nothing eager left to truncate. **The `stage_weights` hook is deliberately not added, and the reason is worth recording because the issue asks for it.** hw-native-sys#163 proposes it "between `_create_runner` and `init_kv_cache`", motivated by Qwen's `DistributedWorker` forking inside `init_kv_cache` — so staging must happen before that. Checking the actual order in `PyptoExecutor.register_model`, it already does: `_compile_model` runs first, and Qwen stages inside it, well ahead of `_create_runner` and the fork. The constraint the hook exists to satisfy is met without it. That leaves the hook as an architectural tidy-up — separating "compile kernels" from "stage weights" into named phases — and it would touch `common/runner/model_runner.py` plus both `npu_runner.py`, which is exactly where the six open PRs are. Adding surface to the most contested files in the repo to formalise a phase ordering that already holds is a poor trade this week. Worth doing after they land, with the timing rationale restated then rather than assumed.
hw-native-sys#163 step 7, as much of it as is safe to do while several PRs are open against the files the rest of it would touch. `RuntimeModel.layers` is now defaulted and empty for every loader in the tree, and `LayerWeights` is documented as deprecated with a pointer to what replaced it. Both are kept rather than deleted on purpose: hw-native-sys#168, hw-native-sys#114, hw-native-sys#152, hw-native-sys#145, hw-native-sys#144 and hw-native-sys#132 are open, and some construct a `RuntimeModel`. Removing the field would break them for no gain that cannot wait — it goes when they have landed. `--num-layers-override` in the Qwen example loses its `runtime_model.layers[:n]` slice, which had quietly become a no-op: staging reads `config.num_hidden_layers` and pulls exactly that many layers from the checkpoint, so replacing the config *is* the override now. The knob still works; there is simply nothing eager left to truncate. **The `stage_weights` hook is deliberately not added, and the reason is worth recording because the issue asks for it.** hw-native-sys#163 proposes it "between `_create_runner` and `init_kv_cache`", motivated by Qwen's `DistributedWorker` forking inside `init_kv_cache` — so staging must happen before that. Checking the actual order in `PyptoExecutor.register_model`, it already does: `_compile_model` runs first, and Qwen stages inside it, well ahead of `_create_runner` and the fork. The constraint the hook exists to satisfy is met without it. That leaves the hook as an architectural tidy-up — separating "compile kernels" from "stage weights" into named phases — and it would touch `common/runner/model_runner.py` plus both `npu_runner.py`, which is exactly where the six open PRs are. Adding surface to the most contested files in the repo to formalise a phase ordering that already holds is a poor trade this week. Worth doing after they land, with the timing rationale restated then rather than assumed.
files the rest of it would touch. `RuntimeModel.layers` is now defaulted and empty for every loader in the tree, and `LayerWeights` is documented as deprecated with a pointer to what replaced it. Both are kept rather than deleted on purpose: hw-native-sys#168, hw-native-sys#114, hw-native-sys#152, hw-native-sys#145, hw-native-sys#144 and hw-native-sys#132 are open, and some construct a `RuntimeModel`. Removing the field would break them for no gain that cannot wait — it goes when they have landed. `--num-layers-override` in the Qwen example loses its `runtime_model.layers[:n]` slice, which had quietly become a no-op: staging reads `config.num_hidden_layers` and pulls exactly that many layers from the checkpoint, so replacing the config *is* the override now. The knob still works; there is simply nothing eager left to truncate. **The `stage_weights` hook is deliberately not added, and the reason is worth recording because the issue asks for it.** hw-native-sys#163 proposes it "between `_create_runner` and `init_kv_cache`", motivated by Qwen's `DistributedWorker` forking inside `init_kv_cache` — so staging must happen before that. Checking the actual order in `PyptoExecutor.register_model`, it already does: `_compile_model` runs first, and Qwen stages inside it, well ahead of `_create_runner` and the fork. The constraint the hook exists to satisfy is met without it. That leaves the hook as an architectural tidy-up — separating "compile kernels" from "stage weights" into named phases — and it would touch `common/runner/model_runner.py` plus both `npu_runner.py`, which is exactly where the six open PRs are. Adding surface to the most contested files in the repo to formalise a phase ordering that already holds is a poor trade this week. Worth doing after they land, with the timing rationale restated then rather than assumed.
files the rest of it would touch. `RuntimeModel.layers` is now defaulted and empty for every loader in the tree, and `LayerWeights` is documented as deprecated with a pointer to what replaced it. Both are kept rather than deleted on purpose: hw-native-sys#168, hw-native-sys#114, hw-native-sys#152, hw-native-sys#145, hw-native-sys#144 and hw-native-sys#132 are open, and some construct a `RuntimeModel`. Removing the field would break them for no gain that cannot wait — it goes when they have landed. `--num-layers-override` in the Qwen example loses its `runtime_model.layers[:n]` slice, which had quietly become a no-op: staging reads `config.num_hidden_layers` and pulls exactly that many layers from the checkpoint, so replacing the config *is* the override now. The knob still works; there is simply nothing eager left to truncate. **The `stage_weights` hook is deliberately not added, and the reason is worth recording because the issue asks for it.** hw-native-sys#163 proposes it "between `_create_runner` and `init_kv_cache`", motivated by Qwen's `DistributedWorker` forking inside `init_kv_cache` — so staging must happen before that. Checking the actual order in `PyptoExecutor.register_model`, it already does: `_compile_model` runs first, and Qwen stages inside it, well ahead of `_create_runner` and the fork. The constraint the hook exists to satisfy is met without it. That leaves the hook as an architectural tidy-up — separating "compile kernels" from "stage weights" into named phases — and it would touch `common/runner/model_runner.py` plus both `npu_runner.py`, which is exactly where the six open PRs are. Adding surface to the most contested files in the repo to formalise a phase ordering that already holds is a poor trade this week. Worth doing after they land, with the timing rationale restated then rather than assumed.
files the rest of it would touch. `RuntimeModel.layers` is now defaulted and empty for every loader in the tree, and `LayerWeights` is documented as deprecated with a pointer to what replaced it. Both are kept rather than deleted on purpose: hw-native-sys#168, hw-native-sys#114, hw-native-sys#152, hw-native-sys#145, hw-native-sys#144 and hw-native-sys#132 are open, and some construct a `RuntimeModel`. Removing the field would break them for no gain that cannot wait — it goes when they have landed. `--num-layers-override` in the Qwen example loses its `runtime_model.layers[:n]` slice, which had quietly become a no-op: staging reads `config.num_hidden_layers` and pulls exactly that many layers from the checkpoint, so replacing the config *is* the override now. The knob still works; there is simply nothing eager left to truncate. **The `stage_weights` hook is deliberately not added, and the reason is worth recording because the issue asks for it.** hw-native-sys#163 proposes it "between `_create_runner` and `init_kv_cache`", motivated by Qwen's `DistributedWorker` forking inside `init_kv_cache` — so staging must happen before that. Checking the actual order in `PyptoExecutor.register_model`, it already does: `_compile_model` runs first, and Qwen stages inside it, well ahead of `_create_runner` and the fork. The constraint the hook exists to satisfy is met without it. That leaves the hook as an architectural tidy-up — separating "compile kernels" from "stage weights" into named phases — and it would touch `common/runner/model_runner.py` plus both `npu_runner.py`, which is exactly where the six open PRs are. Adding surface to the most contested files in the repo to formalise a phase ordering that already holds is a poor trade this week. Worth doing after they land, with the timing rationale restated then rather than assumed.
Summary - expand DeepSeek V4 prefill from one request per DP rank to four partition-local slots, while retaining compiler-compatible B1S128 kernel calls - allow scheduler steps to mix decode and prefill work, with decode dispatched first for latency - split decode work beyond one fixed launch into partition-safe micro-batches and merge their outputs in one worker step - preserve request-local MTP committed-tail state across micro-batches by paging it through the fixed B4 device pool - remove the serving-wide single-dispatch request cap and document the resulting fixed-shape concurrency model ## Scope and ownership This PR changes the
pypto-servingscheduler, worker dispatch, DeepSeek V4 runner, configuration, tests, and developer documentation. It does not changepypto-libor the compiled kernel ABI. The fixed launch specializations remain: - prefill: up to four B1S128 calls per DP rank in one worker batch - MTP decode: B4S2 per rank, K=1, up to 32 requests per launch across 8 DP ranks - non-MTP decode: B8S1 per rank, up to 64 requests per launch across 8 DP ranks Larger active sets are served by additional worker dispatches. This increases functional serving concurrency, not per-launch kernel throughput, and extra launches plus MTP tail paging add latency. ## Out of scope - MTP K greater than 1 - dynamic-shape or unified prefill/decode kernels - changing the B4S2/B8S1 kernel specializations -pypto-libkernel or ABI changes - hardware performance claims or benchmark targets ## ValidationCurrent validated revisions:
pypto-serving@ee5f21f0d9efb63db844c115f4a9712c289c0169pypto-lib@f0d352ea2a40cefca8aa26873261d6e19e0eefecpypto@1c66f01c180e1fc3a858817dfb8f52d6ecefff88CPU and static checks:
78 passed3 passedpython -m py_compileandgit diff --check: passed8-NPU DeepSeek V4 Flash W8A8 functional validation on devices 8-15:
task_20260809_021107_78789517123, exit 0max_num_seqs=16a leading global information and communications technology (ICT)/data/chenshenai/test1/pypto-serving/.validation/full-mtp-20260809_021107Shutdown emitted one PyPTO runner close-budget error for a child process, followed by normal application shutdown. The task still exited 0, no serving/chip process remained, and all device locks were released. This validation is a functional concurrency result, not a throughput benchmark or performance claim.