Repository navigation
feat(spec): capture the Qwen3.8 MTP draft steps in CUDA graphs (MRoPE) #2663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
53f99a9
06b96b8
ddd0b5e
d7d1f82
4823b47
816a0a8
c0dfd87
4b28fdd
80bd51d
528b871
2daa454
aea839b
9cd4ea5
19cc0e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| passes still live with the flavor; only the machine is here. | ||
| """ | ||
|
|
||
| import math | ||
| from collections.abc import Callable, Mapping | ||
| from dataclasses import dataclass, field | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
@@ -38,10 +39,16 @@ def _owned(value: Any, staged: tuple[torch.Tensor, ...] = ()) -> Any: | |
|
|
||
| @dataclass(frozen=True) | ||
| class StagedInput: | ||
| """One staged input's type: everything but the leading batch axis.""" | ||
| """One staged input's type: everything but the batch axis. | ||
|
|
||
| The batch axis leads unless ``batch_last`` (MRoPE positions, ``[3, N]``): | ||
| each batch then views a flat buffer's front as a contiguous ``[*shape, bs]``, | ||
| the layout the runner gives its own MRoPE positions. | ||
| """ | ||
|
|
||
| shape: tuple[int, ...] = () | ||
| dtype: torch.dtype = torch.int32 | ||
| batch_last: bool = False | ||
|
|
||
|
|
||
| @dataclass | ||
|
|
@@ -113,14 +120,21 @@ def bind(self, config: "Config", device) -> "DraftGraph": | |
| ) | ||
| self._buffers = { | ||
| role: torch.zeros( | ||
| (config.max_num_seqs, *staged.shape), | ||
| ( | ||
| (config.max_num_seqs * math.prod(staged.shape),) | ||
| if staged.batch_last | ||
| else (config.max_num_seqs, *staged.shape) | ||
| ), | ||
| dtype=staged.dtype, | ||
| device=device, | ||
| ) | ||
| for role, staged in self.inputs.items() | ||
| } | ||
| return self | ||
|
|
||
| def _batch_of(self, role: str, src: torch.Tensor) -> int: | ||
| return src.shape[-1] if self.inputs[role].batch_last else src.shape[0] | ||
|
|
||
| @property | ||
| def will_capture(self) -> bool: | ||
| """Capture only a supported pass when the deployment enables it.""" | ||
|
|
@@ -147,7 +161,12 @@ def stage( | |
| How many rows are real comes from the sources, never from the caller: a | ||
| count that cannot disagree with the tensor it describes. | ||
| """ | ||
| counts = {t.shape[0] for t in srcs.values()} | ||
| undeclared = sorted(srcs.keys() - self._buffers.keys()) | ||
| assert not undeclared, ( | ||
| f"{self.name} has no staged inputs {undeclared}; " | ||
| f"declared roles are {sorted(self._buffers)}" | ||
| ) | ||
| counts = {self._batch_of(role, t) for role, t in srcs.items()} | ||
| assert len(counts) <= 1, ( | ||
| f"{self.name} was handed batches of {sorted(counts)}; they " | ||
| f"describe one step, so one of them is not this step's" | ||
|
|
@@ -167,6 +186,10 @@ def buffer(self, role: str, bs: int) -> torch.Tensor: | |
| f"{self.name} has no staged input {role!r}; " | ||
| f"declared roles are {sorted(self._buffers)}" | ||
| ) | ||
| assert not self.inputs[role].batch_last, ( | ||
| f"{self.name} stages {role!r} batch-last, whose view at one batch is " | ||
| f"not the front of a wider one; hand it to stage instead" | ||
| ) | ||
| buf = self._buffers[role] | ||
| assert 0 <= bs <= buf.shape[0], ( | ||
| f"{self.name} requested {bs} rows from draft input {role!r}, " | ||
|
|
@@ -175,7 +198,8 @@ def buffer(self, role: str, bs: int) -> torch.Tensor: | |
| return buf[:bs] | ||
|
|
||
| def _stage_one(self, role: str, running_bs: int, src: torch.Tensor | None): | ||
| """Copy ``src`` into this input's fixed buffer, tail-repeating its last row. | ||
| """Copy ``src`` into this input's fixed buffer, tail-repeating its last row | ||
| (its last column if ``batch_last``). | ||
|
|
||
| Every input repeats the same last index, so a pad row is a coherent copy | ||
| of the last real one and only redoes work that row already does. Zeros | ||
|
|
@@ -186,21 +210,25 @@ def _stage_one(self, role: str, running_bs: int, src: torch.Tensor | None): | |
| batch, not a particular one. | ||
| """ | ||
| buf = self._buffers[role] | ||
| out = buf[:running_bs] | ||
| shape, batch_last = self.inputs[role].shape, self.inputs[role].batch_last | ||
| out = ( | ||
| buf[: math.prod(shape) * running_bs].view(*shape, running_bs) | ||
| if batch_last | ||
| else buf[:running_bs] | ||
| ) | ||
| if src is None: | ||
| return out | ||
| assert src.dtype == buf.dtype, ( | ||
| f"draft input '{role}' of {self.name} arrived as {src.dtype}, but " | ||
| f"its storage is {buf.dtype}; a capture has that address baked" | ||
| ) | ||
| assert src.ndim == buf.ndim, ( | ||
| assert src.ndim == out.ndim, ( | ||
| f"draft input '{role}' of {self.name} arrived as {tuple(src.shape)}, " | ||
| f"whose leading axis is not the batch; this pass stages " | ||
| f"{tuple(buf.shape[1:])} per sequence. MRoPE positions ([3, N]) are " | ||
| f"the case that reaches here" | ||
| f"whose {'last' if batch_last else 'leading'} axis is not the batch; " | ||
| f"this pass stages {shape} per sequence" | ||
| ) | ||
| scheduled_bs = src.shape[0] | ||
| dst = out[:scheduled_bs] | ||
| scheduled_bs = self._batch_of(role, src) | ||
| dst = out[..., :scheduled_bs] if batch_last else out[:scheduled_bs] | ||
| assert src.shape == dst.shape, ( | ||
| f"draft input '{role}' of {self.name} arrived as {tuple(src.shape)}, " | ||
| f"but its fixed storage expects {tuple(dst.shape)}" | ||
|
|
@@ -210,7 +238,10 @@ def _stage_one(self, role: str, running_bs: int, src: torch.Tensor | None): | |
| if src.data_ptr() != dst.data_ptr() or src.stride() != dst.stride(): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. batch_last + producer fill gives an overlapping self-copy with wrong strides.. assert it or document it?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @valar-cop I followed your suggestion on You're right that it's real. On CPU I filled
(commit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lgtm. the guard now refuses before checked the callers: only There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. stride differs when running_bs != scheduled_bs, so this guard takes the copy branch over overlapping storage.. latent today but hidden_states would corrupt silently. assert no alias or copy via clone?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @valar-cop I followed your suggestion on One correction on scope: Every source that lives inside a staged buffer today is the exact view:
So the assert doesn't fire on any current path, and every copy that ran before still runs. A future alias fails loudly instead of corrupting. (commit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified against the code: lgtm, my mistake on hidden_states. |
||
| dst.copy_(src) | ||
| if running_bs > scheduled_bs: | ||
| out[scheduled_bs:].copy_(src[-1]) # broadcasts; no expand needed | ||
| if batch_last: | ||
| out[..., scheduled_bs:].copy_(src[..., -1:]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. docstring still says row, now it's the last real sequence for batch_last.. please reword
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @valar-cop I followed your suggestion on The (commit There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, that reads right now, pad repeats the last real sequence in both layouts and lgtm from me. note the vllm accuracy matrix is still skipped on this head, it needs the approval or a |
||
| else: | ||
| out[scheduled_bs:].copy_(src[-1]) # broadcasts; no expand needed | ||
| return out | ||
|
|
||
| def is_captured(self, running_bs: int) -> bool: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
undeclared role now raises bare KeyError instead of the old assert message? let's keep the diagnostic..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 2daa454.
stage()andbuffer()now check the roles before anything else, through one_assert_declared. It names each undeclared role and lists the declared ones (... has no staged input 'missing'; declared roles are ['row']) instead of raising a bareKeyError.test_staging_an_undeclared_role_names_itcovers it; on528b871beit fails withKeyError: 'missing'.