engram.py 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033
  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. from vllm.config import VllmConfig, get_current_vllm_config
  37. from vllm.distributed import (
  38. get_tensor_model_parallel_rank,
  39. get_tensor_model_parallel_world_size,
  40. tensor_model_parallel_all_gather,
  41. )
  42. from vllm.logger import init_logger
  43. from vllm.model_executor.layers.linear import ReplicatedLinear
  44. from vllm.model_executor.layers.quantization import QuantizationConfig
  45. from vllm.model_executor.utils import set_weight_attrs
  46. from vllm.triton_utils import tl, triton
  47. from vllm.utils.platform_utils import is_uva_available
  48. from vllm.utils.torch_utils import get_accelerator_view_from_cpu_tensor
  49. logger = init_logger(__name__)
  50. # Cache value for tokens that take no part in an n-gram (image spans).
  51. DEAD_ID = -1
  52. def _is_prime(n: int) -> bool:
  53. """Deterministic Miller-Rabin for n < 2**32 (avoids a sympy import)."""
  54. if n < 2:
  55. return False
  56. for p in (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37):
  57. if n % p == 0:
  58. return n == p
  59. d = n - 1
  60. r = 0
  61. while d % 2 == 0:
  62. d //= 2
  63. r += 1
  64. for a in (2, 7, 61):
  65. x = pow(a, d, n)
  66. if x in (1, n - 1):
  67. continue
  68. for _ in range(r - 1):
  69. x = x * x % n
  70. if x == n - 1:
  71. break
  72. else:
  73. return False
  74. return True
  75. def find_next_prime(start: int, seen_primes: set[int]) -> int:
  76. """The smallest prime above `start` that has not been handed out yet."""
  77. candidate = start + 1
  78. while not _is_prime(candidate) or candidate in seen_primes:
  79. candidate += 1
  80. return candidate
  81. def build_compressed_token_map(tokenizer) -> tuple[list[int], int]:
  82. """Map every token id onto a smaller id space where tokens that normalize
  83. alike collapse together.
  84. N-grams are hashed over these compressed ids, so " The", "the" and "THE"
  85. all hash the same way. The compressed size matters beyond bounds checking:
  86. every hash multiplier is derived from it.
  87. """
  88. from tokenizers import Regex, normalizers
  89. # A private-use char, so a token that is exactly one space survives
  90. # Strip() instead of collapsing to the empty string and merging with
  91. # unrelated tokens.
  92. sentinel = "\ue000"
  93. normalizer = normalizers.Sequence(
  94. [
  95. normalizers.NFKC(),
  96. normalizers.NFD(),
  97. normalizers.StripAccents(),
  98. normalizers.Lowercase(),
  99. normalizers.Replace(Regex(r"[ \t\r\n]+"), " "),
  100. normalizers.Replace(Regex(r"^ $"), sentinel),
  101. normalizers.Strip(),
  102. normalizers.Replace(sentinel, " "),
  103. ]
  104. )
  105. # The raw Rust tokenizer, matching what training decodes with
  106. # (no clean_up_tokenization_spaces).
  107. backend = tokenizer.backend_tokenizer
  108. key_to_new: dict[str, int] = {}
  109. lookup = [0] * len(tokenizer)
  110. for token_id in range(len(tokenizer)):
  111. text = backend.decode([token_id], skip_special_tokens=False)
  112. if "\ufffd" in text:
  113. # A partial UTF-8 byte token: nothing to normalize, so key it
  114. # by its raw form.
  115. key = backend.id_to_token(token_id)
  116. else:
  117. normalized = normalizer.normalize_str(text)
  118. key = normalized if normalized else text
  119. new_id = key_to_new.get(key)
  120. if new_id is None:
  121. new_id = len(key_to_new)
  122. key_to_new[key] = new_id
  123. lookup[token_id] = new_id
  124. return lookup, len(key_to_new)
  125. def compute_hash_multipliers(
  126. layer_ids: tuple[int, ...], max_ngram_size: int, compressed_vocab_size: int
  127. ) -> torch.Tensor:
  128. """One multiplier per (layer, lookback), from a per-layer RNG so layers
  129. hash differently. Kept odd and bounded so `token_id * multiplier` cannot
  130. overflow int64.
  131. """
  132. max_long = np.iinfo(np.int64).max
  133. multiplier_bound = max(1, (max_long // compressed_vocab_size) // 2)
  134. rows = []
  135. for layer_id in layer_ids:
  136. generator = np.random.default_rng(10007 * layer_id)
  137. values = generator.integers(
  138. low=0,
  139. high=multiplier_bound,
  140. size=(max_ngram_size,),
  141. dtype=np.int64,
  142. )
  143. rows.append(torch.tensor(values * 2 + 1))
  144. return torch.stack(rows)
  145. class EngramLayout:
  146. """Bucket layout of the n-gram hash tables.
  147. A position is hashed as `max_ngram_size - 1` n-grams (2-gram .. max), each
  148. split over `n_heads` heads. Every (n-gram size, head) pair owns its own
  149. prime-sized bucket range in the layer's table; the primes are drawn in
  150. order and never reused, which keeps the ranges disjoint.
  151. """
  152. def __init__(self, config) -> None:
  153. self.layer_ids: tuple[int, ...] = tuple(config.engram_layer_ids)
  154. self.num_embeddings: tuple[int, ...] = tuple(config.engram_num_embeddings)
  155. self.max_ngram_size: int = config.engram_max_ngram_size
  156. self.n_heads: int = config.engram_n_heads
  157. self.head_dim: int = config.engram_head_dim
  158. self.compressed_vocab_size: int = config.engram_compressed_vocab_size
  159. self.pad_token_id: int = config.engram_pad_token_id
  160. assert len(self.layer_ids) == len(self.num_embeddings)
  161. primes = []
  162. seen: set[int] = set()
  163. for _ in self.layer_ids:
  164. per_ngram = []
  165. for _ in range(self.max_ngram_size - 1):
  166. sizes, current = [], config.engram_vocab_size - 1
  167. for _ in range(self.n_heads):
  168. current = find_next_prime(current, seen)
  169. seen.add(current)
  170. sizes.append(current)
  171. per_ngram.append(tuple(sizes))
  172. primes.append(tuple(per_ngram))
  173. self.primes: tuple[tuple[tuple[int, ...], ...], ...] = tuple(primes)
  174. self.n_hash_cols = (self.max_ngram_size - 1) * self.n_heads
  175. flat = [[p for per_ngram in layer for p in per_ngram] for layer in primes]
  176. offsets = [np.cumsum([0, *sizes[:-1]]) for sizes in flat]
  177. self.offsets = torch.tensor(np.array(offsets)) # [n_layers, n_hash_cols]
  178. @classmethod
  179. def from_config(cls, config) -> "EngramLayout | None":
  180. if not getattr(config, "engram_layer_ids", None):
  181. return None
  182. return cls(config)
  183. @triton.jit(do_not_specialize=["num_tokens"])
  184. def _write_hash_cache_kernel(
  185. input_ids,
  186. token_map,
  187. dead_mask,
  188. slot_mapping,
  189. cache,
  190. num_tokens,
  191. input_stride,
  192. mask_stride,
  193. slot_stride,
  194. BLOCK_SIZE: tl.constexpr,
  195. dead_id,
  196. ):
  197. token_idx = tl.program_id(0) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  198. slot = tl.load(
  199. slot_mapping + token_idx * slot_stride, token_idx < num_tokens, other=-1
  200. ).to(tl.int64)
  201. valid = (token_idx < num_tokens) & (slot >= 0)
  202. token = tl.load(input_ids + token_idx * input_stride, valid, other=0)
  203. value = tl.load(token_map + token, valid, other=0)
  204. dead = tl.load(dead_mask + token_idx * mask_stride, valid, other=False)
  205. value = tl.where(dead, dead_id, value)
  206. tl.store(cache + slot, value, valid)
  207. @triton.jit(
  208. do_not_specialize=[
  209. "num_tokens",
  210. "num_slots",
  211. "num_query_rows",
  212. "num_table_rows",
  213. "max_blocks",
  214. ]
  215. )
  216. def _hash_ids_kernel(
  217. input_ids,
  218. token_map,
  219. dead_mask,
  220. positions,
  221. block_table,
  222. query_start_loc,
  223. multipliers,
  224. primes,
  225. offsets,
  226. cache,
  227. lookback_token_ids,
  228. lookback_dead_mask,
  229. output,
  230. num_tokens,
  231. num_slots,
  232. pad_id,
  233. input_stride,
  234. mask_stride,
  235. position_stride,
  236. table_stride,
  237. table_col_stride,
  238. query_stride,
  239. num_query_rows,
  240. num_table_rows,
  241. max_blocks,
  242. cache_block_size,
  243. MAX_NGRAM: tl.constexpr,
  244. num_heads,
  245. BLOCK_T: tl.constexpr,
  246. BLOCK_H: tl.constexpr,
  247. dead_id,
  248. lookback_depth,
  249. lookback_row_stride,
  250. lookback_col_stride,
  251. lookback_mask_row_stride,
  252. lookback_mask_col_stride,
  253. ):
  254. token = tl.program_id(0) * BLOCK_T + tl.arange(0, BLOCK_T)
  255. layer = tl.program_id(1)
  256. num_layers = tl.num_programs(1)
  257. valid = token < num_tokens
  258. # Upper bound in query_start_loc[1:], including repeated padding boundaries.
  259. lo = tl.full((BLOCK_T,), 0, tl.int32)
  260. hi = tl.full((BLOCK_T,), num_query_rows, tl.int32)
  261. while tl.sum((lo < hi).to(tl.int32), 0) > 0:
  262. mid = (lo + hi) // 2
  263. end = tl.load(
  264. query_start_loc + (mid + 1) * query_stride,
  265. lo < hi,
  266. other=0,
  267. )
  268. right = token >= end
  269. active = lo < hi
  270. lo = tl.where(active & right, mid + 1, lo)
  271. hi = tl.where(active & ~right, mid, hi)
  272. req = tl.minimum(lo, num_query_rows - 1).to(tl.int64)
  273. chunk_idx = tl.load(query_start_loc + req * query_stride)
  274. chunk_idx = tl.minimum(chunk_idx, num_tokens - 1).to(tl.int64)
  275. chunk_start = tl.load(positions + chunk_idx * position_stride)
  276. position = tl.load(positions + token * position_stride, valid, other=0).to(tl.int64)
  277. head = tl.arange(0, BLOCK_H)
  278. blocked = tl.full((BLOCK_T,), False, tl.int1)
  279. rolling = tl.full((BLOCK_T,), 0, tl.int64)
  280. for shift in tl.static_range(MAX_NGRAM):
  281. lookback = position - shift
  282. in_batch = lookback >= chunk_start
  283. batch_idx = tl.maximum(token - shift, 0)
  284. batch_token = tl.load(
  285. input_ids + batch_idx * input_stride, valid & in_batch, other=0
  286. )
  287. batch_source = tl.load(token_map + batch_token, valid & in_batch, other=0)
  288. batch_dead = tl.load(
  289. dead_mask + batch_idx * mask_stride, valid & in_batch, other=False
  290. )
  291. batch_source = tl.where(batch_dead, dead_id, batch_source)
  292. col = chunk_start - 1 - lookback
  293. in_window = valid & ~in_batch & (col >= 0) & (col < lookback_depth)
  294. col = tl.minimum(tl.maximum(col, 0), lookback_depth - 1)
  295. window_token = tl.load(
  296. lookback_token_ids + req * lookback_row_stride + col * lookback_col_stride,
  297. in_window,
  298. other=-1,
  299. )
  300. known = in_window & (window_token >= 0)
  301. window_source = tl.load(token_map + window_token, known, other=0)
  302. window_dead = tl.load(
  303. lookback_dead_mask
  304. + req * lookback_mask_row_stride
  305. + col * lookback_mask_col_stride,
  306. known,
  307. other=False,
  308. )
  309. window_source = tl.where(window_dead, dead_id, window_source)
  310. if cache is not None:
  311. clamped = tl.minimum(
  312. tl.maximum(lookback, 0), max_blocks * cache_block_size - 1
  313. )
  314. block_row = tl.minimum(req, num_table_rows - 1)
  315. needs_cache = valid & ~in_batch & ~known
  316. block = tl.load(
  317. block_table
  318. + block_row * table_stride
  319. + (clamped // cache_block_size) * table_col_stride,
  320. needs_cache,
  321. other=0,
  322. ).to(tl.int64)
  323. slot = tl.minimum(
  324. tl.maximum(block * cache_block_size + clamped % cache_block_size, 0),
  325. num_slots - 1,
  326. )
  327. fallback = tl.load(cache + slot, needs_cache, other=0)
  328. else:
  329. fallback = tl.full((BLOCK_T,), pad_id, tl.int32)
  330. source = tl.where(
  331. in_batch, batch_source, tl.where(known, window_source, fallback)
  332. ).to(tl.int64)
  333. blocked |= (lookback < 0) | (source == dead_id)
  334. value = tl.where(blocked, pad_id, source)
  335. multiplier = tl.load(multipliers + layer * MAX_NGRAM + shift)
  336. rolling ^= value * multiplier
  337. if shift > 0:
  338. col = (shift - 1) * num_heads + head
  339. param_offset = layer * (MAX_NGRAM - 1) * num_heads + col
  340. prime = tl.load(primes + param_offset, head < num_heads, other=1)
  341. offset = tl.load(offsets + param_offset, head < num_heads, other=0)
  342. hashed = rolling[:, None] % prime[None, :] + offset[None, :]
  343. out_offset = (token.to(tl.int64) * num_layers + layer)[:, None] * (
  344. (MAX_NGRAM - 1) * num_heads
  345. ) + col[None, :]
  346. tl.store(output + out_offset, hashed, valid[:, None] & (head < num_heads))
  347. class NgramHashState(nn.Module):
  348. """Maps each position to the hash ids of the n-grams ending there.
  349. Stateless on the V2 runner, which supplies every lookback token id. On
  350. the V1 runner it also keeps `hash_cache`, the slot-keyed rolling store
  351. of compressed ids (see module docstring), for generated tokens.
  352. """
  353. def __init__(
  354. self,
  355. vllm_config: VllmConfig,
  356. layout: EngramLayout,
  357. swa_cache_module: nn.Module,
  358. ) -> None:
  359. super().__init__()
  360. self.layout = layout
  361. self.swa_cache_module = swa_cache_module
  362. self.block_size: int = swa_cache_module.block_size
  363. self.lookback_depth: int = layout.max_ngram_size - 1
  364. self.use_slot_cache: bool = not vllm_config.use_v2_model_runner
  365. self._cache: torch.Tensor | None = None
  366. self._kv_cache_ref: weakref.ReferenceType[torch.Tensor] | None = None
  367. model_config = vllm_config.model_config
  368. from transformers import AutoTokenizer
  369. tokenizer = AutoTokenizer.from_pretrained(
  370. model_config.tokenizer,
  371. trust_remote_code=model_config.trust_remote_code,
  372. revision=model_config.revision,
  373. )
  374. token_map, vocab_size = build_compressed_token_map(tokenizer)
  375. if vocab_size != layout.compressed_vocab_size:
  376. raise ValueError(
  377. f"Compressed vocab size mismatch: built {vocab_size} from the "
  378. f"tokenizer, config expects {layout.compressed_vocab_size}; "
  379. "every hash multiplier derives from it, so the engram tables "
  380. "would be silently rehashed."
  381. )
  382. self.pad_id = token_map[layout.pad_token_id]
  383. multipliers = compute_hash_multipliers(
  384. layout.layer_ids, layout.max_ngram_size, vocab_size
  385. )
  386. self.register_buffer(
  387. "token_map", torch.tensor(token_map, dtype=torch.int32), persistent=False
  388. )
  389. self.register_buffer("primes", torch.tensor(layout.primes), persistent=False)
  390. self.register_buffer("offsets", layout.offsets, persistent=False)
  391. self.register_buffer("multipliers", multipliers, persistent=False)
  392. logger.info(
  393. "Built engram token map (%d -> %d ids) for layers %s",
  394. len(token_map),
  395. vocab_size,
  396. layout.layer_ids,
  397. )
  398. def ensure_cache(self) -> bool:
  399. """Lazily size the slot-keyed cache from the bound SWA KV cache.
  400. Returns False while the KV cache is unbound (profile run); the caller
  401. skips engram hashing then. Without the slot cache only that check
  402. remains.
  403. """
  404. kv_cache = self.swa_cache_module.kv_cache
  405. if kv_cache.numel() == 0:
  406. self._cache = None
  407. self._kv_cache_ref = None
  408. return False
  409. if not self.use_slot_cache:
  410. return True
  411. if self._kv_cache_ref is not None and self._kv_cache_ref() is kv_cache:
  412. return True
  413. # Graph memory profiling binds a temporary, smaller KV cache first.
  414. # Rebinding must discard its hash history without retaining KV storage.
  415. self._cache = torch.zeros(
  416. kv_cache.shape[0] * self.block_size,
  417. dtype=torch.int32,
  418. device=kv_cache.device,
  419. )
  420. self._kv_cache_ref = weakref.ref(kv_cache)
  421. return True
  422. def forward(
  423. self,
  424. input_ids: torch.Tensor,
  425. positions: torch.Tensor,
  426. query_start_loc: torch.Tensor,
  427. dead_mask: torch.Tensor,
  428. lookback_token_ids: torch.Tensor,
  429. lookback_dead_mask: torch.Tensor,
  430. slot_mapping: torch.Tensor | None,
  431. block_table: torch.Tensor | None,
  432. ) -> torch.Tensor:
  433. """Compute [tokens, layers, hash columns] int32 n-gram hashes.
  434. History comes from the current chunk, then the runner's lookback
  435. window, then the optional V1 slot cache. V2 needs only one launch.
  436. """
  437. cache = self._cache if self.use_slot_cache else None
  438. num_tokens = input_ids.shape[0]
  439. num_layers, max_ngram = self.multipliers.shape
  440. num_heads = self.primes.shape[-1]
  441. output = input_ids.new_empty(
  442. (num_tokens, num_layers, (max_ngram - 1) * num_heads), dtype=torch.int32
  443. )
  444. if num_tokens == 0:
  445. return output
  446. if self.use_slot_cache:
  447. assert cache is not None and slot_mapping is not None
  448. assert block_table is not None
  449. # Finish writes before other thread blocks read fallback history.
  450. _write_hash_cache_kernel[(triton.cdiv(num_tokens, 256),)](
  451. input_ids,
  452. self.token_map,
  453. dead_mask,
  454. slot_mapping,
  455. cache,
  456. num_tokens,
  457. input_ids.stride(0),
  458. dead_mask.stride(0),
  459. slot_mapping.stride(0),
  460. 256,
  461. DEAD_ID,
  462. )
  463. _hash_ids_kernel[(triton.cdiv(num_tokens, 32), num_layers)](
  464. input_ids,
  465. self.token_map,
  466. dead_mask,
  467. positions,
  468. block_table,
  469. query_start_loc,
  470. self.multipliers,
  471. self.primes,
  472. self.offsets,
  473. cache,
  474. lookback_token_ids,
  475. lookback_dead_mask,
  476. output,
  477. num_tokens,
  478. cache.shape[0] if cache is not None else 0,
  479. self.pad_id,
  480. input_stride=input_ids.stride(0),
  481. mask_stride=dead_mask.stride(0),
  482. position_stride=positions.stride(0),
  483. table_stride=block_table.stride(0) if block_table is not None else 0,
  484. table_col_stride=block_table.stride(1) if block_table is not None else 0,
  485. query_stride=query_start_loc.stride(0),
  486. num_query_rows=query_start_loc.numel() - 1,
  487. num_table_rows=block_table.shape[0] if block_table is not None else 0,
  488. max_blocks=block_table.shape[1] if block_table is not None else 0,
  489. cache_block_size=self.block_size,
  490. MAX_NGRAM=max_ngram,
  491. num_heads=num_heads,
  492. BLOCK_T=32,
  493. BLOCK_H=triton.next_power_of_2(num_heads),
  494. dead_id=DEAD_ID,
  495. lookback_depth=lookback_token_ids.shape[1],
  496. lookback_row_stride=lookback_token_ids.stride(0),
  497. lookback_col_stride=lookback_token_ids.stride(1),
  498. lookback_mask_row_stride=lookback_dead_mask.stride(0),
  499. lookback_mask_col_stride=lookback_dead_mask.stride(1),
  500. num_warps=4,
  501. )
  502. return output
  503. def _engram_head_shard_weight_loader(
  504. param: torch.nn.Parameter, loaded_weight: torch.Tensor
  505. ) -> None:
  506. """Load this rank's complete head buckets. ue8m0 scales arrive as
  507. float8_e8m0fnu; keep the raw bytes (the param stores uint8)."""
  508. part_rows = param.shape[0]
  509. if loaded_weight.dtype == torch.float8_e8m0fnu:
  510. loaded_weight = loaded_weight.view(torch.uint8)
  511. shard = loaded_weight.narrow(0, param.engram_vocab_start, part_rows)
  512. assert shard.shape == param.shape, (
  513. f"engram shard {tuple(shard.shape)} does not fit param {tuple(param.shape)}"
  514. )
  515. param.data.copy_(shard)
  516. @triton.jit
  517. def _engram_lookup_kernel(
  518. weight,
  519. scales,
  520. ids,
  521. out,
  522. vocab_start,
  523. vocab_end,
  524. num_rows,
  525. ids_stride_t,
  526. ids_stride_h,
  527. HEAD_START: tl.constexpr,
  528. LOCAL_HEADS: tl.constexpr,
  529. TOTAL_HEADS: tl.constexpr,
  530. DIM: tl.constexpr,
  531. QUANT_BLOCK: tl.constexpr,
  532. BLOCK_R: tl.constexpr,
  533. GRID: tl.constexpr,
  534. ):
  535. """Gather fp8 rows, apply their ue8m0 block scales, write bf16.
  536. Only this rank's heads are read; padded heads write zeros for all-gather.
  537. `weight`/`scales` may address pinned host memory through UVA.
  538. """
  539. cols = tl.arange(0, DIM)
  540. scale_cols = cols // QUANT_BLOCK
  541. for base in tl.range(tl.program_id(0) * BLOCK_R, num_rows, GRID * BLOCK_R):
  542. rows = base + tl.arange(0, BLOCK_R)
  543. valid = rows < num_rows
  544. head = HEAD_START + rows % LOCAL_HEADS
  545. token = (rows // LOCAL_HEADS).to(tl.int64)
  546. index = tl.load(
  547. ids + token * ids_stride_t + head * ids_stride_h,
  548. mask=valid & (head < TOTAL_HEADS),
  549. other=-1,
  550. ).to(tl.int64)
  551. owned = valid & (head < TOTAL_HEADS)
  552. owned &= (index >= vocab_start) & (index < vocab_end)
  553. local = tl.where(owned, index - vocab_start, 0)
  554. values = tl.load(
  555. weight + local[:, None] * DIM + cols[None, :],
  556. mask=owned[:, None],
  557. other=0.0,
  558. )
  559. scale = tl.load(
  560. scales + local[:, None] * (DIM // QUANT_BLOCK) + scale_cols[None, :],
  561. mask=owned[:, None],
  562. other=0,
  563. )
  564. # ue8m0 is a power of two, so its byte *is* the fp32 exponent field.
  565. scale = (scale.to(tl.int32) << 23).to(tl.float32, bitcast=True)
  566. tl.store(
  567. out + rows[:, None] * DIM + cols[None, :],
  568. (values.to(tl.float32) * scale).to(tl.bfloat16),
  569. mask=valid[:, None],
  570. )
  571. class ParallelEngramEmbedding(nn.Module):
  572. """The n-gram hash table, sharded by complete hash heads over TP ranks.
  573. Rows stay fp8 and are dequantized with ue8m0 per-32 scales on lookup.
  574. With `cpu_offload` the shard lives in pinned host memory and is read over
  575. UVA instead of HBM; the TP sharding is unchanged either way.
  576. """
  577. def __init__(
  578. self,
  579. num_embeddings: int,
  580. dim: int,
  581. head_sizes: tuple[int, ...],
  582. block_size: int = 32,
  583. cpu_offload: bool = False,
  584. ):
  585. super().__init__()
  586. tp_size = get_tensor_model_parallel_world_size()
  587. tp_rank = get_tensor_model_parallel_rank()
  588. assert head_sizes and all(size > 0 for size in head_sizes)
  589. assert sum(head_sizes) <= num_embeddings
  590. if cpu_offload and not is_uva_available():
  591. raise RuntimeError("Engram CPU offload requires UVA support")
  592. self.num_embeddings = num_embeddings
  593. self.dim = dim
  594. self.block_size = block_size
  595. self.n_hash_cols = len(head_sizes)
  596. self.part_n_hash_cols = triton.cdiv(self.n_hash_cols, tp_size)
  597. self.head_start = tp_rank * self.part_n_hash_cols
  598. head_end = self.head_start + self.part_n_hash_cols
  599. self.vocab_start_idx = sum(head_sizes[: self.head_start])
  600. self.vocab_end_idx = sum(head_sizes[:head_end])
  601. self.part_num_embeddings = self.vocab_end_idx - self.vocab_start_idx
  602. self.tp_size = tp_size
  603. self.cpu_offload = cpu_offload
  604. self._views: tuple[torch.Tensor, torch.Tensor] | None = None
  605. self._view_src: tuple[int, int] | None = None
  606. self._num_sms = torch.cuda.get_device_properties(
  607. torch.accelerator.current_device_index()
  608. ).multi_processor_count
  609. # Explicit device: model init runs under a `torch.device("cuda")`
  610. # context, which would otherwise put the shard in HBM.
  611. kwargs = {"device": "cpu", "pin_memory": True} if cpu_offload else {}
  612. self.weight = nn.Parameter(
  613. torch.empty(
  614. self.part_num_embeddings, dim, dtype=torch.float8_e4m3fn, **kwargs
  615. ),
  616. requires_grad=False,
  617. )
  618. self.weight_scale_inv = nn.Parameter(
  619. torch.empty(
  620. self.part_num_embeddings,
  621. dim // block_size,
  622. dtype=torch.uint8,
  623. **kwargs,
  624. ),
  625. requires_grad=False,
  626. )
  627. for param in (self.weight, self.weight_scale_inv):
  628. set_weight_attrs(
  629. param,
  630. {
  631. "weight_loader": _engram_head_shard_weight_loader,
  632. "engram_vocab_start": self.vocab_start_idx,
  633. },
  634. )
  635. if cpu_offload:
  636. logger.info(
  637. "Engram table offloaded to pinned host memory: %d rows x %d, "
  638. "%.2f GiB per rank",
  639. self.part_num_embeddings,
  640. dim,
  641. self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
  642. )
  643. def _storage(self) -> tuple[torch.Tensor, torch.Tensor]:
  644. """Parameters when resident, else cached UVA views of the pinned shard.
  645. Rebuilt if anything swaps `.data`, so a stale device pointer cannot
  646. survive silently.
  647. """
  648. if not self.cpu_offload:
  649. return self.weight.data, self.weight_scale_inv.data
  650. src = (self.weight.data_ptr(), self.weight_scale_inv.data_ptr())
  651. if self._view_src != src:
  652. self._views = (
  653. get_accelerator_view_from_cpu_tensor(self.weight.data),
  654. get_accelerator_view_from_cpu_tensor(self.weight_scale_inv.data),
  655. )
  656. self._view_src = src
  657. assert self._views is not None
  658. return self._views
  659. def lookup(
  660. self, indices: torch.Tensor, out: torch.Tensor, background: bool = False
  661. ) -> None:
  662. """Look up local heads of [T, heads] into [T, local_heads, dim] bf16.
  663. `background` limits the grid to leave SMs for concurrent work.
  664. """
  665. rows = indices.shape[0] * self.part_n_hash_cols
  666. if not rows:
  667. return
  668. weight, scales = self._storage()
  669. # The table dwarfs TLB reach, so a persistent grid near the SM count
  670. # beats one program per row; halve it to leave SMs for the main stream.
  671. tiles = triton.cdiv(rows, 16)
  672. grid = min(tiles, self._num_sms // 2 if background else self._num_sms)
  673. _engram_lookup_kernel[(grid,)](
  674. weight,
  675. scales,
  676. indices,
  677. out,
  678. self.vocab_start_idx,
  679. self.vocab_end_idx,
  680. rows,
  681. indices.stride(0),
  682. indices.stride(1),
  683. HEAD_START=self.head_start,
  684. LOCAL_HEADS=self.part_n_hash_cols,
  685. TOTAL_HEADS=self.n_hash_cols,
  686. DIM=self.dim,
  687. QUANT_BLOCK=self.block_size,
  688. BLOCK_R=16,
  689. GRID=grid,
  690. )
  691. def forward(self, indices: torch.Tensor) -> torch.Tensor:
  692. """indices: [num_tokens, n_hash_cols] -> [num_tokens, n_hash_cols, dim]
  693. bf16, gathered from all TP shards."""
  694. out = torch.empty(
  695. (indices.shape[0], self.part_n_hash_cols, self.dim),
  696. dtype=torch.bfloat16,
  697. device=indices.device,
  698. )
  699. self.lookup(indices, out)
  700. if self.tp_size > 1:
  701. out = tensor_model_parallel_all_gather(out, dim=1)
  702. out = out[:, : self.n_hash_cols]
  703. return out
  704. @triton.jit(do_not_specialize=["num_kv_tokens"])
  705. def _fused_engram_post_wkv_kernel(
  706. hidden_states,
  707. kv,
  708. q_weight,
  709. k_weight,
  710. token_mask,
  711. output,
  712. num_kv_tokens,
  713. hidden_stride_t,
  714. hidden_stride_h,
  715. hidden_stride_d,
  716. kv_stride_t,
  717. kv_stride_d,
  718. q_stride_h,
  719. q_stride_d,
  720. k_stride_h,
  721. k_stride_d,
  722. mask_stride,
  723. output_stride_t,
  724. output_stride_h,
  725. output_stride_d,
  726. eps,
  727. clamp_value,
  728. DIM: tl.constexpr,
  729. HC_MULT: tl.constexpr,
  730. BLOCK_SIZE: tl.constexpr,
  731. HAS_MASK: tl.constexpr,
  732. ):
  733. program_idx = tl.program_id(0)
  734. token_idx = program_idx // HC_MULT
  735. hc_idx = program_idx % HC_MULT
  736. token_idx = token_idx.to(tl.int64)
  737. source_idx = token_idx
  738. source_valid = source_idx < num_kv_tokens
  739. dim_offsets = tl.arange(0, BLOCK_SIZE)
  740. dim_valid = dim_offsets < DIM
  741. hidden = tl.load(
  742. hidden_states
  743. + token_idx * hidden_stride_t
  744. + hc_idx * hidden_stride_h
  745. + dim_offsets * hidden_stride_d,
  746. mask=dim_valid,
  747. other=0.0,
  748. ).to(tl.float32)
  749. key = tl.load(
  750. kv + source_idx * kv_stride_t + (hc_idx * DIM + dim_offsets) * kv_stride_d,
  751. mask=source_valid & dim_valid,
  752. other=0.0,
  753. ).to(tl.float32)
  754. q = tl.load(
  755. q_weight + hc_idx * q_stride_h + dim_offsets * q_stride_d,
  756. mask=dim_valid,
  757. other=0.0,
  758. ).to(tl.float32)
  759. k = tl.load(
  760. k_weight + hc_idx * k_stride_h + dim_offsets * k_stride_d,
  761. mask=dim_valid,
  762. other=0.0,
  763. ).to(tl.float32)
  764. hidden_rms = tl.rsqrt(tl.sum(hidden * hidden, axis=0) / DIM + eps)
  765. key_rms = tl.rsqrt(tl.sum(key * key, axis=0) / DIM + eps)
  766. dot = tl.sum(hidden * q * k * key, axis=0)
  767. dot *= hidden_rms * key_rms * tl.rsqrt(DIM * 1.0)
  768. gate_input = tl.sqrt(tl.maximum(tl.abs(dot), clamp_value))
  769. gate_input = tl.where(dot < 0.0, -gate_input, gate_input)
  770. gate = tl.sigmoid(gate_input)
  771. if HAS_MASK:
  772. active = tl.load(
  773. token_mask + source_idx * mask_stride,
  774. mask=source_valid,
  775. other=0,
  776. )
  777. gate = tl.where(active, gate, 0.0)
  778. value = tl.load(
  779. kv + source_idx * kv_stride_t + (HC_MULT * DIM + dim_offsets) * kv_stride_d,
  780. mask=source_valid & dim_valid,
  781. other=0.0,
  782. ).to(tl.float32)
  783. tl.store(
  784. output
  785. + token_idx * output_stride_t
  786. + hc_idx * output_stride_h
  787. + dim_offsets * output_stride_d,
  788. hidden + gate * value,
  789. mask=dim_valid,
  790. )
  791. @triton.jit(do_not_specialize=["num_tokens", "token_start", "num_elements"])
  792. def _engram_sp_rows_kernel(
  793. gathered,
  794. output,
  795. num_tokens,
  796. token_start,
  797. num_elements,
  798. LOCAL_WIDTH: tl.constexpr,
  799. WIDTH: tl.constexpr,
  800. BLOCK_SIZE: tl.constexpr,
  801. ):
  802. offsets = tl.program_id(0).to(tl.int64) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  803. tokens = token_start + offsets // WIDTH
  804. cols = offsets % WIDTH
  805. source = (cols // LOCAL_WIDTH * num_tokens + tokens) * LOCAL_WIDTH
  806. source += cols % LOCAL_WIDTH
  807. values = tl.load(
  808. gathered + source, (offsets < num_elements) & (tokens < num_tokens), other=0
  809. )
  810. tl.store(output + offsets, values, offsets < num_elements)
  811. class Engram(nn.Module):
  812. """Writes an n-gram lookup into the residual stream, gated by how well it
  813. matches that stream.
  814. The hash ids fetch `n_hash_cols` rows; `wkv` turns them into one key per
  815. hc copy plus a shared value. The gate is a normalized dot product of the
  816. stream against the key, signed-sqrt'ed before the sigmoid (matching the
  817. training kernel).
  818. """
  819. def __init__(
  820. self,
  821. config,
  822. quant_config: QuantizationConfig | None,
  823. layout: EngramLayout,
  824. layer_hash_index: int,
  825. use_sequence_parallel: bool,
  826. prefix: str,
  827. ) -> None:
  828. super().__init__()
  829. self.layer_hash_index = layer_hash_index
  830. self.dim = config.hidden_size
  831. self.hc_mult = config.hc_mult
  832. self.eps = config.rms_norm_eps
  833. self.clamp_value = 1e-6
  834. self.use_sequence_parallel = use_sequence_parallel
  835. # Named ``embed_tokens`` so the checkpoint's ``engram.embed.weight``
  836. # survives the mapper's ``embed.weight`` -> ``embed_tokens.weight``
  837. # suffix rule.
  838. engram_config = get_current_vllm_config().engram_config
  839. self.embed_tokens = ParallelEngramEmbedding(
  840. layout.num_embeddings[layer_hash_index],
  841. layout.head_dim,
  842. tuple(size for order in layout.primes[layer_hash_index] for size in order),
  843. cpu_offload=engram_config.cpu_offload if engram_config else True,
  844. )
  845. n_hash_cols = (layout.max_ngram_size - 1) * layout.n_heads
  846. self.wkv = ReplicatedLinear(
  847. n_hash_cols * layout.head_dim,
  848. self.dim * (self.hc_mult + 1),
  849. bias=False,
  850. quant_config=quant_config,
  851. return_bias=False,
  852. prefix=f"{prefix}.wkv",
  853. )
  854. self.q_weight = nn.Parameter(
  855. torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
  856. requires_grad=False,
  857. )
  858. self.k_weight = nn.Parameter(
  859. torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
  860. requires_grad=False,
  861. )
  862. max_tokens = get_current_vllm_config().scheduler_config.max_num_batched_tokens
  863. # Keep lookup results alive across breakable graph segments.
  864. self.staged_rows = torch.empty(
  865. max_tokens,
  866. self.embed_tokens.part_n_hash_cols,
  867. layout.head_dim,
  868. dtype=torch.bfloat16,
  869. )
  870. def prepare_embeddings(self, hash_ids: torch.Tensor) -> None:
  871. """Gather this layer's rows on the main stream before decoder layers."""
  872. self.embed_tokens.lookup(hash_ids, self.staged_rows[: hash_ids.shape[0]])
  873. def embed(self, hash_ids: torch.Tensor) -> torch.Tensor:
  874. """Gather heads, returning only local tokens when SP is enabled."""
  875. rows = self.staged_rows[: hash_ids.shape[0]]
  876. if self.embed_tokens.tp_size == 1:
  877. return rows
  878. if self.use_sequence_parallel:
  879. tp_size = self.embed_tokens.tp_size
  880. num_tokens, local_heads, dim = rows.shape
  881. gathered = tensor_model_parallel_all_gather(rows, dim=0)
  882. chunk = (num_tokens + tp_size - 1) // tp_size
  883. rows = rows.new_empty((chunk, self.embed_tokens.n_hash_cols, dim))
  884. _engram_sp_rows_kernel[(triton.cdiv(rows.numel(), 1024),)](
  885. gathered,
  886. rows,
  887. num_tokens,
  888. get_tensor_model_parallel_rank() * chunk,
  889. rows.numel(),
  890. local_heads * dim,
  891. self.embed_tokens.n_hash_cols * dim,
  892. BLOCK_SIZE=1024,
  893. )
  894. return rows
  895. rows = tensor_model_parallel_all_gather(rows, dim=1)
  896. return rows[:, : self.embed_tokens.n_hash_cols]
  897. def forward(
  898. self,
  899. hidden_states: torch.Tensor,
  900. hash_ids: torch.Tensor,
  901. token_mask: torch.Tensor | None = None,
  902. ) -> torch.Tensor:
  903. """hidden_states: [T, hc_mult, dim]; hash_ids: [T, n_hash_cols] (all
  904. tokens, pre sequence-parallel shard); token_mask: [T], False shuts
  905. the gate so those positions pass through untouched."""
  906. kv = self.wkv(self.embed(hash_ids).flatten(-2))
  907. num_kv_tokens = hash_ids.shape[0]
  908. assert token_mask is None or token_mask.shape == (num_kv_tokens,)
  909. if self.use_sequence_parallel:
  910. tp_size = get_tensor_model_parallel_world_size()
  911. tp_rank = get_tensor_model_parallel_rank()
  912. shard_size = (num_kv_tokens + tp_size - 1) // tp_size
  913. assert hidden_states.shape[0] == shard_size
  914. start = min(tp_rank * shard_size, num_kv_tokens)
  915. num_kv_tokens = min(shard_size, num_kv_tokens - start)
  916. if token_mask is not None:
  917. token_mask = token_mask[start : start + num_kv_tokens]
  918. num_tokens, hc_mult, dim = hidden_states.shape
  919. assert hc_mult == self.hc_mult and dim == self.dim
  920. assert kv.ndim == 2 and kv.shape[1] == (hc_mult + 1) * dim
  921. output = torch.empty_like(hidden_states)
  922. if num_tokens == 0:
  923. return output
  924. block_size = triton.next_power_of_2(dim)
  925. num_warps = 8 if block_size >= 2048 else 4
  926. mask = token_mask if token_mask is not None else hidden_states
  927. _fused_engram_post_wkv_kernel[(num_tokens * hc_mult,)](
  928. hidden_states,
  929. kv,
  930. self.q_weight,
  931. self.k_weight,
  932. mask,
  933. output,
  934. num_kv_tokens,
  935. hidden_states.stride(0),
  936. hidden_states.stride(1),
  937. hidden_states.stride(2),
  938. kv.stride(0),
  939. kv.stride(1),
  940. self.q_weight.stride(0),
  941. self.q_weight.stride(1),
  942. self.k_weight.stride(0),
  943. self.k_weight.stride(1),
  944. token_mask.stride(0) if token_mask is not None else 0,
  945. output.stride(0),
  946. output.stride(1),
  947. output.stride(2),
  948. self.eps,
  949. self.clamp_value,
  950. DIM=dim,
  951. HC_MULT=hc_mult,
  952. BLOCK_SIZE=block_size,
  953. HAS_MASK=token_mask is not None,
  954. num_warps=num_warps,
  955. )
  956. return output