glm53_dflash2_kv_groups.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. #!/usr/bin/env python3
  2. """Let the GLM-5-Next KV grouper carry a DFlash2 drafter's attention layers.
  3. With `--speculative-config {"method":"dflash",...}` the drafter adds five
  4. SlidingWindowSpec layers (Qwen3, 2 kv heads at TP=4, window 2048) to the
  5. target's spec set. `_get_kv_cache_groups_glm5_next` returns None when any
  6. non-mamba, non-tail spec is not exactly MLAAttentionSpec, so the model falls
  7. off its own layout onto the generic path. The page unifier there takes the
  8. largest page (the MLA latent, 512 B/token) and needs every smaller page to
  9. divide it. The kpool indexer page is 33 B/token (132 B per 4-token pool), and
  10. 512/33 is not an integer for any block size. MLA pages cannot be padded, so
  11. `unify_kv_cache_spec_page_size` raises
  12. Layer language_model.model.layers.3.self_attn.indexer.k_cache: page size
  13. is not divisible by the maximum page size and cannot be padded.
  14. The full-allocation fallback fails too (draft and target block sizes differ),
  15. and the engine dies after a full weight load. No flag reaches the indexer (it
  16. is not an Attention layer).
  17. The fix: carve the drafter's specs out before the MLA check and append them as
  18. their own group(s) after every existing group, so the target's groups, ids,
  19. tensors and slot sharing are unchanged. `_glm5_next_tensor_layout` learns to
  20. recognise those groups, and the GLM branches of the config builder, the
  21. bytes-per-block helper and the memory estimate add one unshared region per
  22. draft layer after the indexer regions. This is the pre-merge image's fork,
  23. reduced to what the nightly lacks.
  24. The draft group is a `DraftSlidingWindowSpec` whose `prefix_cacheable` is
  25. False, the nightly's own opt-out (the kpool tail uses it). The engine core's
  26. block-size minimum, the hash-block gcd, the coordinator's hit lookup and the
  27. base manager's `cache_blocks` all honour it, so no custom manager and no
  28. registry entry are needed (the registry walks the MRO to SlidingWindowSpec).
  29. The opt-out keeps the target's prefix caching exactly what it is without a
  30. drafter, whatever block the draft group uses. Its cost: after a prefix-cache
  31. hit the drafter never writes K/V for the cached tokens, so drafts attend to
  32. stale blocks until the 2048-token window rolls past them. Output is unaffected
  33. (drafts are verified). Acceptance dips for that request.
  34. Draft block size: the target's (`--block-size`, 2304) by default.
  35. VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE shrinks it to a multiple of 64 that divides the
  36. target block. On the old image 576 gave 1.5x the KV pool and 43% slower
  37. decode. Every non-MLA backend the drafter can land on runs any multiple of 64
  38. (FlashAttention/Triton/Flex: MultipleOf(16). FlashInfer: 16/32/64), and the
  39. runner splits the storage block into kernel blocks itself.
  40. Every anchor must match exactly once, or the build fails.
  41. """
  42. from pathlib import Path
  43. PATH = Path("/usr/local/lib/python3.12/dist-packages/vllm/v1/core/kv_cache_utils.py")
  44. MARK = "GLM53-DFLASH2-KV"
  45. EDITS: list[tuple[str, str, str]] = []
  46. def edit(label: str, old: str, new: str) -> None:
  47. EDITS.append((label, old, new))
  48. # 1. Carve the drafter's plain attention specs out before the MLA check.
  49. edit(
  50. "grouper carve-out",
  51. """ attn_specs = {
  52. name: spec
  53. for name, spec in kv_cache_spec.items()
  54. if not isinstance(spec, (MambaSpec, KpoolTailSpec))
  55. }
  56. if not mamba_specs or not all(
  57. type(spec) is MLAAttentionSpec for spec in attn_specs.values()
  58. ):
  59. return None
  60. mla_specs = cast(dict[str, MLAAttentionSpec], attn_specs)
  61. """,
  62. """ attn_specs = {
  63. name: spec
  64. for name, spec in kv_cache_spec.items()
  65. if not isinstance(spec, (MambaSpec, KpoolTailSpec))
  66. }
  67. # GLM53-DFLASH2-KV: a speculative drafter (DFlash2, EAGLE3) adds ordinary
  68. # attention layers to the target's MLA(+indexer)+mamba set. Left in, they
  69. # fail the MLA check below and the model falls onto the generic unifier,
  70. # where the indexer page (33 B/token) never divides the MLA page
  71. # (512 B/token) and cannot be padded. Carve them out into their own
  72. # group(s), appended last. The target's layout is untouched.
  73. draft_specs = {
  74. name: spec
  75. for name, spec in attn_specs.items()
  76. if type(spec) in (SlidingWindowSpec, FullAttentionSpec)
  77. }
  78. attn_specs = {
  79. name: spec for name, spec in attn_specs.items() if name not in draft_specs
  80. }
  81. if (
  82. not mamba_specs
  83. or not attn_specs
  84. or not all(type(spec) is MLAAttentionSpec for spec in attn_specs.values())
  85. ):
  86. return None
  87. mla_specs = cast(dict[str, MLAAttentionSpec], attn_specs)
  88. """,
  89. )
  90. # 2. Draft groups go last so every existing group id is unchanged.
  91. edit(
  92. "grouper return",
  93. """ return (
  94. [KVCacheGroupSpec(list(attn_specs), uniform_spec)]
  95. + ([tail_group] if tail_group is not None else [])
  96. + create_kv_cache_group_specs(padded_specs, mamba_grouped_names)
  97. )
  98. """,
  99. """ return (
  100. [KVCacheGroupSpec(list(attn_specs), uniform_spec)]
  101. + ([tail_group] if tail_group is not None else [])
  102. + create_kv_cache_group_specs(padded_specs, mamba_grouped_names)
  103. # GLM53-DFLASH2-KV: draft groups last, so existing group ids hold.
  104. + _glm5_next_draft_groups(draft_specs, uniform_spec.block_size)
  105. )
  106. """,
  107. )
  108. # 3. The draft spec and its helpers, inserted before the layout recogniser.
  109. HELPERS = '''@dataclass(frozen=True, kw_only=True)
  110. class DraftSlidingWindowSpec(SlidingWindowSpec):
  111. """GLM53-DFLASH2-KV: a drafter's sliding-window layer on the GLM-5-Next
  112. layout. Identical to SlidingWindowSpec but opted out of prefix caching,
  113. the same opt-out the kpool tail uses: the engine core's block-size
  114. minimum, the hash-block gcd, the coordinator's hit lookup and the base
  115. manager's cache_blocks all skip it, so the target's caching behaviour is
  116. exactly what it is without a drafter, whatever block size this group has.
  117. The registry resolves it to SlidingWindowManager through the MRO.
  118. """
  119. @property
  120. def prefix_cacheable(self) -> bool:
  121. return False
  122. def _glm5_next_draft_block_size(target_block_size: int) -> int:
  123. """GLM53-DFLASH2-KV: block size for the drafter's sliding-window group.
  124. The target block by default. VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE shrinks the
  125. draft tensors. It must be a multiple of 64, which every non-MLA backend
  126. runs (MultipleOf(16) or 16/32/64), and divide the target block, which
  127. keeps the scheduler block (lcm over groups) at the target's.
  128. """
  129. requested = os.environ.get("VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE")
  130. if requested is None:
  131. return target_block_size
  132. block = int(requested)
  133. if block <= 0 or block % 64 != 0 or target_block_size % block != 0:
  134. raise ValueError(
  135. f"VLLM_GLM5NEXT_DRAFT_BLOCK_SIZE={block} must be a positive multiple "
  136. f"of 64 that divides the target block size {target_block_size}"
  137. )
  138. return block
  139. def _glm5_next_draft_groups(
  140. draft_specs: dict[str, KVCacheSpec], target_block_size: int
  141. ) -> list[KVCacheGroupSpec]:
  142. """GLM53-DFLASH2-KV: KV cache groups for drafter attention layers riding
  143. on the GLM-5-Next layout. Each draft layer gets its own unshared region
  144. (see get_kv_cache_config_from_groups). Sliding-window layers become
  145. DraftSlidingWindowSpec. A full-attention drafter layer (EAGLE3 without a
  146. window) keeps participating at the target's block size.
  147. """
  148. if not draft_specs:
  149. return []
  150. scaled: dict[str, KVCacheSpec] = {}
  151. for name, spec in draft_specs.items():
  152. assert isinstance(spec, AttentionSpec)
  153. if spec.page_size_padded is not None:
  154. raise NotImplementedError(
  155. f"Layer {name}: a padded draft page (kv_cache_dtype_skip_layers) "
  156. "is not supported on the GLM-5-Next layout. Drop that flag."
  157. )
  158. if isinstance(spec, SlidingWindowSpec):
  159. scaled[name] = replace_as(
  160. spec,
  161. DraftSlidingWindowSpec,
  162. block_size=_glm5_next_draft_block_size(target_block_size),
  163. )
  164. continue
  165. if target_block_size % spec.block_size != 0:
  166. raise NotImplementedError(
  167. f"Layer {name}: draft block size {spec.block_size} does not "
  168. f"divide the target block size {target_block_size}. Pick a "
  169. "--block-size that is a multiple of the draft backend's kernel "
  170. "block."
  171. )
  172. scaled[name] = replace(spec, block_size=target_block_size)
  173. same_spec_layers: dict[KVCacheSpec, list[str]] = defaultdict(list)
  174. for name, spec in scaled.items():
  175. same_spec_layers[spec].append(name)
  176. return create_kv_cache_group_specs(scaled, list(same_spec_layers.values()))
  177. def _glm5_next_draft_bytes_per_block(draft_groups: list[KVCacheGroupSpec]) -> int:
  178. """GLM53-DFLASH2-KV: bytes one pool block costs across all draft layers."""
  179. return sum(
  180. len(group.layer_names) * group.kv_cache_spec.page_size_bytes
  181. for group in draft_groups
  182. )
  183. '''
  184. edit(
  185. "helpers inserted",
  186. "def _glm5_next_tensor_layout(\n",
  187. HELPERS + "def _glm5_next_tensor_layout(\n",
  188. )
  189. # 4. The layout tuple gains a ninth element: the draft groups.
  190. edit(
  191. "layout signature",
  192. ''' list[str],
  193. int,
  194. ]
  195. | None
  196. ):
  197. """Recognize the GLM-5.3-Flash grouping after optional PP projection."""
  198. ''',
  199. ''' list[str],
  200. int,
  201. list[KVCacheGroupSpec],
  202. ]
  203. | None
  204. ):
  205. """Recognize the GLM-5.3-Flash grouping after optional PP projection.
  206. GLM53-DFLASH2-KV: the ninth element is the drafter's groups, empty when
  207. there is no drafter.
  208. """
  209. ''',
  210. )
  211. edit(
  212. "layout draft classification",
  213. """ mamba_groups = [
  214. group for group in kv_cache_groups if isinstance(group.kv_cache_spec, MambaSpec)
  215. ]
  216. attn_group: KVCacheGroupSpec | None = None
  217. """,
  218. """ mamba_groups = [
  219. group for group in kv_cache_groups if isinstance(group.kv_cache_spec, MambaSpec)
  220. ]
  221. # GLM53-DFLASH2-KV: draft groups carry a plain AttentionSpec. The target's
  222. # attention and tail groups are UniformTypeKVCacheSpecs, so none of them
  223. # is misclassified. A PP stage without the drafter sees the group with an
  224. # empty layer list, and it still classifies.
  225. draft_groups = [
  226. group
  227. for group in kv_cache_groups
  228. if isinstance(group.kv_cache_spec, AttentionSpec)
  229. ]
  230. attn_group: KVCacheGroupSpec | None = None
  231. """,
  232. )
  233. edit(
  234. "layout group count",
  235. """ if len(uniform_groups) + len(mamba_groups) != len(kv_cache_groups):
  236. return None
  237. """,
  238. """ if len(uniform_groups) + len(mamba_groups) + len(draft_groups) != len(
  239. kv_cache_groups
  240. ):
  241. return None
  242. """,
  243. )
  244. edit(
  245. "layout return",
  246. """ tail_names,
  247. tail_page,
  248. )
  249. def unify_kv_cache_spec_page_size(
  250. """,
  251. """ tail_names,
  252. tail_page,
  253. draft_groups,
  254. )
  255. def unify_kv_cache_spec_page_size(
  256. """,
  257. )
  258. # 5. Bytes per pool block (also feeds the null-block reserve and the re-plan).
  259. edit(
  260. "bytes per block",
  261. """ _, _, mla_names, idx_names, mla_page, idx_page, _, _ = glm5_layout
  262. return len(mla_names) * mla_page + len(idx_names) * idx_page
  263. """,
  264. """ _, _, mla_names, idx_names, mla_page, idx_page, _, _, draft_groups = (
  265. glm5_layout
  266. )
  267. return (
  268. len(mla_names) * mla_page
  269. + len(idx_names) * idx_page
  270. + _glm5_next_draft_bytes_per_block(draft_groups)
  271. )
  272. """,
  273. )
  274. # 6. The config builder: count the draft pages and emit their regions.
  275. edit(
  276. "config builder unpack",
  277. """ tail_names,
  278. _,
  279. ) = glm5_layout
  280. bytes_per_block = len(mla_names) * mla_page + len(idx_names) * idx_page
  281. num_blocks = may_override_num_blocks(
  282. """,
  283. """ tail_names,
  284. _,
  285. draft_groups,
  286. ) = glm5_layout
  287. bytes_per_block = (
  288. len(mla_names) * mla_page
  289. + len(idx_names) * idx_page
  290. # GLM53-DFLASH2-KV: every pool block also carries the draft pages.
  291. + _glm5_next_draft_bytes_per_block(draft_groups)
  292. )
  293. num_blocks = may_override_num_blocks(
  294. """,
  295. )
  296. edit(
  297. "config builder draft regions",
  298. """ add_tensor(tail_name, tail_specs[tail_name], offset)
  299. return KVCacheConfig(
  300. """,
  301. """ add_tensor(tail_name, tail_specs[tail_name], offset)
  302. # GLM53-DFLASH2-KV: one unshared region per draft layer, after the
  303. # indexer regions. Nothing aliases these.
  304. draft_offset = idx_base + len(idx_names) * idx_page * num_blocks
  305. for group in draft_groups:
  306. for layer_name in group.layer_names:
  307. add_tensor(layer_name, group.kv_cache_spec, draft_offset)
  308. draft_offset += group.kv_cache_spec.page_size_bytes * num_blocks
  309. return KVCacheConfig(
  310. """,
  311. )
  312. # 7. The memory estimate: draft groups draw window-bounded blocks from the
  313. # same pool, and every pool block carries the draft pages.
  314. edit(
  315. "memory estimate unpack",
  316. """ tail_names,
  317. _,
  318. ) = glm5_layout
  319. uniform_spec = cast(UniformTypeKVCacheSpecs, attn_group.kv_cache_spec)
  320. total_blocks = uniform_spec.max_memory_usage_pages(vllm_config)
  321. """,
  322. """ tail_names,
  323. _,
  324. draft_groups,
  325. ) = glm5_layout
  326. uniform_spec = cast(UniformTypeKVCacheSpecs, attn_group.kv_cache_spec)
  327. total_blocks = uniform_spec.max_memory_usage_pages(vllm_config)
  328. """,
  329. )
  330. edit(
  331. "memory estimate total",
  332. """ if tail_names:
  333. total_blocks += 1
  334. return total_blocks * (len(mla_names) * mla_page + len(idx_names) * idx_page)
  335. """,
  336. """ if tail_names:
  337. total_blocks += 1
  338. # GLM53-DFLASH2-KV: draft groups draw their window-bounded blocks from
  339. # the same pool, and every pool block also carries the draft pages.
  340. for group in draft_groups:
  341. if group.layer_names:
  342. total_blocks += cdiv(
  343. group.kv_cache_spec.max_memory_usage_bytes(vllm_config),
  344. group.kv_cache_spec.page_size_bytes,
  345. )
  346. return total_blocks * (
  347. len(mla_names) * mla_page
  348. + len(idx_names) * idx_page
  349. + _glm5_next_draft_bytes_per_block(draft_groups)
  350. )
  351. """,
  352. )
  353. def main() -> None:
  354. text = PATH.read_text()
  355. if MARK in text:
  356. print("[glm53-dflash2-kv] already applied")
  357. return
  358. for label, old, _ in EDITS:
  359. n = text.count(old)
  360. assert n == 1, (
  361. f"[glm53-dflash2-kv] anchor '{label}' matched {n} times. "
  362. "The stock tree changed."
  363. )
  364. for _, old, new in EDITS:
  365. text = text.replace(old, new)
  366. PATH.write_text(text)
  367. print(
  368. f"[glm53-dflash2-kv] {len(EDITS)} edits: GLM-5-Next grouper carries the "
  369. "DFlash2 drafter's attention layers as their own trailing group"
  370. )
  371. if __name__ == "__main__":
  372. main()