Repository navigation
Reduce Qwen3.5 GDN device patch - #356
Conversation
|
[P1] MTP short prefill corrupts the convolution ring state While reviewing this PR ( The write indices are calculated from each sequence's end in the flattened batch and only clamped to global index 0. For a sequence/chunk shorter than the convolution width, the gathered values can come from the previous request, or repeat the first current token instead of retaining the cached history. With width 4 and a 1–2 token prefill, this changes subsequent decode outputs. It also affects an ordinary long prompt whose final prefill chunk is short. Minimal CPU reproduction Run the following from a dlinfer checkout at import ast
from pathlib import Path
from typing import Any, Optional
import torch
import torch.nn.functional as F
# Run from a dlinfer checkout at 40cfc6c. Load the two unchanged CPU
# functions directly, bypassing torch_npu/Triton package initialization.
path = Path('dlinfer/vendor/ascend/triton_ops/causal_conv1d.py')
tree = ast.parse(path.read_text())
tree.body = [n for n in tree.body if isinstance(n, ast.FunctionDef)
and n.name in ('causal_conv1d_ref', 'causal_conv1d_fn')]
ns = dict(torch=torch, F=F, Any=Any, Optional=Optional, PAD_SLOT_ID=-1)
exec(compile(tree, str(path), 'exec'), ns)
prefill, reference = ns['causal_conv1d_fn'], ns['causal_conv1d_ref']
weight = torch.ones(1, 4) # one channel, convolution width 4
def run(cache, tokens, lengths, history, ids):
q = torch.tensor(lengths, dtype=torch.int32)
h = torch.tensor(history, dtype=torch.int32)
prefill(
torch.tensor([tokens], dtype=torch.float32), weight,
conv_states=cache, has_initial_state=h > 0,
cache_indices=torch.tensor(ids, dtype=torch.int32),
query_start_loc=F.pad(q.cumsum(0, dtype=torch.int32), (1, 0)),
read_conv_offsets=(h[:, None] + torch.arange(-3, 0)) % 6,
write_conv_offsets=((h + q)[:, None] + torch.arange(-4, 0)) % 6,
)
def next_output(cache, slot, history):
offsets = (history + torch.arange(-3, 0)) % 6
initial = cache[slot].index_select(-1, offsets)
out, _ = reference(torch.tensor([[2.]]), weight, initial_states=initial)
return out.item()
# Fresh requests: [10, 20, 30, 40] and [1]. Ring length = 4 + 2.
cache = torch.zeros(3, 1, 6)
run(cache, [10, 20, 30, 40, 1], [4, 1], [0, 0], [1, 2])
expected = torch.zeros_like(cache)
expected[2, 0, 0] = 1
print('fresh cache:', cache[2, 0].tolist())
print('expected: ', expected[2, 0].tolist())
print('next output:', next_output(cache, 2, 1), 'expected:', next_output(expected, 2, 1))
# A one-token continuation after history [10, 20, 30, 40].
cache = torch.zeros(2, 1, 6)
cache[1, 0, :4] = torch.tensor([10., 20., 30., 40.])
expected = cache.clone()
expected[1, 0, 4] = 1
run(cache, [1], [1], [4], [1])
print('tail cache: ', cache[1, 0].tolist())
print('expected: ', expected[1, 0].tolist())
print('next output:', next_output(cache, 1, 5), 'expected:', next_output(expected, 1, 5))Observed output: In the first case, request 2's history contains Suggested fix For each valid sequence, gather its existing Please add regression tests for fresh and continuing sequences with query lengths Validation here is CPU execution of the original prefill/reference functions. The same failure was also observed through LMDeploy's new metadata/conv adapter with the vendor decode kernel under Triton's CPU interpreter; this is not an end-to-end Ascend NPU test. |
The vectorised ring write in causal_conv1d_fn gathered each sequence's trailing width tokens from the flattened batch x, with indices only clamped to global 0. When a sequence or chunk is shorter than the conv width (chunked-prefill tail, MTP short prefill), the gather reaches across request boundaries -- writing another request's prompt tokens into this request's conv state -- or repeats the first token via clamp_min(0), destroying the valid history already seated in the ring. Subsequent decode steps then silently read corrupted state; outputs diverge with no error raised. Fix: gather the cached history via read_conv_offsets before any scatter (zero padding for fresh requests), concatenate the sequence's own new tokens, and scatter the trailing width values to write_conv_offsets. The write now also skips PAD_SLOT_ID and zero-length rows, matching the compute loop. Verified on CPU against causal_conv1d_ref (16 regression cases: fresh and continuing chunks q=1..8, ring wraparound, mixed-length batches, chunked-vs-one-shot, next-token outputs), and end-to-end on A3 with Qwen3.5-9B + MTP under greedy decoding: with identical batch shapes, buggy and fixed builds diverge in both a mixed-batch short-prefill scenario (token 13) and a 1-token chunked-prefill tail scenario (token 100 of 256). Co-Authored-By: Claude Code <noreply@anthropic.com>
Confirmed — excellent catch, and the analysis was spot-on. I reproduced your CPU repro exactly (both the cross-request case and the one-token continuation), and traced the root cause to the same lines: the Fixed in ce25158. The write now follows exactly the semantics you suggested:
While fixing this I also found the old block scattered rows the compute loop skips ( Regression tests ( End-to-end on A3 (Qwen3.5-9B, MTP
Your validation caveat (CPU / Triton-interpreter only) was fair — the NPU run above closes that gap. Thanks again for the detailed report; the boundary-crossing gather would have been very hard to spot from |
No description provided.