engram.py 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411
  1. # SPDX-License-Identifier: Apache-2.0
  2. # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
  3. """Engram: n-gram hash lookups gated into the hyper-connection stream.
  4. Port of the reference ``inference/engram.py`` + ``Engram`` /
  5. ``ParallelEngramEmbedding`` from ``inference/model.py`` (DeepSeek V4.1
  6. checkpoint layout). Engram modules live on the backbone layers listed in
  7. ``engram_layer_ids`` only.
  8. Two pieces of cross-forward state are needed because vLLM streams tokens
  9. chunk-by-chunk while an n-gram at position ``p`` needs the token ids at
  10. ``p-1..p-3``:
  11. - ``token_map``: token id -> compressed vocab id, built once from the
  12. model's tokenizer at init (deterministic; asserted against
  13. ``engram_compressed_vocab_size``).
  14. - ``hash_cache``: one int32 slot per KV slot of the first local layer's
  15. sliding-window cache, holding the compressed id (or DEAD) of the token
  16. last written to that slot. Slots are stable per (request, position) —
  17. the block table pins a position to a physical slot, prefix-cache hits
  18. reuse both the physical blocks and the identical token ids, and
  19. spec-decode rollbacks rewrite the same slots — so lookbacks read back
  20. exactly what the owning request wrote. Lookback depth (3) is far inside
  21. the sliding window (128), so window eviction never frees a block a
  22. live lookback still needs.
  23. Slots are not part of the KV cache, so KV loaded from another instance
  24. (P/D, offload connectors) leaves them unwritten. The runner therefore
  25. passes ``lookback_token_ids``, the ids just before each request's chunk
  26. start, which take precedence over the slots. The V2 runner reads them
  27. from its device-resident token history and needs no slot cache; the V1
  28. runner's CPU token table holds placeholders for generated tokens under
  29. async scheduling, so it passes prompt positions only and keeps the slot
  30. cache for the rest.
  31. """
  32. import weakref
  33. import numpy as np
  34. import torch
  35. from torch import nn
  36. # --- Tech2Wild/Kai 2026-09-10: disk-backed Engram tables for DGX Spark (UMA) ---
  37. import json as _kai_json
  38. import os as _kai_os
  39. import struct as _kai_struct
  40. from concurrent.futures import ThreadPoolExecutor as _KaiPool
  41. from vllm.config import VllmConfig, get_current_vllm_config
  42. from vllm.distributed import (
  43. get_tensor_model_parallel_rank,
  44. get_tensor_model_parallel_world_size,
  45. tensor_model_parallel_all_gather,
  46. )
  47. from vllm.logger import init_logger
  48. from vllm.model_executor.layers.linear import ReplicatedLinear
  49. from vllm.model_executor.layers.quantization import QuantizationConfig
  50. from vllm.model_executor.utils import set_weight_attrs
  51. from vllm.triton_utils import tl, triton
  52. from vllm.utils.platform_utils import is_uva_available
  53. from vllm.utils.torch_utils import get_accelerator_view_from_cpu_tensor
  54. logger = init_logger(__name__)
  55. # Cache value for tokens that take no part in an n-gram (image spans).
  56. DEAD_ID = -1
  57. def _is_prime(n: int) -> bool:
  58. """Deterministic Miller-Rabin for n < 2**32 (avoids a sympy import)."""
  59. if n < 2:
  60. return False
  61. for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
  62. if n % p == 0:
  63. return n == p
  64. d = n - 1
  65. r = 0
  66. while d % 2 == 0:
  67. d //= 2
  68. r += 1
  69. for a in (2, 7, 61):
  70. x = pow(a, d, n)
  71. if x in (1, n - 1):
  72. continue
  73. for _ in range(r - 1):
  74. x = x * x % n
  75. if x == n - 1:
  76. break
  77. else:
  78. return False
  79. return True
  80. def find_next_prime(start: int, seen_primes: set[int]) -> int:
  81. """The smallest prime above `start` that has not been handed out yet."""
  82. candidate = start + 1
  83. while not _is_prime(candidate) or candidate in seen_primes:
  84. candidate += 1
  85. return candidate
  86. def build_compressed_token_map(tokenizer) -> tuple[list[int], int]:
  87. """Map every token id onto a smaller id space where tokens that normalize
  88. alike collapse together.
  89. N-grams are hashed over these compressed ids, so " The", "the" and "THE"
  90. all hash the same way. The compressed size matters beyond bounds checking:
  91. every hash multiplier is derived from it.
  92. """
  93. from tokenizers import Regex, normalizers
  94. # A private-use char, so a token that is exactly one space survives
  95. # Strip() instead of collapsing to the empty string and merging with
  96. # unrelated tokens.
  97. sentinel = "\ue000"
  98. normalizer = normalizers.Sequence(
  99. [
  100. normalizers.NFKC(),
  101. normalizers.NFD(),
  102. normalizers.StripAccents(),
  103. normalizers.Lowercase(),
  104. normalizers.Replace(Regex(r"[ \t\r\n]+"), " "),
  105. normalizers.Replace(Regex(r"^ $"), sentinel),
  106. normalizers.Strip(),
  107. normalizers.Replace(sentinel, " "),
  108. ]
  109. )
  110. # The raw Rust tokenizer, matching what training decodes with
  111. # (no clean_up_tokenization_spaces).
  112. backend = tokenizer.backend_tokenizer
  113. key_to_new: dict[str, int] = {}
  114. lookup = [0] * len(tokenizer)
  115. for token_id in range(len(tokenizer)):
  116. text = backend.decode([token_id], skip_special_tokens=False)
  117. if "\ufffd" in text:
  118. # A partial UTF-8 byte token: nothing to normalize, so key it
  119. # by its raw form.
  120. key = backend.id_to_token(token_id)
  121. else:
  122. normalized = normalizer.normalize_str(text)
  123. key = normalized if normalized else text
  124. new_id = key_to_new.get(key)
  125. if new_id is None:
  126. new_id = len(key_to_new)
  127. key_to_new[key] = new_id
  128. lookup[token_id] = new_id
  129. return lookup, len(key_to_new)
  130. def compute_hash_multipliers(
  131. layer_ids: tuple[int, ...], max_ngram_size: int, compressed_vocab_size: int
  132. ) -> torch.Tensor:
  133. """One multiplier per (layer, lookback), from a per-layer RNG so layers
  134. hash differently. Kept odd and bounded so `token_id * multiplier` cannot
  135. overflow int64.
  136. """
  137. max_long = np.iinfo(np.int64).max
  138. multiplier_bound = max(1, (max_long // compressed_vocab_size) // 2)
  139. rows = []
  140. for layer_id in layer_ids:
  141. generator = np.random.default_rng(10007 * layer_id)
  142. values = generator.integers(
  143. low=0,
  144. high=multiplier_bound,
  145. size=(max_ngram_size,),
  146. dtype=np.int64,
  147. )
  148. rows.append(torch.tensor(values * 2 + 1))
  149. return torch.stack(rows)
  150. class EngramLayout:
  151. """Bucket layout of the n-gram hash tables.
  152. A position is hashed as `max_ngram_size - 1` n-grams (2-gram .. max), each
  153. split over `n_heads` heads. Every (n-gram size, head) pair owns its own
  154. prime-sized bucket range in the layer's table; the primes are drawn in
  155. order and never reused, which keeps the ranges disjoint.
  156. """
  157. def __init__(self, config) -> None:
  158. self.layer_ids: tuple[int, ...] = tuple(config.engram_layer_ids)
  159. self.num_embeddings: tuple[int, ...] = tuple(config.engram_num_embeddings)
  160. self.max_ngram_size: int = config.engram_max_ngram_size
  161. self.n_heads: int = config.engram_n_heads
  162. self.head_dim: int = config.engram_head_dim
  163. self.compressed_vocab_size: int = config.engram_compressed_vocab_size
  164. self.pad_token_id: int = config.engram_pad_token_id
  165. assert len(self.layer_ids) == len(self.num_embeddings)
  166. primes = []
  167. seen: set[int] = set()
  168. for _ in self.layer_ids:
  169. per_ngram = []
  170. for _ in range(self.max_ngram_size - 1):
  171. sizes, current = [], config.engram_vocab_size - 1
  172. for _ in range(self.n_heads):
  173. current = find_next_prime(current, seen)
  174. seen.add(current)
  175. sizes.append(current)
  176. per_ngram.append(tuple(sizes))
  177. primes.append(tuple(per_ngram))
  178. self.primes: tuple[tuple[tuple[int, ...], ...], ...] = tuple(primes)
  179. self.n_hash_cols = (self.max_ngram_size - 1) * self.n_heads
  180. flat = [[p for per_ngram in layer for p in per_ngram] for layer in primes]
  181. offsets = [np.cumsum([0, *sizes[:-1]]) for sizes in flat]
  182. self.offsets = torch.tensor(np.array(offsets)) # [n_layers, n_hash_cols]
  183. @classmethod
  184. def from_config(cls, config) -> "EngramLayout | None":
  185. if not getattr(config, "engram_layer_ids", None):
  186. return None
  187. return cls(config)
  188. @triton.jit(do_not_specialize=["num_tokens"])
  189. def _write_hash_cache_kernel(
  190. input_ids,
  191. token_map,
  192. dead_mask,
  193. slot_mapping,
  194. cache,
  195. num_tokens,
  196. input_stride,
  197. mask_stride,
  198. slot_stride,
  199. BLOCK_SIZE: tl.constexpr,
  200. dead_id,
  201. ):
  202. token_idx = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  203. slot = tl.load(
  204. slot_mapping + token_idx * slot_stride, token_idx < num_tokens, other=-1
  205. ).to(tl.int64)
  206. valid = (token_idx < num_tokens) & (slot >= 0)
  207. token = tl.load(input_ids + token_idx * input_stride, valid, other=0)
  208. value = tl.load(token_map + token, valid, other=0)
  209. dead = tl.load(dead_mask + token_idx * mask_stride, valid, other=False)
  210. value = tl.where(dead, dead_id, value)
  211. tl.store(cache + slot, value, valid)
  212. @triton.jit(
  213. do_not_specialize=[
  214. "num_tokens",
  215. "num_slots",
  216. "num_query_rows",
  217. "num_table_rows",
  218. "max_blocks",
  219. ]
  220. )
  221. def _hash_ids_kernel(
  222. input_ids,
  223. token_map,
  224. dead_mask,
  225. positions,
  226. block_table,
  227. query_start_loc,
  228. multipliers,
  229. primes,
  230. offsets,
  231. cache,
  232. lookback_token_ids,
  233. lookback_dead_mask,
  234. output,
  235. num_tokens,
  236. num_slots,
  237. pad_id,
  238. input_stride,
  239. mask_stride,
  240. position_stride,
  241. table_stride,
  242. table_col_stride,
  243. query_stride,
  244. num_query_rows,
  245. num_table_rows,
  246. max_blocks,
  247. cache_block_size,
  248. MAX_NGRAM: tl.constexpr,
  249. num_heads,
  250. BLOCK_T: tl.constexpr,
  251. BLOCK_H: tl.constexpr,
  252. dead_id,
  253. lookback_depth,
  254. lookback_row_stride,
  255. lookback_col_stride,
  256. lookback_mask_row_stride,
  257. lookback_mask_col_stride,
  258. ):
  259. token = tl.program_id(0) * BLOCK_T + tl.arange(0, BLOCK_T)
  260. layer = tl.program_id(1)
  261. num_layers = tl.num_programs(1)
  262. valid = token < num_tokens
  263. # Upper bound in query_start_loc[1:], including repeated padding boundaries.
  264. lo = tl.full((BLOCK_T,), 0, tl.int32)
  265. hi = tl.full((BLOCK_T,), num_query_rows, tl.int32)
  266. while tl.sum((lo < hi).to(tl.int32), 0) > 0:
  267. mid = (lo + hi) // 2
  268. end = tl.load(
  269. query_start_loc + (mid + 1) * query_stride,
  270. lo < hi,
  271. other=0,
  272. )
  273. right = token >= end
  274. active = lo < hi
  275. lo = tl.where(active & right, mid + 1, lo)
  276. hi = tl.where(active & ~right, mid, hi)
  277. req = tl.minimum(lo, num_query_rows - 1).to(tl.int64)
  278. chunk_idx = tl.load(query_start_loc + req * query_stride)
  279. chunk_idx = tl.minimum(chunk_idx, num_tokens - 1).to(tl.int64)
  280. chunk_start = tl.load(positions + chunk_idx * position_stride)
  281. position = tl.load(positions + token * position_stride, valid, other=0).to(tl.int64)
  282. head = tl.arange(0, BLOCK_H)
  283. blocked = tl.full((BLOCK_T,), False, tl.int1)
  284. rolling = tl.full((BLOCK_T,), 0, tl.int64)
  285. for shift in tl.static_range(MAX_NGRAM):
  286. lookback = position - shift
  287. in_batch = lookback >= chunk_start
  288. batch_idx = tl.maximum(token - shift, 0)
  289. batch_token = tl.load(
  290. input_ids + batch_idx * input_stride, valid & in_batch, other=0
  291. )
  292. batch_source = tl.load(token_map + batch_token, valid & in_batch, other=0)
  293. batch_dead = tl.load(
  294. dead_mask + batch_idx * mask_stride, valid & in_batch, other=False
  295. )
  296. batch_source = tl.where(batch_dead, dead_id, batch_source)
  297. col = chunk_start - 1 - lookback
  298. in_window = valid & ~in_batch & (col >= 0) & (col < lookback_depth)
  299. col = tl.minimum(tl.maximum(col, 0), lookback_depth - 1)
  300. window_token = tl.load(
  301. lookback_token_ids + req * lookback_row_stride + col * lookback_col_stride,
  302. in_window,
  303. other=-1,
  304. )
  305. known = in_window & (window_token >= 0)
  306. window_source = tl.load(token_map + window_token, known, other=0)
  307. window_dead = tl.load(
  308. lookback_dead_mask
  309. + req * lookback_mask_row_stride
  310. + col * lookback_mask_col_stride,
  311. known,
  312. other=False,
  313. )
  314. window_source = tl.where(window_dead, dead_id, window_source)
  315. if cache is not None:
  316. clamped = tl.minimum(
  317. tl.maximum(lookback, 0), max_blocks * cache_block_size - 1
  318. )
  319. block_row = tl.minimum(req, num_table_rows - 1)
  320. needs_cache = valid & ~in_batch & ~known
  321. block = tl.load(
  322. block_table
  323. + block_row * table_stride
  324. + (clamped // cache_block_size) * table_col_stride,
  325. needs_cache,
  326. other=0,
  327. ).to(tl.int64)
  328. slot = tl.minimum(
  329. tl.maximum(block * cache_block_size + clamped % cache_block_size, 0),
  330. num_slots - 1,
  331. )
  332. fallback = tl.load(cache + slot, needs_cache, other=0)
  333. else:
  334. fallback = tl.full((BLOCK_T,), pad_id, tl.int32)
  335. source = tl.where(
  336. in_batch, batch_source, tl.where(known, window_source, fallback)
  337. ).to(tl.int64)
  338. blocked |= (lookback < 0) | (source == dead_id)
  339. value = tl.where(blocked, pad_id, source)
  340. multiplier = tl.load(multipliers + layer * MAX_NGRAM + shift)
  341. rolling ^= value * multiplier
  342. if shift > 0:
  343. col = (shift - 1) * num_heads + head
  344. param_offset = layer * (MAX_NGRAM - 1) * num_heads + col
  345. prime = tl.load(primes + param_offset, head < num_heads, other=1)
  346. offset = tl.load(offsets + param_offset, head < num_heads, other=0)
  347. hashed = rolling[:, None] % prime[None, :] + offset[None, :]
  348. out_offset = (token.to(tl.int64) * num_layers + layer)[:, None] * (
  349. (MAX_NGRAM - 1) * num_heads
  350. ) + col[None, :]
  351. tl.store(output + out_offset, hashed, valid[:, None] & (head < num_heads))
  352. class NgramHashState(nn.Module):
  353. """Maps each position to the hash ids of the n-grams ending there.
  354. Stateless on the V2 runner, which supplies every lookback token id. On
  355. the V1 runner it also keeps `hash_cache`, the slot-keyed rolling store
  356. of compressed ids (see module docstring), for generated tokens.
  357. """
  358. def __init__(
  359. self,
  360. vllm_config: VllmConfig,
  361. layout: EngramLayout,
  362. swa_cache_module: nn.Module,
  363. ) -> None:
  364. super().__init__()
  365. self.layout = layout
  366. self.swa_cache_module = swa_cache_module
  367. self.block_size: int = swa_cache_module.block_size
  368. self.lookback_depth: int = layout.max_ngram_size - 1
  369. self.use_slot_cache: bool = not vllm_config.use_v2_model_runner
  370. self._cache: torch.Tensor | None = None
  371. self._kv_cache_ref: weakref.ReferenceType[torch.Tensor] | None = None
  372. model_config = vllm_config.model_config
  373. from transformers import AutoTokenizer
  374. tokenizer = AutoTokenizer.from_pretrained(
  375. model_config.tokenizer,
  376. trust_remote_code=model_config.trust_remote_code,
  377. revision=model_config.revision,
  378. )
  379. token_map, vocab_size = build_compressed_token_map(tokenizer)
  380. if vocab_size != layout.compressed_vocab_size:
  381. raise ValueError(
  382. f"Compressed vocab size mismatch: built {vocab_size} from the "
  383. f"tokenizer, config expects {layout.compressed_vocab_size}; "
  384. "every hash multiplier derives from it, so the engram tables "
  385. "would be silently rehashed."
  386. )
  387. self.pad_id = token_map[layout.pad_token_id]
  388. multipliers = compute_hash_multipliers(
  389. layout.layer_ids, layout.max_ngram_size, vocab_size
  390. )
  391. self.register_buffer(
  392. "token_map", torch.tensor(token_map, dtype=torch.int32), persistent=False
  393. )
  394. self.register_buffer("primes", torch.tensor(layout.primes), persistent=False)
  395. self.register_buffer("offsets", layout.offsets, persistent=False)
  396. self.register_buffer("multipliers", multipliers, persistent=False)
  397. logger.info(
  398. "Built engram token map (%d -> %d ids) for layers %s",
  399. len(token_map),
  400. vocab_size,
  401. layout.layer_ids,
  402. )
  403. def ensure_cache(self) -> bool:
  404. """Lazily size the slot-keyed cache from the bound SWA KV cache.
  405. Returns False while the KV cache is unbound (profile run); the caller
  406. skips engram hashing then. Without the slot cache only that check
  407. remains.
  408. """
  409. kv_cache = self.swa_cache_module.kv_cache
  410. if kv_cache.numel() == 0:
  411. self._cache = None
  412. self._kv_cache_ref = None
  413. return False
  414. if not self.use_slot_cache:
  415. return True
  416. if self._kv_cache_ref is not None and self._kv_cache_ref() is kv_cache:
  417. return True
  418. # Graph memory profiling binds a temporary, smaller KV cache first.
  419. # Rebinding must discard its hash history without retaining KV storage.
  420. self._cache = torch.zeros(
  421. kv_cache.shape[0] * self.block_size,
  422. dtype=torch.int32,
  423. device=kv_cache.device,
  424. )
  425. self._kv_cache_ref = weakref.ref(kv_cache)
  426. return True
  427. def forward(
  428. self,
  429. input_ids: torch.Tensor,
  430. positions: torch.Tensor,
  431. query_start_loc: torch.Tensor,
  432. dead_mask: torch.Tensor,
  433. lookback_token_ids: torch.Tensor,
  434. lookback_dead_mask: torch.Tensor,
  435. slot_mapping: torch.Tensor | None,
  436. block_table: torch.Tensor | None,
  437. ) -> torch.Tensor:
  438. """Compute [tokens, layers, hash columns] int32 n-gram hashes.
  439. History comes from the current chunk, then the runner's lookback
  440. window, then the optional V1 slot cache. V2 needs only one launch.
  441. """
  442. cache = self._cache if self.use_slot_cache else None
  443. num_tokens = input_ids.shape[0]
  444. num_layers, max_ngram = self.multipliers.shape
  445. num_heads = self.primes.shape[-1]
  446. output = input_ids.new_empty(
  447. (num_tokens, num_layers, (max_ngram - 1) * num_heads), dtype=torch.int32
  448. )
  449. if num_tokens == 0:
  450. return output
  451. if self.use_slot_cache:
  452. assert cache is not None and slot_mapping is not None
  453. assert block_table is not None
  454. # Finish writes before other thread blocks read fallback history.
  455. _write_hash_cache_kernel[(triton.cdiv(num_tokens, 256),)](
  456. input_ids,
  457. self.token_map,
  458. dead_mask,
  459. slot_mapping,
  460. cache,
  461. num_tokens,
  462. input_ids.stride(0),
  463. dead_mask.stride(0),
  464. slot_mapping.stride(0),
  465. 256,
  466. DEAD_ID,
  467. )
  468. _hash_ids_kernel[(triton.cdiv(num_tokens, 32), num_layers)](
  469. input_ids,
  470. self.token_map,
  471. dead_mask,
  472. positions,
  473. block_table,
  474. query_start_loc,
  475. self.multipliers,
  476. self.primes,
  477. self.offsets,
  478. cache,
  479. lookback_token_ids,
  480. lookback_dead_mask,
  481. output,
  482. num_tokens,
  483. cache.shape[0] if cache is not None else 0,
  484. self.pad_id,
  485. input_stride=input_ids.stride(0),
  486. mask_stride=dead_mask.stride(0),
  487. position_stride=positions.stride(0),
  488. table_stride=block_table.stride(0) if block_table is not None else 0,
  489. table_col_stride=block_table.stride(1) if block_table is not None else 0,
  490. query_stride=query_start_loc.stride(0),
  491. num_query_rows=query_start_loc.numel() - 1,
  492. num_table_rows=block_table.shape[0] if block_table is not None else 0,
  493. max_blocks=block_table.shape[1] if block_table is not None else 0,
  494. cache_block_size=self.block_size,
  495. MAX_NGRAM=max_ngram,
  496. num_heads=num_heads,
  497. BLOCK_T=32,
  498. BLOCK_H=triton.next_power_of_2(num_heads),
  499. dead_id=DEAD_ID,
  500. lookback_depth=lookback_token_ids.shape[1],
  501. lookback_row_stride=lookback_token_ids.stride(0),
  502. lookback_col_stride=lookback_token_ids.stride(1),
  503. lookback_mask_row_stride=lookback_dead_mask.stride(0),
  504. lookback_mask_col_stride=lookback_dead_mask.stride(1),
  505. num_warps=4,
  506. )
  507. return output
  508. def _engram_head_shard_weight_loader(
  509. param: torch.nn.Parameter, loaded_weight: torch.Tensor
  510. ) -> None:
  511. """Load this rank's complete head buckets. ue8m0 scales arrive as
  512. float8_e8m0fnu; keep the raw bytes (the param stores uint8)."""
  513. part_rows = param.shape[0]
  514. if loaded_weight.dtype == torch.float8_e8m0fnu:
  515. loaded_weight = loaded_weight.view(torch.uint8)
  516. shard = loaded_weight.narrow(0, param.engram_vocab_start, part_rows)
  517. assert shard.shape == param.shape, (
  518. f"engram shard {tuple(shard.shape)} does not fit param {tuple(param.shape)}"
  519. )
  520. param.data.copy_(shard)
  521. @triton.jit
  522. def _engram_lookup_kernel(
  523. weight,
  524. scales,
  525. ids,
  526. out,
  527. vocab_start,
  528. vocab_end,
  529. num_rows,
  530. ids_stride_t,
  531. ids_stride_h,
  532. HEAD_START: tl.constexpr,
  533. LOCAL_HEADS: tl.constexpr,
  534. TOTAL_HEADS: tl.constexpr,
  535. DIM: tl.constexpr,
  536. QUANT_BLOCK: tl.constexpr,
  537. BLOCK_R: tl.constexpr,
  538. GRID: tl.constexpr,
  539. ):
  540. """Gather fp8 rows, apply their ue8m0 block scales, write bf16.
  541. Only this rank's heads are read; padded heads write zeros for all-gather.
  542. `weight`/`scales` may address pinned host memory through UVA.
  543. """
  544. cols = tl.arange(0, DIM)
  545. scale_cols = cols // QUANT_BLOCK
  546. for base in tl.range(tl.program_id(0) * BLOCK_R, num_rows, GRID * BLOCK_R):
  547. rows = base + tl.arange(0, BLOCK_R)
  548. valid = rows < num_rows
  549. head = HEAD_START + rows % LOCAL_HEADS
  550. token = (rows // LOCAL_HEADS).to(tl.int64)
  551. index = tl.load(
  552. ids + token * ids_stride_t + head * ids_stride_h,
  553. mask=valid & (head < TOTAL_HEADS),
  554. other=-1,
  555. ).to(tl.int64)
  556. owned = valid & (head < TOTAL_HEADS)
  557. owned &= (index >= vocab_start) & (index < vocab_end)
  558. local = tl.where(owned, index - vocab_start, 0)
  559. values = tl.load(
  560. weight + local[:, None] * DIM + cols[None, :],
  561. mask=owned[:, None],
  562. other=0.0,
  563. )
  564. scale = tl.load(
  565. scales + local[:, None] * (DIM // QUANT_BLOCK) + scale_cols[None, :],
  566. mask=owned[:, None],
  567. other=0,
  568. )
  569. # ue8m0 is a power of two, so its byte *is* the fp32 exponent field.
  570. scale = (scale.to(tl.int32) << 23).to(tl.float32, bitcast=True)
  571. tl.store(
  572. out + rows[:, None] * DIM + cols[None, :],
  573. (values.to(tl.float32) * scale).to(tl.bfloat16),
  574. mask=valid[:, None],
  575. )
  576. _DSV41_ENGRAM_DISK = _kai_os.environ.get("DSV41_ENGRAM_DISK", "0") == "1"
  577. # --- Tech2Wild/Kai 2026-09-10 (CUDA graphs): one process-wide read pool shared
  578. # by every DiskEngramTable. A step's reads for BOTH engram layers (weight and
  579. # scale rows) go out as one batch, so decode latency is ~one pread instead of
  580. # 24 serial ones (measured on Reddie: 7.7 ms serial vs 3.1 ms parallel, C1).
  581. _KAI_THREADS = int(_kai_os.environ.get("DSV41_ENGRAM_DISK_THREADS", "32"))
  582. _KAI_CHUNK = int(_kai_os.environ.get("DSV41_ENGRAM_DISK_CHUNK", "16"))
  583. _KAI_POOL: _KaiPool | None = None
  584. def _kai_pool() -> _KaiPool:
  585. global _KAI_POOL
  586. if _KAI_POOL is None:
  587. _KAI_POOL = _KaiPool(max_workers=_KAI_THREADS, thread_name_prefix="engram-disk")
  588. return _KAI_POOL
  589. def _kai_pread_rows(
  590. fd: int, base: int, rel: list, lo: int, hi: int, row_bytes: int, buf
  591. ) -> None:
  592. for i in range(lo, hi):
  593. off = base + rel[i] * row_bytes
  594. view = buf[i * row_bytes : (i + 1) * row_bytes]
  595. got = 0
  596. while got < row_bytes:
  597. n = _kai_os.preadv(fd, [view[got:]], off + got)
  598. if n <= 0:
  599. raise OSError("engram disk table: short read")
  600. got += n
  601. def _kai_parallel_read(jobs: list) -> None:
  602. """jobs: [(fd, base, rel_list, row_bytes, memoryview)]. Every row of every job
  603. is in flight at once: a task carries ceil(total / threads) rows (1 row at
  604. decode sizes), capped at DSV41_ENGRAM_DISK_CHUNK for prefill-sized batches.
  605. Only the calling thread submits, so the shared pool cannot deadlock."""
  606. total = sum(len(job[2]) for job in jobs)
  607. if total == 0:
  608. return
  609. if total == 1:
  610. for fd, base, rel, row_bytes, buf in jobs:
  611. _kai_pread_rows(fd, base, rel, 0, len(rel), row_bytes, buf)
  612. return
  613. chunk = max(1, min(_KAI_CHUNK, -(-total // _KAI_THREADS)))
  614. pool = _kai_pool()
  615. futs = [
  616. pool.submit(
  617. _kai_pread_rows, fd, base, rel, lo, min(lo + chunk, len(rel)), row_bytes, buf
  618. )
  619. for fd, base, rel, row_bytes, buf in jobs
  620. for lo in range(0, len(rel), chunk)
  621. ]
  622. for fut in futs:
  623. fut.result()
  624. def _dsv41_engram_local_dir(model_dir: str, layer_id: int, row_start: int, num_rows) -> str:
  625. """Tech2Wild 2026-09-10: node-local Engram rows. DSV41_ENGRAM_DIR points at a sparse copy
  626. of the Engram shards holding only this rank's rows at the original byte offsets, plus
  627. engram-local.json with the copied row range per layer (tools/engram_local.py). It is used
  628. only when that range covers this table's rows; otherwise the table reads model_dir as before,
  629. so a changed rank map can never read the copy's empty holes."""
  630. local = _kai_os.environ.get("DSV41_ENGRAM_DIR", "")
  631. if not local:
  632. return model_dir
  633. try:
  634. with open(_kai_os.path.join(local, "engram-local.json")) as f:
  635. lo, hi = _kai_json.load(f)["layers"][str(layer_id)]
  636. except Exception as e: # noqa: BLE001
  637. logger.warning("DSV41_ENGRAM_DIR=%s unusable for layer %d (%s); reading %s",
  638. local, layer_id, e, model_dir)
  639. return model_dir
  640. if num_rows is None or not (lo <= row_start and row_start + num_rows <= hi):
  641. logger.warning("DSV41_ENGRAM_DIR=%s holds layer %d rows [%d, %d), this rank needs "
  642. "[%d, +%s); reading %s", local, layer_id, lo, hi, row_start, num_rows, model_dir)
  643. return model_dir
  644. logger.info("Engram layer %d rows [%d, %d) read from node-local %s",
  645. layer_id, row_start, row_start + num_rows, local)
  646. return local
  647. class DiskEngramTable:
  648. """Tech2Wild/Kai 2026-09-10: read Engram rows straight from the safetensors
  649. shards with positional preads on a thread pool, so the per-rank Engram
  650. shard (~47 GiB at TP4) never occupies memory. Built for DGX Spark, where
  651. "pinned host memory" is the same 128 GB pool the GPU uses. Rows are
  652. dequantized on the CPU (fp8 e4m3 x ue8m0 block scales -> bf16) and copied
  653. into the GPU staging buffer. Enable with DSV41_ENGRAM_DISK=1.
  654. Lineage: our own PLE-on-disk patch for Qwen3.8-Flash-Next (2026-09-05).
  655. """
  656. def __init__(
  657. self,
  658. model_dir: str,
  659. layer_id: int,
  660. dim: int,
  661. block_size: int,
  662. row_start: int = 0,
  663. num_rows: int | None = None,
  664. ):
  665. model_dir = _dsv41_engram_local_dir(model_dir, layer_id, row_start, num_rows)
  666. idx_path = _kai_os.path.join(model_dir, "model.safetensors.index.json")
  667. with open(idx_path) as f:
  668. weight_map = _kai_json.load(f)["weight_map"]
  669. wname = f"layers.{layer_id}.engram.embed.weight"
  670. sname = f"layers.{layer_id}.engram.embed.scale"
  671. self.w_fd, self.w_off, self.w_shape = self._open(model_dir, weight_map[wname], wname)
  672. self.s_fd, self.s_off, self.s_shape = self._open(model_dir, weight_map[sname], sname)
  673. self.dim = dim
  674. self.sb = dim // block_size
  675. assert self.w_shape[1] == dim, (self.w_shape, dim)
  676. assert self.s_shape[1] == self.sb, (self.s_shape, self.sb)
  677. assert self.s_shape[0] == self.w_shape[0], (self.s_shape, self.w_shape)
  678. # The checkpoint holds the FULL table (every TP rank's hash heads) per layer,
  679. # while callers pass rank-local row ids (global - vocab_start), so reads must
  680. # start at this rank's first row.
  681. if num_rows is None:
  682. num_rows = self.w_shape[0] - row_start
  683. assert 0 <= row_start and row_start + num_rows <= self.w_shape[0], (
  684. row_start, num_rows, self.w_shape)
  685. self.w_off += row_start * dim
  686. self.s_off += row_start * self.sb
  687. self.row_start = row_start
  688. self.num_rows = num_rows
  689. self.threads = _KAI_THREADS
  690. self.chunk = _KAI_CHUNK
  691. self.pool = _kai_pool() # shared by all tables (was one pool per table)
  692. logger.info(
  693. "Engram DISK mode: layer %d rows [%d, %d) read from %s (off=%d) and %s (off=%d); "
  694. "%d threads, chunk %d",
  695. layer_id, row_start, row_start + num_rows, weight_map[wname], self.w_off,
  696. weight_map[sname], self.s_off, self.threads, self.chunk,
  697. )
  698. @staticmethod
  699. def _open(model_dir: str, fname: str, tname: str):
  700. path = _kai_os.path.join(model_dir, fname)
  701. fd = _kai_os.open(path, _kai_os.O_RDONLY)
  702. try:
  703. _kai_os.posix_fadvise(fd, 0, 0, _kai_os.POSIX_FADV_RANDOM)
  704. except Exception: # noqa: BLE001
  705. pass
  706. with open(path, "rb") as f:
  707. n = _kai_struct.unpack("<Q", f.read(8))[0]
  708. hdr = _kai_json.loads(f.read(n))
  709. meta = hdr[tname]
  710. start = meta["data_offsets"][0]
  711. return fd, 8 + n + start, tuple(meta["shape"])
  712. def _read_rows(self, fd: int, base: int, rel: list, row_bytes: int, buf) -> None:
  713. _kai_parallel_read([(fd, base, rel, row_bytes, buf)])
  714. def read_jobs(self, rel_l: list, w: torch.Tensor, s: torch.Tensor) -> list:
  715. """Read jobs for rank-local rows `rel_l` into uint8 w [R, dim], s [R, sb]."""
  716. return [
  717. (self.w_fd, self.w_off, rel_l, self.dim, memoryview(w.numpy()).cast("B")),
  718. (self.s_fd, self.s_off, rel_l, self.sb, memoryview(s.numpy()).cast("B")),
  719. ]
  720. def dequant(self, w: torch.Tensor, s: torch.Tensor) -> torch.Tensor:
  721. """fp8 e4m3 rows (as uint8) x ue8m0 block scales -> [R, dim] fp32."""
  722. R = w.shape[0]
  723. vals = w.view(torch.float8_e4m3fn).to(torch.float32).view(
  724. R, self.sb, self.dim // self.sb
  725. )
  726. # ue8m0 byte is the fp32 exponent field: 2^(e-127)
  727. scale = (s.to(torch.int32) << 23).view(torch.float32)
  728. return (vals * scale[:, :, None]).reshape(R, self.dim)
  729. def gather_dequant(self, rel: torch.Tensor, owned: torch.Tensor) -> torch.Tensor:
  730. """rel: [R] int64 CPU local row ids; owned: [R] bool. Returns [R, dim] bf16 CPU."""
  731. return gather_dequant_many([(self, rel, owned)])[0]
  732. def gather_dequant_many(requests: list) -> list[torch.Tensor]:
  733. """Tech2Wild/Kai 2026-09-10: requests = [(DiskEngramTable, rel [R] int64 CPU,
  734. owned [R] bool)] -> one [R, dim] bf16 CPU tensor per request. Rows are
  735. de-duplicated per table (repeated n-grams, zero-filled dummy batches) and the
  736. reads of every table go out in ONE parallel batch. Same math as before."""
  737. plans, jobs = [], []
  738. for table, rel, owned in requests:
  739. uniq, inverse = torch.unique(rel, return_inverse=True)
  740. w = torch.empty((uniq.numel(), table.dim), dtype=torch.uint8)
  741. s = torch.empty((uniq.numel(), table.sb), dtype=torch.uint8)
  742. jobs += table.read_jobs(uniq.tolist(), w, s)
  743. plans.append((table, w, s, inverse, owned))
  744. _kai_parallel_read(jobs)
  745. outs = []
  746. for table, w, s, inverse, owned in plans:
  747. out = table.dequant(w, s)[inverse]
  748. out[~owned] = 0
  749. outs.append(out.to(torch.bfloat16))
  750. return outs
  751. class ParallelEngramEmbedding(nn.Module):
  752. """The n-gram hash table, sharded by complete hash heads over TP ranks.
  753. Rows stay fp8 and are dequantized with ue8m0 per-32 scales on lookup.
  754. With `cpu_offload` the shard lives in pinned host memory and is read over
  755. UVA instead of HBM; the TP sharding is unchanged either way.
  756. """
  757. def __init__(
  758. self,
  759. num_embeddings: int,
  760. dim: int,
  761. head_sizes: tuple[int, ...],
  762. block_size: int = 32,
  763. cpu_offload: bool = False,
  764. model_dir: str | None = None,
  765. layer_id: int | None = None,
  766. ):
  767. super().__init__()
  768. self.disk: DiskEngramTable | None = None
  769. if _DSV41_ENGRAM_DISK:
  770. cpu_offload = False
  771. tp_size = get_tensor_model_parallel_world_size()
  772. tp_rank = get_tensor_model_parallel_rank()
  773. assert head_sizes and all(size > 0 for size in head_sizes)
  774. assert sum(head_sizes) <= num_embeddings
  775. if cpu_offload and not is_uva_available():
  776. raise RuntimeError("Engram CPU offload requires UVA support")
  777. self.num_embeddings = num_embeddings
  778. self.dim = dim
  779. self.block_size = block_size
  780. self.n_hash_cols = len(head_sizes)
  781. self.part_n_hash_cols = triton.cdiv(self.n_hash_cols, tp_size)
  782. self.head_start = tp_rank * self.part_n_hash_cols
  783. head_end = self.head_start + self.part_n_hash_cols
  784. self.vocab_start_idx = sum(head_sizes[: self.head_start])
  785. self.vocab_end_idx = sum(head_sizes[:head_end])
  786. self.part_num_embeddings = self.vocab_end_idx - self.vocab_start_idx
  787. self.tp_size = tp_size
  788. self.cpu_offload = cpu_offload
  789. self._views: tuple[torch.Tensor, torch.Tensor] | None = None
  790. self._view_src: tuple[int, int] | None = None
  791. self._num_sms = torch.cuda.get_device_properties(
  792. torch.accelerator.current_device_index()
  793. ).multi_processor_count
  794. # Explicit device: model init runs under a `torch.device("cuda")`
  795. # context, which would otherwise put the shard in HBM.
  796. kwargs = {"device": "cpu", "pin_memory": True} if cpu_offload else {}
  797. if _DSV41_ENGRAM_DISK:
  798. assert model_dir is not None and layer_id is not None
  799. self.disk = DiskEngramTable(
  800. model_dir, layer_id, dim, block_size,
  801. row_start=self.vocab_start_idx, num_rows=self.part_num_embeddings,
  802. )
  803. # 1-row placeholders as BUFFERS (not Parameters): the checkpoint
  804. # rows are skipped by the weights iterator and must not trip the
  805. # "weights not initialized" check.
  806. self.register_buffer("weight", torch.zeros(1, dim, dtype=torch.float8_e4m3fn, device="cpu"), persistent=False)
  807. self.register_buffer("weight_scale_inv", torch.zeros(1, dim // block_size, dtype=torch.uint8, device="cpu"), persistent=False)
  808. logger.info(
  809. "Engram table DISK-backed: %d rows x %d per rank stay on disk (%.2f GiB not allocated)",
  810. self.part_num_embeddings, dim,
  811. self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
  812. )
  813. return
  814. self.weight = nn.Parameter(
  815. torch.empty(
  816. self.part_num_embeddings, dim, dtype=torch.float8_e4m3fn, **kwargs
  817. ),
  818. requires_grad=False,
  819. )
  820. self.weight_scale_inv = nn.Parameter(
  821. torch.empty(
  822. self.part_num_embeddings,
  823. dim // block_size,
  824. dtype=torch.uint8,
  825. **kwargs,
  826. ),
  827. requires_grad=False,
  828. )
  829. for param in (self.weight, self.weight_scale_inv):
  830. set_weight_attrs(
  831. param,
  832. {
  833. "weight_loader": _engram_head_shard_weight_loader,
  834. "engram_vocab_start": self.vocab_start_idx,
  835. },
  836. )
  837. if cpu_offload:
  838. logger.info(
  839. "Engram table offloaded to pinned host memory: %d rows x %d, "
  840. "%.2f GiB per rank",
  841. self.part_num_embeddings,
  842. dim,
  843. self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
  844. )
  845. def _storage(self) -> tuple[torch.Tensor, torch.Tensor]:
  846. """Parameters when resident, else cached UVA views of the pinned shard.
  847. Rebuilt if anything swaps `.data`, so a stale device pointer cannot
  848. survive silently.
  849. """
  850. if not self.cpu_offload:
  851. return self.weight.data, self.weight_scale_inv.data
  852. src = (self.weight.data_ptr(), self.weight_scale_inv.data_ptr())
  853. if self._view_src != src:
  854. self._views = (
  855. get_accelerator_view_from_cpu_tensor(self.weight.data),
  856. get_accelerator_view_from_cpu_tensor(self.weight_scale_inv.data),
  857. )
  858. self._view_src = src
  859. assert self._views is not None
  860. return self._views
  861. def lookup(
  862. self, indices: torch.Tensor, out: torch.Tensor, background: bool = False
  863. ) -> None:
  864. """Look up local heads of [T, heads] into [T, local_heads, dim] bf16.
  865. `background` limits the grid to leave SMs for concurrent work.
  866. """
  867. rows = indices.shape[0] * self.part_n_hash_cols
  868. if not rows:
  869. return
  870. if self.disk is not None:
  871. self._disk_lookup(indices, out)
  872. return
  873. weight, scales = self._storage()
  874. # The table dwarfs TLB reach, so a persistent grid near the SM count
  875. # beats one program per row; halve it to leave SMs for the main stream.
  876. tiles = triton.cdiv(rows, 16)
  877. grid = min(tiles, self._num_sms // 2 if background else self._num_sms)
  878. _engram_lookup_kernel[(grid,)](
  879. weight,
  880. scales,
  881. indices,
  882. out,
  883. self.vocab_start_idx,
  884. self.vocab_end_idx,
  885. rows,
  886. indices.stride(0),
  887. indices.stride(1),
  888. HEAD_START=self.head_start,
  889. LOCAL_HEADS=self.part_n_hash_cols,
  890. TOTAL_HEADS=self.n_hash_cols,
  891. DIM=self.dim,
  892. QUANT_BLOCK=self.block_size,
  893. BLOCK_R=16,
  894. GRID=grid,
  895. )
  896. def disk_rel_owned(self, local: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
  897. """local: [T, <=L] int64 CPU hash ids of this rank's heads -> flat
  898. (rel [T*L] rank-local row ids, owned [T*L] bool). Padded heads (beyond
  899. TOTAL_HEADS) and ids outside this rank's range are not owned (zeros)."""
  900. T = local.shape[0]
  901. L = self.part_n_hash_cols
  902. if local.shape[1] < L:
  903. pad = torch.full((T, L - local.shape[1]), -1, dtype=torch.int64)
  904. local = torch.cat([local, pad], dim=1)
  905. rows = local.reshape(-1)
  906. owned = (rows >= self.vocab_start_idx) & (rows < self.vocab_end_idx)
  907. rel = torch.where(owned, rows - self.vocab_start_idx, torch.zeros_like(rows))
  908. return rel, owned
  909. def _disk_lookup(self, indices: torch.Tensor, out: torch.Tensor) -> None:
  910. """Host-side gather + dequant for DISK mode, then H2D into `out`
  911. ([T, local_heads, dim] bf16). Padded heads (beyond TOTAL_HEADS) and
  912. rows outside this rank's range write zeros, matching the kernel.
  913. In-forward fallback only (V1 runner / eager). Under the V2 runner the
  914. rows are staged by EngramDiskStager before the forward instead."""
  915. if torch.cuda.is_available() and torch.cuda.is_current_stream_capturing():
  916. raise RuntimeError(
  917. "Engram DISK lookup reached a CUDA-graph capture: it needs a host "
  918. "round trip. Stage rows before the forward (EngramDiskStager, V2 "
  919. "runner + patched nvidia/model_state.py) or run --enforce-eager."
  920. )
  921. T = indices.shape[0]
  922. L = self.part_n_hash_cols
  923. ids = indices.detach().to("cpu", dtype=torch.int64)
  924. head_end = min(self.head_start + L, self.n_hash_cols)
  925. rel, owned = self.disk_rel_owned(ids[:, self.head_start:head_end])
  926. assert self.disk is not None
  927. deq = self.disk.gather_dequant(rel, owned)
  928. out[:T].copy_(deq.view(T, L, self.dim))
  929. def forward(self, indices: torch.Tensor) -> torch.Tensor:
  930. """indices: [num_tokens, n_hash_cols] -> [num_tokens, n_hash_cols, dim]
  931. bf16, gathered from all TP shards."""
  932. out = torch.empty(
  933. (indices.shape[0], self.part_n_hash_cols, self.dim),
  934. dtype=torch.bfloat16,
  935. device=indices.device,
  936. )
  937. self.lookup(indices, out)
  938. if self.tp_size > 1:
  939. out = tensor_model_parallel_all_gather(out, dim=1)
  940. out = out[:, : self.n_hash_cols]
  941. return out
  942. @triton.jit(do_not_specialize=["num_kv_tokens"])
  943. def _fused_engram_post_wkv_kernel(
  944. hidden_states,
  945. kv,
  946. q_weight,
  947. k_weight,
  948. token_mask,
  949. output,
  950. num_kv_tokens,
  951. hidden_stride_t,
  952. hidden_stride_h,
  953. hidden_stride_d,
  954. kv_stride_t,
  955. kv_stride_d,
  956. q_stride_h,
  957. q_stride_d,
  958. k_stride_h,
  959. k_stride_d,
  960. mask_stride,
  961. output_stride_t,
  962. output_stride_h,
  963. output_stride_d,
  964. eps,
  965. clamp_value,
  966. DIM: tl.constexpr,
  967. HC_MULT: tl.constexpr,
  968. BLOCK_SIZE: tl.constexpr,
  969. HAS_MASK: tl.constexpr,
  970. ):
  971. program_idx = tl.program_id(0)
  972. token_idx = program_idx // HC_MULT
  973. hc_idx = program_idx % HC_MULT
  974. token_idx = token_idx.to(tl.int64)
  975. source_idx = token_idx
  976. source_valid = source_idx < num_kv_tokens
  977. dim_offsets = tl.arange(0, BLOCK_SIZE)
  978. dim_valid = dim_offsets < DIM
  979. hidden = tl.load(
  980. hidden_states
  981. + token_idx * hidden_stride_t
  982. + hc_idx * hidden_stride_h
  983. + dim_offsets * hidden_stride_d,
  984. mask=dim_valid,
  985. other=0.0,
  986. ).to(tl.float32)
  987. key = tl.load(
  988. kv + source_idx * kv_stride_t + (hc_idx * DIM + dim_offsets) * kv_stride_d,
  989. mask=source_valid & dim_valid,
  990. other=0.0,
  991. ).to(tl.float32)
  992. q = tl.load(
  993. q_weight + hc_idx * q_stride_h + dim_offsets * q_stride_d,
  994. mask=dim_valid,
  995. other=0.0,
  996. ).to(tl.float32)
  997. k = tl.load(
  998. k_weight + hc_idx * k_stride_h + dim_offsets * k_stride_d,
  999. mask=dim_valid,
  1000. other=0.0,
  1001. ).to(tl.float32)
  1002. hidden_rms = tl.rsqrt(tl.sum(hidden * hidden, axis=0) / DIM + eps)
  1003. key_rms = tl.rsqrt(tl.sum(key * key, axis=0) / DIM + eps)
  1004. dot = tl.sum(hidden * q * k * key, axis=0)
  1005. dot *= hidden_rms * key_rms * tl.rsqrt(DIM * 1.0)
  1006. gate_input = tl.sqrt(tl.maximum(tl.abs(dot), clamp_value))
  1007. gate_input = tl.where(dot < 0.0, -gate_input, gate_input)
  1008. gate = tl.sigmoid(gate_input)
  1009. if HAS_MASK:
  1010. active = tl.load(
  1011. token_mask + source_idx * mask_stride,
  1012. mask=source_valid,
  1013. other=0,
  1014. )
  1015. gate = tl.where(active, gate, 0.0)
  1016. value = tl.load(
  1017. kv + source_idx * kv_stride_t + (HC_MULT * DIM + dim_offsets) * kv_stride_d,
  1018. mask=source_valid & dim_valid,
  1019. other=0.0,
  1020. ).to(tl.float32)
  1021. tl.store(
  1022. output
  1023. + token_idx * output_stride_t
  1024. + hc_idx * output_stride_h
  1025. + dim_offsets * output_stride_d,
  1026. hidden + gate * value,
  1027. mask=dim_valid,
  1028. )
  1029. @triton.jit(do_not_specialize=["num_tokens", "token_start", "num_elements"])
  1030. def _engram_sp_rows_kernel(
  1031. gathered,
  1032. output,
  1033. num_tokens,
  1034. token_start,
  1035. num_elements,
  1036. LOCAL_WIDTH: tl.constexpr,
  1037. WIDTH: tl.constexpr,
  1038. BLOCK_SIZE: tl.constexpr,
  1039. ):
  1040. offsets = tl.program_id(0).to(tl.int64) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  1041. tokens = token_start + offsets // WIDTH
  1042. cols = offsets % WIDTH
  1043. source = (cols // LOCAL_WIDTH * num_tokens + tokens) * LOCAL_WIDTH
  1044. source += cols % LOCAL_WIDTH
  1045. values = tl.load(
  1046. gathered + source, (offsets < num_elements) & (tokens < num_tokens), other=0
  1047. )
  1048. tl.store(output + offsets, values, offsets < num_elements)
  1049. class Engram(nn.Module):
  1050. """Writes an n-gram lookup into the residual stream, gated by how well it
  1051. matches that stream.
  1052. The hash ids fetch `n_hash_cols` rows; `wkv` turns them into one key per
  1053. hc copy plus a shared value. The gate is a normalized dot product of the
  1054. stream against the key, signed-sqrt'ed before the sigmoid (matching the
  1055. training kernel).
  1056. """
  1057. def __init__(
  1058. self,
  1059. config,
  1060. quant_config: QuantizationConfig | None,
  1061. layout: EngramLayout,
  1062. layer_hash_index: int,
  1063. use_sequence_parallel: bool,
  1064. prefix: str,
  1065. ) -> None:
  1066. super().__init__()
  1067. self.layer_hash_index = layer_hash_index
  1068. self.dim = config.hidden_size
  1069. self.hc_mult = config.hc_mult
  1070. self.eps = config.rms_norm_eps
  1071. self.clamp_value = 1e-6
  1072. self.use_sequence_parallel = use_sequence_parallel
  1073. # Named ``embed_tokens`` so the checkpoint's ``engram.embed.weight``
  1074. # survives the mapper's ``embed.weight`` -> ``embed_tokens.weight``
  1075. # suffix rule.
  1076. engram_config = get_current_vllm_config().engram_config
  1077. self.embed_tokens = ParallelEngramEmbedding(
  1078. layout.num_embeddings[layer_hash_index],
  1079. layout.head_dim,
  1080. tuple(size for order in layout.primes[layer_hash_index] for size in order),
  1081. cpu_offload=engram_config.cpu_offload if engram_config else True,
  1082. model_dir=get_current_vllm_config().model_config.model,
  1083. layer_id=layout.layer_ids[layer_hash_index],
  1084. )
  1085. n_hash_cols = (layout.max_ngram_size - 1) * layout.n_heads
  1086. self.wkv = ReplicatedLinear(
  1087. n_hash_cols * layout.head_dim,
  1088. self.dim * (self.hc_mult + 1),
  1089. bias=False,
  1090. quant_config=quant_config,
  1091. return_bias=False,
  1092. prefix=f"{prefix}.wkv",
  1093. )
  1094. self.q_weight = nn.Parameter(
  1095. torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
  1096. requires_grad=False,
  1097. )
  1098. self.k_weight = nn.Parameter(
  1099. torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
  1100. requires_grad=False,
  1101. )
  1102. max_tokens = get_current_vllm_config().scheduler_config.max_num_batched_tokens
  1103. # Tech2Wild/Kai 2026-09-10 (CUDA graphs): under the V2 runner a DISK
  1104. # table is staged by the model state (EngramDiskStager) BEFORE the
  1105. # forward, so the forward holds no host round trip and can be captured
  1106. # (FULL decode graphs included). The V1 runner keeps the in-forward path.
  1107. self.prestage = self.embed_tokens.disk is not None and bool(
  1108. getattr(get_current_vllm_config(), "use_v2_model_runner", False)
  1109. )
  1110. # Keep lookup results alive across breakable graph segments. Zeroed so
  1111. # rows of cudagraph padding tokens (never staged) stay finite.
  1112. self.staged_rows = torch.zeros(
  1113. max_tokens,
  1114. self.embed_tokens.part_n_hash_cols,
  1115. layout.head_dim,
  1116. dtype=torch.bfloat16,
  1117. )
  1118. def prepare_embeddings(self, hash_ids: torch.Tensor) -> None:
  1119. """Gather this layer's rows on the main stream before decoder layers."""
  1120. if self.prestage:
  1121. # Already staged for this step by EngramDiskStager.stage().
  1122. return
  1123. self.embed_tokens.lookup(hash_ids, self.staged_rows[: hash_ids.shape[0]])
  1124. def embed(self, hash_ids: torch.Tensor) -> torch.Tensor:
  1125. """Gather heads, returning only local tokens when SP is enabled."""
  1126. rows = self.staged_rows[: hash_ids.shape[0]]
  1127. if self.embed_tokens.tp_size == 1:
  1128. return rows
  1129. if self.use_sequence_parallel:
  1130. tp_size = self.embed_tokens.tp_size
  1131. num_tokens, local_heads, dim = rows.shape
  1132. gathered = tensor_model_parallel_all_gather(rows, dim=0)
  1133. chunk = (num_tokens + tp_size - 1) // tp_size
  1134. rows = rows.new_empty((chunk, self.embed_tokens.n_hash_cols, dim))
  1135. _engram_sp_rows_kernel[(triton.cdiv(rows.numel(), 1024),)](
  1136. gathered,
  1137. rows,
  1138. num_tokens,
  1139. get_tensor_model_parallel_rank() * chunk,
  1140. rows.numel(),
  1141. local_heads * dim,
  1142. self.embed_tokens.n_hash_cols * dim,
  1143. BLOCK_SIZE=1024,
  1144. )
  1145. return rows
  1146. rows = tensor_model_parallel_all_gather(rows, dim=1)
  1147. return rows[:, : self.embed_tokens.n_hash_cols]
  1148. def forward(
  1149. self,
  1150. hidden_states: torch.Tensor,
  1151. hash_ids: torch.Tensor,
  1152. token_mask: torch.Tensor | None = None,
  1153. ) -> torch.Tensor:
  1154. """hidden_states: [T, hc_mult, dim]; hash_ids: [T, n_hash_cols] (all
  1155. tokens, pre sequence-parallel shard); token_mask: [T], False shuts
  1156. the gate so those positions pass through untouched."""
  1157. kv = self.wkv(self.embed(hash_ids).flatten(-2))
  1158. num_kv_tokens = hash_ids.shape[0]
  1159. assert token_mask is None or token_mask.shape == (num_kv_tokens,)
  1160. if self.use_sequence_parallel:
  1161. tp_size = get_tensor_model_parallel_world_size()
  1162. tp_rank = get_tensor_model_parallel_rank()
  1163. shard_size = (num_kv_tokens + tp_size - 1) // tp_size
  1164. assert hidden_states.shape[0] == shard_size
  1165. start = min(tp_rank * shard_size, num_kv_tokens)
  1166. num_kv_tokens = min(shard_size, num_kv_tokens - start)
  1167. if token_mask is not None:
  1168. token_mask = token_mask[start : start + num_kv_tokens]
  1169. num_tokens, hc_mult, dim = hidden_states.shape
  1170. assert hc_mult == self.hc_mult and dim == self.dim
  1171. assert kv.ndim == 2 and kv.shape[1] == (hc_mult + 1) * dim
  1172. output = torch.empty_like(hidden_states)
  1173. if num_tokens == 0:
  1174. return output
  1175. block_size = triton.next_power_of_2(dim)
  1176. num_warps = 8 if block_size >= 2048 else 4
  1177. mask = token_mask if token_mask is not None else hidden_states
  1178. _fused_engram_post_wkv_kernel[(num_tokens * hc_mult,)](
  1179. hidden_states,
  1180. kv,
  1181. self.q_weight,
  1182. self.k_weight,
  1183. mask,
  1184. output,
  1185. num_kv_tokens,
  1186. hidden_states.stride(0),
  1187. hidden_states.stride(1),
  1188. hidden_states.stride(2),
  1189. kv.stride(0),
  1190. kv.stride(1),
  1191. self.q_weight.stride(0),
  1192. self.q_weight.stride(1),
  1193. self.k_weight.stride(0),
  1194. self.k_weight.stride(1),
  1195. token_mask.stride(0) if token_mask is not None else 0,
  1196. output.stride(0),
  1197. output.stride(1),
  1198. output.stride(2),
  1199. self.eps,
  1200. self.clamp_value,
  1201. DIM=dim,
  1202. HC_MULT=hc_mult,
  1203. BLOCK_SIZE=block_size,
  1204. HAS_MASK=token_mask is not None,
  1205. num_warps=num_warps,
  1206. )
  1207. return output
  1208. class EngramDiskStager:
  1209. """Tech2Wild/Kai 2026-09-10 (CUDA graphs): stage DISK-mode Engram rows
  1210. OUTSIDE the model forward.
  1211. The V2 model state calls :meth:`stage` from ``prepare_inputs`` once per
  1212. step, after the runner has written this step's input ids, positions and
  1213. query_start_loc (all on the current stream). It hashes the step on the GPU
  1214. (same kernel, same inputs as the forward), copies only this rank's hash
  1215. columns to pinned host memory, waits on ONE event, reads every row of every
  1216. engram layer in one parallel batch, dequantizes on the CPU exactly like the
  1217. in-forward path, and copies (async, from pinned memory) into each layer's
  1218. persistent ``staged_rows``. The forward (eager, breakable PIECEWISE or FULL
  1219. graph) only reads ``staged_rows``, so it never needs the host.
  1220. """
  1221. def __init__(self, hash_state: NgramHashState, engrams: list["Engram"]) -> None:
  1222. assert engrams and all(e.embed_tokens.disk is not None for e in engrams)
  1223. self.hash_state = hash_state
  1224. self.engrams = sorted(engrams, key=lambda e: e.layer_hash_index)
  1225. emb = self.engrams[0].embed_tokens
  1226. self.local_heads = emb.part_n_hash_cols
  1227. self.head_start = emb.head_start
  1228. self.head_end = min(emb.head_start + emb.part_n_hash_cols, emb.n_hash_cols)
  1229. self.dim = emb.dim
  1230. self.max_tokens = self.engrams[0].staged_rows.shape[0]
  1231. num_layers = hash_state.multipliers.shape[0]
  1232. self.hash_host = torch.empty(
  1233. (self.max_tokens, num_layers, self.head_end - self.head_start),
  1234. dtype=torch.int32,
  1235. device="cpu",
  1236. pin_memory=True,
  1237. )
  1238. self.rows_host = [
  1239. torch.empty(
  1240. (self.max_tokens, self.local_heads, self.dim),
  1241. dtype=torch.bfloat16,
  1242. device="cpu",
  1243. pin_memory=True,
  1244. )
  1245. for _ in self.engrams
  1246. ]
  1247. self.hashes_ready = torch.cuda.Event()
  1248. self.num_staged = 0
  1249. logger.info(
  1250. "Engram DISK rows staged before the forward (graph-safe): %d layers, "
  1251. "heads [%d, %d) of %d, up to %d tokens/step, %d read threads",
  1252. len(self.engrams),
  1253. self.head_start,
  1254. self.head_end,
  1255. emb.n_hash_cols,
  1256. self.max_tokens,
  1257. _KAI_THREADS,
  1258. )
  1259. @torch.inference_mode()
  1260. def stage(
  1261. self,
  1262. input_ids: torch.Tensor,
  1263. positions: torch.Tensor,
  1264. query_start_loc: torch.Tensor,
  1265. lookback_token_ids: torch.Tensor,
  1266. num_tokens: int,
  1267. ) -> int:
  1268. """Stage rows for the first `num_tokens` (unpadded) tokens of this step.
  1269. Returns the number staged; 0 while the KV cache is unbound (profiling),
  1270. matching the forward, which skips engram then."""
  1271. n = min(int(num_tokens), self.max_tokens)
  1272. if n <= 0 or not self.hash_state.ensure_cache():
  1273. return 0
  1274. from .mm_preprocess import image_sentinel_mask
  1275. ids = input_ids[:n]
  1276. hashes = self.hash_state(
  1277. ids,
  1278. positions[:n],
  1279. query_start_loc,
  1280. image_sentinel_mask(ids),
  1281. lookback_token_ids,
  1282. image_sentinel_mask(lookback_token_ids),
  1283. None,
  1284. None,
  1285. )
  1286. host = self.hash_host[:n]
  1287. host.copy_(hashes[:, :, self.head_start : self.head_end], non_blocking=True)
  1288. self.hashes_ready.record()
  1289. # The one host sync per step. It also orders this step's rewrite of the
  1290. # pinned buffers after the previous step's async H2D copies from them.
  1291. self.hashes_ready.synchronize()
  1292. requests = []
  1293. for engram in self.engrams:
  1294. local = host[:, engram.layer_hash_index, :].to(torch.int64)
  1295. rel, owned = engram.embed_tokens.disk_rel_owned(local)
  1296. requests.append((engram.embed_tokens.disk, rel, owned))
  1297. rows_per_layer = gather_dequant_many(requests)
  1298. for engram, rows, buf in zip(self.engrams, rows_per_layer, self.rows_host):
  1299. staged = buf[:n]
  1300. staged.copy_(rows.view(n, self.local_heads, self.dim))
  1301. engram.staged_rows[:n].copy_(staged, non_blocking=True)
  1302. self.num_staged = n
  1303. return n