| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420 |
- #!/usr/bin/env python3
- """Let the GLM-5-Next KV grouper carry a DFlash2 drafter's attention layers.
- With `--speculative-config {"method":"dflash",...}` the drafter adds five
- SlidingWindowSpec layers (Qwen3, 2 kv heads at TP=4, window 2048) to the
- target's spec set. `_get_kv_cache_groups_glm5_next` returns None when any
- non-mamba, non-tail spec is not exactly MLAAttentionSpec, so the model falls
- off its own layout onto the generic path. The page unifier there takes the
- largest page (the MLA latent, 512 B/token) and needs every smaller page to
- divide it. The kpool indexer page is 33 B/token (132 B per 4-token pool), and
- 512/33 is not an integer for any block size. MLA pages cannot be padded, so
- `unify_kv_cache_spec_page_size` raises
- Layer language_model.model.layers.3.self_attn.indexer.k_cache: page size
- is not divisible by the maximum page size and cannot be padded.
- The full-allocation fallback fails too (draft and target block sizes differ),
- and the engine dies after a full weight load. No flag reaches the indexer (it
- is not an Attention layer).
- The fix: carve the drafter's specs out before the MLA check and append them as
- their own group(s) after every existing group, so the target's groups, ids,
- tensors and slot sharing are unchanged. `_glm5_next_tensor_layout` learns to
- recognise those groups, and the GLM branches of the config builder, the
- bytes-per-block helper and the memory estimate add one unshared region per
- draft layer after the indexer regions. This is the pre-merge image's fork,
- reduced to what the nightly lacks.
- The draft group is a `DraftSlidingWindowSpec` whose `prefix_cacheable` is
- False, the nightly's own opt-out (the kpool tail uses it). The engine core's
- block-size minimum, the hash-block gcd, the coordinator's hit lookup and the
- base manager's `cache_blocks` all honour it, so no custom manager and no
- registry entry are needed (the registry walks the MRO to SlidingWindowSpec).
- The opt-out keeps the target's prefix caching exactly what it is without a
- drafter, whatever block the draft group uses. Its cost: after a prefix-cache
- hit the drafter never writes K/V for the cached tokens, so drafts attend to
- stale blocks until the 2048-token window rolls past them. Output is unaffected
- (drafts are verified). Acceptance dips for that request.
- Draft block size: the target's (`--block-size`, 2304) by default.
- VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE shrinks it to a multiple of 64 that divides the
- target block. On the old image 576 gave 1.5x the KV pool and 43% slower
- decode. Every non-MLA backend the drafter can land on runs any multiple of 64
- (FlashAttention/Triton/Flex: MultipleOf(16). FlashInfer: 16/32/64), and the
- runner splits the storage block into kernel blocks itself.
- Every anchor must match exactly once, or the build fails.
- """
- from pathlib import Path
- PATH = Path("/usr/local/lib/python3.12/dist-packages/vllm/v1/core/kv_cache_utils.py")
- MARK = "GLM53-DFLASH2-KV"
- EDITS: list[tuple[str, str, str]] = []
- def edit(label: str, old: str, new: str) -> None:
- EDITS.append((label, old, new))
- # 1. Carve the drafter's plain attention specs out before the MLA check.
- edit(
- "grouper carve-out",
- """ attn_specs = {
- name: spec
- for name, spec in kv_cache_spec.items()
- if not isinstance(spec, (MambaSpec, KpoolTailSpec))
- }
- if not mamba_specs or not all(
- type(spec) is MLAAttentionSpec for spec in attn_specs.values()
- ):
- return None
- mla_specs = cast(dict[str, MLAAttentionSpec], attn_specs)
- """,
- """ attn_specs = {
- name: spec
- for name, spec in kv_cache_spec.items()
- if not isinstance(spec, (MambaSpec, KpoolTailSpec))
- }
- # GLM53-DFLASH2-KV: a speculative drafter (DFlash2, EAGLE3) adds ordinary
- # attention layers to the target's MLA(+indexer)+mamba set. Left in, they
- # fail the MLA check below and the model falls onto the generic unifier,
- # where the indexer page (33 B/token) never divides the MLA page
- # (512 B/token) and cannot be padded. Carve them out into their own
- # group(s), appended last. The target's layout is untouched.
- draft_specs = {
- name: spec
- for name, spec in attn_specs.items()
- if type(spec) in (SlidingWindowSpec, FullAttentionSpec)
- }
- attn_specs = {
- name: spec for name, spec in attn_specs.items() if name not in draft_specs
- }
- if (
- not mamba_specs
- or not attn_specs
- or not all(type(spec) is MLAAttentionSpec for spec in attn_specs.values())
- ):
- return None
- mla_specs = cast(dict[str, MLAAttentionSpec], attn_specs)
- """,
- )
- # 2. Draft groups go last so every existing group id is unchanged.
- edit(
- "grouper return",
- """ return (
- [KVCacheGroupSpec(list(attn_specs), uniform_spec)]
- + ([tail_group] if tail_group is not None else [])
- + create_kv_cache_group_specs(padded_specs, mamba_grouped_names)
- )
- """,
- """ return (
- [KVCacheGroupSpec(list(attn_specs), uniform_spec)]
- + ([tail_group] if tail_group is not None else [])
- + create_kv_cache_group_specs(padded_specs, mamba_grouped_names)
- # GLM53-DFLASH2-KV: draft groups last, so existing group ids hold.
- + _glm5_next_draft_groups(draft_specs, uniform_spec.block_size)
- )
- """,
- )
- # 3. The draft spec and its helpers, inserted before the layout recogniser.
- HELPERS = '''@dataclass(frozen=True, kw_only=True)
- class DraftSlidingWindowSpec(SlidingWindowSpec):
- """GLM53-DFLASH2-KV: a drafter's sliding-window layer on the GLM-5-Next
- layout. Identical to SlidingWindowSpec but opted out of prefix caching,
- the same opt-out the kpool tail uses: the engine core's block-size
- minimum, the hash-block gcd, the coordinator's hit lookup and the base
- manager's cache_blocks all skip it, so the target's caching behaviour is
- exactly what it is without a drafter, whatever block size this group has.
- The registry resolves it to SlidingWindowManager through the MRO.
- """
- @property
- def prefix_cacheable(self) -> bool:
- return False
- def _glm5_next_draft_block_size(target_block_size: int) -> int:
- """GLM53-DFLASH2-KV: block size for the drafter's sliding-window group.
- The target block by default. VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE shrinks the
- draft tensors. It must be a multiple of 64, which every non-MLA backend
- runs (MultipleOf(16) or 16/32/64), and divide the target block, which
- keeps the scheduler block (lcm over groups) at the target's.
- """
- requested = os.environ.get("VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE")
- if requested is None:
- return target_block_size
- block = int(requested)
- if block <= 0 or block % 64 != 0 or target_block_size % block != 0:
- raise ValueError(
- f"VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE={block} must be a positive multiple "
- f"of 64 that divides the target block size {target_block_size}"
- )
- return block
- def _glm5_next_draft_groups(
- draft_specs: dict[str, KVCacheSpec], target_block_size: int
- ) -> list[KVCacheGroupSpec]:
- """GLM53-DFLASH2-KV: KV cache groups for drafter attention layers riding
- on the GLM-5-Next layout. Each draft layer gets its own unshared region
- (see get_kv_cache_config_from_groups). Sliding-window layers become
- DraftSlidingWindowSpec. A full-attention drafter layer (EAGLE3 without a
- window) keeps participating at the target's block size.
- """
- if not draft_specs:
- return []
- scaled: dict[str, KVCacheSpec] = {}
- for name, spec in draft_specs.items():
- assert isinstance(spec, AttentionSpec)
- if spec.page_size_padded is not None:
- raise NotImplementedError(
- f"Layer {name}: a padded draft page (kv_cache_dtype_skip_layers) "
- "is not supported on the GLM-5-Next layout. Drop that flag."
- )
- if isinstance(spec, SlidingWindowSpec):
- scaled[name] = replace_as(
- spec,
- DraftSlidingWindowSpec,
- block_size=_glm5_next_draft_block_size(target_block_size),
- )
- continue
- if target_block_size % spec.block_size != 0:
- raise NotImplementedError(
- f"Layer {name}: draft block size {spec.block_size} does not "
- f"divide the target block size {target_block_size}. Pick a "
- "--block-size that is a multiple of the draft backend's kernel "
- "block."
- )
- scaled[name] = replace(spec, block_size=target_block_size)
- same_spec_layers: dict[KVCacheSpec, list[str]] = defaultdict(list)
- for name, spec in scaled.items():
- same_spec_layers[spec].append(name)
- return create_kv_cache_group_specs(scaled, list(same_spec_layers.values()))
- def _glm5_next_draft_bytes_per_block(draft_groups: list[KVCacheGroupSpec]) -> int:
- """GLM53-DFLASH2-KV: bytes one pool block costs across all draft layers."""
- return sum(
- len(group.layer_names) * group.kv_cache_spec.page_size_bytes
- for group in draft_groups
- )
- '''
- edit(
- "helpers inserted",
- "def _glm5_next_tensor_layout(\n",
- HELPERS + "def _glm5_next_tensor_layout(\n",
- )
- # 4. The layout tuple gains a ninth element: the draft groups.
- edit(
- "layout signature",
- ''' list[str],
- int,
- ]
- | None
- ):
- """Recognize the GLM-5.3-Flash grouping after optional PP projection."""
- ''',
- ''' list[str],
- int,
- list[KVCacheGroupSpec],
- ]
- | None
- ):
- """Recognize the GLM-5.3-Flash grouping after optional PP projection.
- GLM53-DFLASH2-KV: the ninth element is the drafter's groups, empty when
- there is no drafter.
- """
- ''',
- )
- edit(
- "layout draft classification",
- """ mamba_groups = [
- group for group in kv_cache_groups if isinstance(group.kv_cache_spec, MambaSpec)
- ]
- attn_group: KVCacheGroupSpec | None = None
- """,
- """ mamba_groups = [
- group for group in kv_cache_groups if isinstance(group.kv_cache_spec, MambaSpec)
- ]
- # GLM53-DFLASH2-KV: draft groups carry a plain AttentionSpec. The target's
- # attention and tail groups are UniformTypeKVCacheSpecs, so none of them
- # is misclassified. A PP stage without the drafter sees the group with an
- # empty layer list, and it still classifies.
- draft_groups = [
- group
- for group in kv_cache_groups
- if isinstance(group.kv_cache_spec, AttentionSpec)
- ]
- attn_group: KVCacheGroupSpec | None = None
- """,
- )
- edit(
- "layout group count",
- """ if len(uniform_groups) + len(mamba_groups) != len(kv_cache_groups):
- return None
- """,
- """ if len(uniform_groups) + len(mamba_groups) + len(draft_groups) != len(
- kv_cache_groups
- ):
- return None
- """,
- )
- edit(
- "layout return",
- """ tail_names,
- tail_page,
- )
- def unify_kv_cache_spec_page_size(
- """,
- """ tail_names,
- tail_page,
- draft_groups,
- )
- def unify_kv_cache_spec_page_size(
- """,
- )
- # 5. Bytes per pool block (also feeds the null-block reserve and the re-plan).
- edit(
- "bytes per block",
- """ _, _, mla_names, idx_names, mla_page, idx_page, _, _ = glm5_layout
- return len(mla_names) * mla_page + len(idx_names) * idx_page
- """,
- """ _, _, mla_names, idx_names, mla_page, idx_page, _, _, draft_groups = (
- glm5_layout
- )
- return (
- len(mla_names) * mla_page
- + len(idx_names) * idx_page
- + _glm5_next_draft_bytes_per_block(draft_groups)
- )
- """,
- )
- # 6. The config builder: count the draft pages and emit their regions.
- edit(
- "config builder unpack",
- """ tail_names,
- _,
- ) = glm5_layout
- bytes_per_block = len(mla_names) * mla_page + len(idx_names) * idx_page
- num_blocks = may_override_num_blocks(
- """,
- """ tail_names,
- _,
- draft_groups,
- ) = glm5_layout
- bytes_per_block = (
- len(mla_names) * mla_page
- + len(idx_names) * idx_page
- # GLM53-DFLASH2-KV: every pool block also carries the draft pages.
- + _glm5_next_draft_bytes_per_block(draft_groups)
- )
- num_blocks = may_override_num_blocks(
- """,
- )
- edit(
- "config builder draft regions",
- """ add_tensor(tail_name, tail_specs[tail_name], offset)
- return KVCacheConfig(
- """,
- """ add_tensor(tail_name, tail_specs[tail_name], offset)
- # GLM53-DFLASH2-KV: one unshared region per draft layer, after the
- # indexer regions. Nothing aliases these.
- draft_offset = idx_base + len(idx_names) * idx_page * num_blocks
- for group in draft_groups:
- for layer_name in group.layer_names:
- add_tensor(layer_name, group.kv_cache_spec, draft_offset)
- draft_offset += group.kv_cache_spec.page_size_bytes * num_blocks
- return KVCacheConfig(
- """,
- )
- # 7. The memory estimate: draft groups draw window-bounded blocks from the
- # same pool, and every pool block carries the draft pages.
- edit(
- "memory estimate unpack",
- """ tail_names,
- _,
- ) = glm5_layout
- uniform_spec = cast(UniformTypeKVCacheSpecs, attn_group.kv_cache_spec)
- total_blocks = uniform_spec.max_memory_usage_pages(vllm_config)
- """,
- """ tail_names,
- _,
- draft_groups,
- ) = glm5_layout
- uniform_spec = cast(UniformTypeKVCacheSpecs, attn_group.kv_cache_spec)
- total_blocks = uniform_spec.max_memory_usage_pages(vllm_config)
- """,
- )
- edit(
- "memory estimate total",
- """ if tail_names:
- total_blocks += 1
- return total_blocks * (len(mla_names) * mla_page + len(idx_names) * idx_page)
- """,
- """ if tail_names:
- total_blocks += 1
- # GLM53-DFLASH2-KV: draft groups draw their window-bounded blocks from
- # the same pool, and every pool block also carries the draft pages.
- for group in draft_groups:
- if group.layer_names:
- total_blocks += cdiv(
- group.kv_cache_spec.max_memory_usage_bytes(vllm_config),
- group.kv_cache_spec.page_size_bytes,
- )
- return total_blocks * (
- len(mla_names) * mla_page
- + len(idx_names) * idx_page
- + _glm5_next_draft_bytes_per_block(draft_groups)
- )
- """,
- )
- def main() -> None:
- text = PATH.read_text()
- if MARK in text:
- print("[glm53-dflash2-kv] already applied")
- return
- for label, old, _ in EDITS:
- n = text.count(old)
- assert n == 1, (
- f"[glm53-dflash2-kv] anchor '{label}' matched {n} times. "
- "The stock tree changed."
- )
- for _, old, new in EDITS:
- text = text.replace(old, new)
- PATH.write_text(text)
- print(
- f"[glm53-dflash2-kv] {len(EDITS)} edits: GLM-5-Next grouper carries the "
- "DFlash2 drafter's attention layers as their own trailing group"
- )
- if __name__ == "__main__":
- main()
|