Skip to content

perf(deepseek): build decode block tables without a temporary per row - #206

Open
lterrac wants to merge 1 commit into
hw-native-sys:mainfrom
lterrac:perf/deepseek-decode-block-tables
Open

lterrac wants to merge 1 commit into
hw-native-sys:mainfrom
lterrac:perf/deepseek-decode-block-tables

Conversation

@lterrac

@lterrac lterrac commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

What this changes

Two functions that build DeepSeek V4 decode block tables, so they stop allocating a temporary per row. No behaviour change: same values, same dtypes, same validation, same error messages.

Why

Profiling a steady-state decode (8 cards, --dp 8 --ep 8, W8A8, 32-token completions) says the host sets the pace, not the device:

span ms per step
WorkerProcess.execute_step 133
decode.prepare_early (host) 99–105
decode.l3_dispatch (device) 68

Inside prepare_early, building the block tables is 94 ms — seven KV groups × eight ranks per step — against ~7 ms for padding, block_counts and copy_shared combined. Split seven ways:

construction ms/rank max_blocks
hca_state_ring 3.43 2048
csa_state_ring 2.49 4096
csa_inner_ring 1.89 4096
ori_paged 1.63 (delegates to ring)
idx / hca_cmp / csa_cmp 0.59–0.96 128

The ring tables were built as torch.tensor(ids).repeat(ceil(max_blocks / len(ids)))[:max_blocks], so each row allocated a 2048–4098 element tensor and threw away all but the prefix. The tiling pattern depends only on (max_blocks, len(ids)), so it is cached and gathered directly into the destination row. paged_ori_block_table_from_ids delegates to the same function and improves with it.

block_table_from_ids allocated one tensor per row for rows holding a handful of ids; now one per call. Worth ~2 ms.

Result

Same tree, same warm compile cache, same prompt, only these two functions changed:

before after
decode step 130–137 ms 106 ms
host prepare_early 99–105 ms 75 ms
device l3_dispatch 68.2 ms 67.6 ms
4 × 64-token requests 8.24–8.45 s 6.93–7.03 s

~19% off the decode step. The device number not moving is the point: the saving is host work removed, not a measurement that shifted.

Verification

  • Both new constructions compared against the old ones over 20 max_blocks × row-length combinations, before anything was timed.
  • Generated text byte-identical to the unpatched tree on the same node, same prompt (" provides telecommunications equipment and services.\",\n \"headquarters\": \"Shenzhen,").
  • 4 requests × 64 tokens, 256 tokens total, no_fault_in=4; 507901, 507018, TENSOR_WAIT_TIMEOUT and running-stalled all zero.

Numbers are this node's — absolute host times are machine-dependent — so treat the ratio as the result.

Still on the table

prepare_early is 75 ms against 68 ms of device, so the host still just about sets the pace. What is left is spread across the seven constructions rather than concentrated, and three of the seven groups (the ring compress-state ones) change every few steps while four follow the 128-token block, so memoising by group has a ~32 ms ceiling. I tried it and measured 2.7 ms, so there is something wrong with that approach rather than with the idea — not included here.

Profiling a steady-state DeepSeek V4 decode (8 cards, dp8/ep8, W8A8) put 99-105 ms of a
~133 ms step in host-side `decode.prepare_early` against 68 ms of device work, so the host
was setting the pace. Inside it, 94 ms was building 56 block tables per step -- seven KV
groups times eight ranks -- while padding, `block_counts` and `copy_shared` together came to
about 7 ms.

`ring_block_table_from_ids` owned 62 ms of that. Each row is a short cycle of physical block
ids tiled across `max_blocks`, which is 2048 for hca_state and 4096 for the two csa_state
tables, and it built that with

    torch.tensor(ids).repeat(ceil(max_blocks / len(ids)))[:max_blocks]

so every row allocated a 2048-4098 element temporary and discarded all but the prefix. The
tiling pattern depends only on (max_blocks, len(ids)), so it is cached once and gathered
straight into the destination row.

`block_table_from_ids` allocated one tensor per row for rows holding a handful of ids; it now
builds one for the call. Smaller: about 2 ms.

Measured on the same tree, same warm compile cache, same prompt, with only these two
functions changed:

| | before | after |
|---|---|---|
| decode step | 130-137 ms | 106 ms |
| host `prepare_early` | 99-105 ms | 75 ms |
| device `l3_dispatch` | 68.2 ms | 67.6 ms |
| 4 requests x 64 tokens | 8.24-8.45 s each | 6.93-7.03 s each |

About 19% off the decode step, with the device side unchanged as expected. Generated text is
byte-identical to the unpatched tree on the same node and the fault counters stay at zero;
both new constructions were checked against the old ones on 20 shape/length combinations
before any of this was timed.

These are this node's numbers -- absolute host times depend on the machine -- but the ratio
is the result, and the device time staying put is what says the saving is real host work
rather than a shifted measurement.

Note for anyone profiling this path next: the host prepare and the device dispatch overlap
under async scheduling, so the step is not their sum; and the compile cache has no
fingerprinting since hw-native-sys#197, so it must be cleared by hand after a pypto or pypto-lib bump or
it will silently reload stale binaries.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ba3b8513-1130-4a06-bc34-fbb077ab1314


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

1 participant