engram.py 69 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724
  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. # Stage 6: gather rows through a read-only memory map (one NumPy fancy-index copy
  585. # per chunk) instead of one preadv syscall per row.
  586. _KAI_MMAP = _kai_os.environ.get("DSV41_ENGRAM_MMAP", "0") == "1"
  587. # The gather releases the GIL, so more threads = more page faults in flight
  588. # (3 of 4 ranks fault rows in over NFS; each cold row is one round trip).
  589. _KAI_MMAP_THREADS = int(_kai_os.environ.get("DSV41_ENGRAM_MMAP_THREADS", "96"))
  590. # Stage 8: hash-ahead prefetch of the next prefill chunk's rows into the page
  591. # cache while the current chunk computes (the prompt is known up front).
  592. _KAI_PREFETCH = _kai_os.environ.get("DSV41_ENGRAM_PREFETCH", "0") == "1"
  593. _KAI_PREFETCH_MODE = _kai_os.environ.get("DSV41_ENGRAM_PREFETCH_MODE", "fadvise") # fadvise | pread
  594. _KAI_PREFETCH_THREADS = int(_kai_os.environ.get("DSV41_ENGRAM_PREFETCH_THREADS", "32"))
  595. _KAI_PREFETCH_TASK_ROWS = 512
  596. # Gathers smaller than this many unique rows (per table) keep the preadv path
  597. # (0 = memmap for every size). Lets decode steps (72 rows) and prefill chunks
  598. # (tens of thousands) use different paths.
  599. _KAI_MMAP_MIN_ROWS = int(_kai_os.environ.get("DSV41_ENGRAM_MMAP_MIN_ROWS", "0"))
  600. # v3 (window 8: 72 decode rows cost 5-6 ms = one NVMe latency per row, serialized by
  601. # the per-task faults): gathers up to this many rows skip the pool -- first an async
  602. # readahead of every row page (posix_fadvise WILLNEED, ~2 us each, all reads in
  603. # flight together), then ONE fancy-index gather per table on the calling thread.
  604. _KAI_MMAP_INLINE_ROWS = int(_kai_os.environ.get("DSV41_ENGRAM_MMAP_INLINE_ROWS", "2048"))
  605. def _kai_pool() -> _KaiPool:
  606. global _KAI_POOL
  607. if _KAI_POOL is None:
  608. _KAI_POOL = _KaiPool(
  609. max_workers=_KAI_MMAP_THREADS if _KAI_MMAP else _KAI_THREADS,
  610. thread_name_prefix="engram-disk",
  611. )
  612. return _KAI_POOL
  613. def _kai_prefetch_rows(table, rows: list) -> None:
  614. """Warm rank-local rows `rows` of `table` in the page cache: weight rows by
  615. exact byte range, scale rows by 4 KiB page (32 rows share one)."""
  616. if not rows:
  617. return
  618. dim, sb = table.dim, table.sb
  619. if _KAI_PREFETCH_MODE == "pread":
  620. buf = bytearray(dim)
  621. for r in rows:
  622. try:
  623. _kai_os.preadv(table.w_fd, [buf], table.w_off + r * dim)
  624. except OSError:
  625. pass
  626. sbuf = bytearray(4096)
  627. for p in {(table.s_off + r * sb) >> 12 for r in rows}:
  628. try:
  629. _kai_os.preadv(table.s_fd, [sbuf], p << 12)
  630. except OSError:
  631. pass
  632. return
  633. adv = _kai_os.POSIX_FADV_WILLNEED
  634. for r in rows:
  635. try:
  636. _kai_os.posix_fadvise(table.w_fd, table.w_off + r * dim, dim, adv)
  637. except OSError:
  638. pass
  639. for p in {(table.s_off + r * sb) >> 12 for r in rows}:
  640. try:
  641. _kai_os.posix_fadvise(table.s_fd, p << 12, 4096, adv)
  642. except OSError:
  643. pass
  644. def _kai_map_file(path: str):
  645. """Read-only shared mapping of a whole shard with readahead DISABLED
  646. (MADV_RANDOM, the mmap twin of the preadv path's POSIX_FADV_RANDOM): a
  647. fault brings exactly the 4 KiB page of the row. Window 7 (2026-09-11)
  648. showed why: without it every cold-row fault pulled an NFS readahead window
  649. (rsize 1 MiB) — 9K prefill 14.7 s vs 8.1 s, code decode 53 vs 61 tok/s."""
  650. import mmap as _mmap
  651. import numpy as _np
  652. fd = _kai_os.open(path, _kai_os.O_RDONLY)
  653. try:
  654. mm = _mmap.mmap(fd, 0, access=_mmap.ACCESS_READ)
  655. finally:
  656. _kai_os.close(fd)
  657. try:
  658. mm.madvise(_mmap.MADV_RANDOM)
  659. except (AttributeError, OSError): # pragma: no cover - old kernels
  660. pass
  661. return mm, _np.frombuffer(mm, dtype=_np.uint8)
  662. def _kai_mmap_rows(view, rel, lo: int, hi: int, row_bytes: int, buf) -> None:
  663. """Copy rows rel[lo:hi] of the [num_rows, row_bytes] memmap view into buf
  664. (a flat uint8 memoryview). One fancy-index gather: C memcpy for resident
  665. pages, page faults for cold ones; the GIL is released inside NumPy."""
  666. import numpy as _np
  667. dst = _np.frombuffer(buf, dtype=_np.uint8)[lo * row_bytes : hi * row_bytes].reshape(
  668. hi - lo, row_bytes
  669. )
  670. _np.take(view, _np.asarray(rel[lo:hi], dtype=_np.int64), axis=0, out=dst)
  671. def _kai_pread_rows(
  672. fd: int, base: int, rel: list, lo: int, hi: int, row_bytes: int, buf
  673. ) -> None:
  674. for i in range(lo, hi):
  675. off = base + rel[i] * row_bytes
  676. view = buf[i * row_bytes : (i + 1) * row_bytes]
  677. got = 0
  678. while got < row_bytes:
  679. n = _kai_os.preadv(fd, [view[got:]], off + got)
  680. if n <= 0:
  681. raise OSError("engram disk table: short read")
  682. got += n
  683. def _kai_parallel_read(jobs: list) -> None:
  684. """jobs: [(fd, base, rel_list, row_bytes, memoryview)]. Every row of every job
  685. is in flight at once: a task carries ceil(total / threads) rows (1 row at
  686. decode sizes), capped at DSV41_ENGRAM_DISK_CHUNK for prefill-sized batches.
  687. Only the calling thread submits, so the shared pool cannot deadlock."""
  688. total = sum(len(job[2]) for job in jobs)
  689. if total == 0:
  690. return
  691. def _run(job, lo, hi):
  692. fd, base, rel, row_bytes, buf = job[:5]
  693. view = job[5] if len(job) > 5 else None
  694. if view is not None:
  695. _kai_mmap_rows(view, rel, lo, hi, row_bytes, buf)
  696. else:
  697. _kai_pread_rows(fd, base, rel, lo, hi, row_bytes, buf)
  698. if total == 1:
  699. for job in jobs:
  700. _run(job, 0, len(job[2]))
  701. return
  702. # Memmap gathers: spread the rows over every pool thread (cold rows fault
  703. # in sequentially within a task, so more tasks = more reads in flight).
  704. if any(len(job) > 5 and job[5] is not None for job in jobs):
  705. if total <= _KAI_MMAP_INLINE_ROWS and all(len(job) > 5 and job[5] is not None for job in jobs):
  706. adv = _kai_os.POSIX_FADV_WILLNEED
  707. for fd, base, rel, row_bytes, buf, view in jobs:
  708. for r in rel:
  709. try:
  710. _kai_os.posix_fadvise(fd, base + r * row_bytes, row_bytes, adv)
  711. except OSError:
  712. pass
  713. for fd, base, rel, row_bytes, buf, view in jobs:
  714. _kai_mmap_rows(view, rel, 0, len(rel), row_bytes, buf)
  715. return
  716. chunk = max(1, -(-total // _KAI_MMAP_THREADS))
  717. else:
  718. chunk = max(1, min(_KAI_CHUNK, -(-total // _KAI_THREADS)))
  719. pool = _kai_pool()
  720. futs = [
  721. pool.submit(_run, job, lo, min(lo + chunk, len(job[2])))
  722. for job in jobs
  723. for lo in range(0, len(job[2]), chunk)
  724. ]
  725. for fut in futs:
  726. fut.result()
  727. def _dsv41_engram_local_dir(model_dir: str, layer_id: int, row_start: int, num_rows) -> str:
  728. """Tech2Wild 2026-09-10: node-local Engram rows. DSV41_ENGRAM_DIR points at a sparse copy
  729. of the Engram shards holding only this rank's rows at the original byte offsets, plus
  730. engram-local.json with the copied row range per layer (tools/engram_local.py). It is used
  731. only when that range covers this table's rows; otherwise the table reads model_dir as before,
  732. so a changed rank map can never read the copy's empty holes."""
  733. local = _kai_os.environ.get("DSV41_ENGRAM_DIR", "")
  734. if not local:
  735. return model_dir
  736. try:
  737. with open(_kai_os.path.join(local, "engram-local.json")) as f:
  738. lo, hi = _kai_json.load(f)["layers"][str(layer_id)]
  739. except Exception as e: # noqa: BLE001
  740. logger.warning("DSV41_ENGRAM_DIR=%s unusable for layer %d (%s); reading %s",
  741. local, layer_id, e, model_dir)
  742. return model_dir
  743. if num_rows is None or not (lo <= row_start and row_start + num_rows <= hi):
  744. logger.warning("DSV41_ENGRAM_DIR=%s holds layer %d rows [%d, %d), this rank needs "
  745. "[%d, +%s); reading %s", local, layer_id, lo, hi, row_start, num_rows, model_dir)
  746. return model_dir
  747. logger.info("Engram layer %d rows [%d, %d) read from node-local %s",
  748. layer_id, row_start, row_start + num_rows, local)
  749. return local
  750. class DiskEngramTable:
  751. """Tech2Wild/Kai 2026-09-10: read Engram rows straight from the safetensors
  752. shards with positional preads on a thread pool, so the per-rank Engram
  753. shard (~47 GiB at TP4) never occupies memory. Built for DGX Spark, where
  754. "pinned host memory" is the same 128 GB pool the GPU uses. Rows are
  755. dequantized on the CPU (fp8 e4m3 x ue8m0 block scales -> bf16) and copied
  756. into the GPU staging buffer. Enable with DSV41_ENGRAM_DISK=1.
  757. Lineage: our own PLE-on-disk patch for Qwen3.8-Flash-Next (2026-09-05).
  758. """
  759. def __init__(
  760. self,
  761. model_dir: str,
  762. layer_id: int,
  763. dim: int,
  764. block_size: int,
  765. row_start: int = 0,
  766. num_rows: int | None = None,
  767. ):
  768. model_dir = _dsv41_engram_local_dir(model_dir, layer_id, row_start, num_rows)
  769. idx_path = _kai_os.path.join(model_dir, "model.safetensors.index.json")
  770. with open(idx_path) as f:
  771. weight_map = _kai_json.load(f)["weight_map"]
  772. wname = f"layers.{layer_id}.engram.embed.weight"
  773. sname = f"layers.{layer_id}.engram.embed.scale"
  774. self.w_fd, self.w_off, self.w_shape = self._open(model_dir, weight_map[wname], wname)
  775. self.s_fd, self.s_off, self.s_shape = self._open(model_dir, weight_map[sname], sname)
  776. self._w_path = _kai_os.path.join(model_dir, weight_map[wname])
  777. self._s_path = _kai_os.path.join(model_dir, weight_map[sname])
  778. self.dim = dim
  779. self.sb = dim // block_size
  780. assert self.w_shape[1] == dim, (self.w_shape, dim)
  781. assert self.s_shape[1] == self.sb, (self.s_shape, self.sb)
  782. assert self.s_shape[0] == self.w_shape[0], (self.s_shape, self.w_shape)
  783. # The checkpoint holds the FULL table (every TP rank's hash heads) per layer,
  784. # while callers pass rank-local row ids (global - vocab_start), so reads must
  785. # start at this rank's first row.
  786. if num_rows is None:
  787. num_rows = self.w_shape[0] - row_start
  788. assert 0 <= row_start and row_start + num_rows <= self.w_shape[0], (
  789. row_start, num_rows, self.w_shape)
  790. self.w_off += row_start * dim
  791. self.s_off += row_start * self.sb
  792. self.row_start = row_start
  793. self.num_rows = num_rows
  794. # Stage 6: [num_rows, row_bytes] uint8 views of this rank's rows through a
  795. # read-only memory map of each shard (opened once; pages come and go
  796. # with the OS page cache, nothing is pinned).
  797. self.w_view = self.s_view = None
  798. if _KAI_MMAP:
  799. import numpy as _np
  800. self._w_mm, w_buf = _kai_map_file(self._w_path)
  801. self._s_mm, s_buf = _kai_map_file(self._s_path)
  802. self.w_view = _np.ndarray(
  803. (num_rows, dim), dtype=_np.uint8, buffer=w_buf, offset=self.w_off
  804. )
  805. self.s_view = _np.ndarray(
  806. (num_rows, self.sb), dtype=_np.uint8, buffer=s_buf, offset=self.s_off
  807. )
  808. self.threads = _KAI_THREADS
  809. self.chunk = _KAI_CHUNK
  810. self.pool = _kai_pool() # shared by all tables (was one pool per table)
  811. logger.info(
  812. "Engram DISK mode: layer %d rows [%d, %d) read from %s (off=%d) and %s (off=%d); "
  813. "%d threads, chunk %d",
  814. layer_id, row_start, row_start + num_rows, weight_map[wname], self.w_off,
  815. weight_map[sname], self.s_off, self.threads, self.chunk,
  816. )
  817. @staticmethod
  818. def _open(model_dir: str, fname: str, tname: str):
  819. path = _kai_os.path.join(model_dir, fname)
  820. fd = _kai_os.open(path, _kai_os.O_RDONLY)
  821. try:
  822. _kai_os.posix_fadvise(fd, 0, 0, _kai_os.POSIX_FADV_RANDOM)
  823. except Exception: # noqa: BLE001
  824. pass
  825. with open(path, "rb") as f:
  826. n = _kai_struct.unpack("<Q", f.read(8))[0]
  827. hdr = _kai_json.loads(f.read(n))
  828. meta = hdr[tname]
  829. start = meta["data_offsets"][0]
  830. return fd, 8 + n + start, tuple(meta["shape"])
  831. def _read_rows(self, fd: int, base: int, rel: list, row_bytes: int, buf) -> None:
  832. _kai_parallel_read([(fd, base, rel, row_bytes, buf)])
  833. def read_jobs(self, rel_l: list, w: torch.Tensor, s: torch.Tensor) -> list:
  834. """Read jobs for rank-local rows `rel_l` into uint8 w [R, dim], s [R, sb]."""
  835. if self.w_view is not None and len(rel_l) >= _KAI_MMAP_MIN_ROWS:
  836. return [
  837. (self.w_fd, self.w_off, rel_l, self.dim, memoryview(w.numpy()).cast("B"), self.w_view),
  838. (self.s_fd, self.s_off, rel_l, self.sb, memoryview(s.numpy()).cast("B"), self.s_view),
  839. ]
  840. return [
  841. (self.w_fd, self.w_off, rel_l, self.dim, memoryview(w.numpy()).cast("B")),
  842. (self.s_fd, self.s_off, rel_l, self.sb, memoryview(s.numpy()).cast("B")),
  843. ]
  844. def dequant(self, w: torch.Tensor, s: torch.Tensor) -> torch.Tensor:
  845. """fp8 e4m3 rows (as uint8) x ue8m0 block scales -> [R, dim] fp32."""
  846. R = w.shape[0]
  847. vals = w.view(torch.float8_e4m3fn).to(torch.float32).view(
  848. R, self.sb, self.dim // self.sb
  849. )
  850. # ue8m0 byte is the fp32 exponent field: 2^(e-127)
  851. scale = (s.to(torch.int32) << 23).view(torch.float32)
  852. return (vals * scale[:, :, None]).reshape(R, self.dim)
  853. def gather_dequant(self, rel: torch.Tensor, owned: torch.Tensor) -> torch.Tensor:
  854. """rel: [R] int64 CPU local row ids; owned: [R] bool. Returns [R, dim] bf16 CPU."""
  855. return gather_dequant_many([(self, rel, owned)])[0]
  856. def gather_dequant_many(requests: list) -> list[torch.Tensor]:
  857. """Tech2Wild/Kai 2026-09-10: requests = [(DiskEngramTable, rel [R] int64 CPU,
  858. owned [R] bool)] -> one [R, dim] bf16 CPU tensor per request. Rows are
  859. de-duplicated per table (repeated n-grams, zero-filled dummy batches) and the
  860. reads of every table go out in ONE parallel batch. Same math as before."""
  861. plans, jobs = [], []
  862. for table, rel, owned in requests:
  863. uniq, inverse = torch.unique(rel, return_inverse=True)
  864. w = torch.empty((uniq.numel(), table.dim), dtype=torch.uint8)
  865. s = torch.empty((uniq.numel(), table.sb), dtype=torch.uint8)
  866. jobs += table.read_jobs(uniq.tolist(), w, s)
  867. plans.append((table, w, s, inverse, owned))
  868. _kai_parallel_read(jobs)
  869. outs = []
  870. for table, w, s, inverse, owned in plans:
  871. out = table.dequant(w, s)[inverse]
  872. out[~owned] = 0
  873. outs.append(out.to(torch.bfloat16))
  874. return outs
  875. class ParallelEngramEmbedding(nn.Module):
  876. """The n-gram hash table, sharded by complete hash heads over TP ranks.
  877. Rows stay fp8 and are dequantized with ue8m0 per-32 scales on lookup.
  878. With `cpu_offload` the shard lives in pinned host memory and is read over
  879. UVA instead of HBM; the TP sharding is unchanged either way.
  880. """
  881. def __init__(
  882. self,
  883. num_embeddings: int,
  884. dim: int,
  885. head_sizes: tuple[int, ...],
  886. block_size: int = 32,
  887. cpu_offload: bool = False,
  888. model_dir: str | None = None,
  889. layer_id: int | None = None,
  890. ):
  891. super().__init__()
  892. self.disk: DiskEngramTable | None = None
  893. if _DSV41_ENGRAM_DISK:
  894. cpu_offload = False
  895. tp_size = get_tensor_model_parallel_world_size()
  896. tp_rank = get_tensor_model_parallel_rank()
  897. assert head_sizes and all(size > 0 for size in head_sizes)
  898. assert sum(head_sizes) <= num_embeddings
  899. if cpu_offload and not is_uva_available():
  900. raise RuntimeError("Engram CPU offload requires UVA support")
  901. self.num_embeddings = num_embeddings
  902. self.dim = dim
  903. self.block_size = block_size
  904. self.n_hash_cols = len(head_sizes)
  905. self.part_n_hash_cols = triton.cdiv(self.n_hash_cols, tp_size)
  906. self.head_start = tp_rank * self.part_n_hash_cols
  907. head_end = self.head_start + self.part_n_hash_cols
  908. self.vocab_start_idx = sum(head_sizes[: self.head_start])
  909. self.vocab_end_idx = sum(head_sizes[:head_end])
  910. self.part_num_embeddings = self.vocab_end_idx - self.vocab_start_idx
  911. self.tp_size = tp_size
  912. self.cpu_offload = cpu_offload
  913. self._views: tuple[torch.Tensor, torch.Tensor] | None = None
  914. self._view_src: tuple[int, int] | None = None
  915. self._num_sms = torch.cuda.get_device_properties(
  916. torch.accelerator.current_device_index()
  917. ).multi_processor_count
  918. # Explicit device: model init runs under a `torch.device("cuda")`
  919. # context, which would otherwise put the shard in HBM.
  920. kwargs = {"device": "cpu", "pin_memory": True} if cpu_offload else {}
  921. if _DSV41_ENGRAM_DISK:
  922. assert model_dir is not None and layer_id is not None
  923. self.disk = DiskEngramTable(
  924. model_dir, layer_id, dim, block_size,
  925. row_start=self.vocab_start_idx, num_rows=self.part_num_embeddings,
  926. )
  927. # 1-row placeholders as BUFFERS (not Parameters): the checkpoint
  928. # rows are skipped by the weights iterator and must not trip the
  929. # "weights not initialized" check.
  930. self.register_buffer("weight", torch.zeros(1, dim, dtype=torch.float8_e4m3fn, device="cpu"), persistent=False)
  931. self.register_buffer("weight_scale_inv", torch.zeros(1, dim // block_size, dtype=torch.uint8, device="cpu"), persistent=False)
  932. logger.info(
  933. "Engram table DISK-backed: %d rows x %d per rank stay on disk (%.2f GiB not allocated)",
  934. self.part_num_embeddings, dim,
  935. self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
  936. )
  937. return
  938. self.weight = nn.Parameter(
  939. torch.empty(
  940. self.part_num_embeddings, dim, dtype=torch.float8_e4m3fn, **kwargs
  941. ),
  942. requires_grad=False,
  943. )
  944. self.weight_scale_inv = nn.Parameter(
  945. torch.empty(
  946. self.part_num_embeddings,
  947. dim // block_size,
  948. dtype=torch.uint8,
  949. **kwargs,
  950. ),
  951. requires_grad=False,
  952. )
  953. for param in (self.weight, self.weight_scale_inv):
  954. set_weight_attrs(
  955. param,
  956. {
  957. "weight_loader": _engram_head_shard_weight_loader,
  958. "engram_vocab_start": self.vocab_start_idx,
  959. },
  960. )
  961. if cpu_offload:
  962. logger.info(
  963. "Engram table offloaded to pinned host memory: %d rows x %d, "
  964. "%.2f GiB per rank",
  965. self.part_num_embeddings,
  966. dim,
  967. self.part_num_embeddings * (dim + dim // block_size) / 1024**3,
  968. )
  969. def _storage(self) -> tuple[torch.Tensor, torch.Tensor]:
  970. """Parameters when resident, else cached UVA views of the pinned shard.
  971. Rebuilt if anything swaps `.data`, so a stale device pointer cannot
  972. survive silently.
  973. """
  974. if not self.cpu_offload:
  975. return self.weight.data, self.weight_scale_inv.data
  976. src = (self.weight.data_ptr(), self.weight_scale_inv.data_ptr())
  977. if self._view_src != src:
  978. self._views = (
  979. get_accelerator_view_from_cpu_tensor(self.weight.data),
  980. get_accelerator_view_from_cpu_tensor(self.weight_scale_inv.data),
  981. )
  982. self._view_src = src
  983. assert self._views is not None
  984. return self._views
  985. def lookup(
  986. self, indices: torch.Tensor, out: torch.Tensor, background: bool = False
  987. ) -> None:
  988. """Look up local heads of [T, heads] into [T, local_heads, dim] bf16.
  989. `background` limits the grid to leave SMs for concurrent work.
  990. """
  991. rows = indices.shape[0] * self.part_n_hash_cols
  992. if not rows:
  993. return
  994. if self.disk is not None:
  995. self._disk_lookup(indices, out)
  996. return
  997. weight, scales = self._storage()
  998. # The table dwarfs TLB reach, so a persistent grid near the SM count
  999. # beats one program per row; halve it to leave SMs for the main stream.
  1000. tiles = triton.cdiv(rows, 16)
  1001. grid = min(tiles, self._num_sms // 2 if background else self._num_sms)
  1002. _engram_lookup_kernel[(grid,)](
  1003. weight,
  1004. scales,
  1005. indices,
  1006. out,
  1007. self.vocab_start_idx,
  1008. self.vocab_end_idx,
  1009. rows,
  1010. indices.stride(0),
  1011. indices.stride(1),
  1012. HEAD_START=self.head_start,
  1013. LOCAL_HEADS=self.part_n_hash_cols,
  1014. TOTAL_HEADS=self.n_hash_cols,
  1015. DIM=self.dim,
  1016. QUANT_BLOCK=self.block_size,
  1017. BLOCK_R=16,
  1018. GRID=grid,
  1019. )
  1020. def disk_rel_owned(self, local: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
  1021. """local: [T, <=L] int64 CPU hash ids of this rank's heads -> flat
  1022. (rel [T*L] rank-local row ids, owned [T*L] bool). Padded heads (beyond
  1023. TOTAL_HEADS) and ids outside this rank's range are not owned (zeros)."""
  1024. T = local.shape[0]
  1025. L = self.part_n_hash_cols
  1026. if local.shape[1] < L:
  1027. pad = torch.full((T, L - local.shape[1]), -1, dtype=torch.int64)
  1028. local = torch.cat([local, pad], dim=1)
  1029. rows = local.reshape(-1)
  1030. owned = (rows >= self.vocab_start_idx) & (rows < self.vocab_end_idx)
  1031. rel = torch.where(owned, rows - self.vocab_start_idx, torch.zeros_like(rows))
  1032. return rel, owned
  1033. def _disk_lookup(self, indices: torch.Tensor, out: torch.Tensor) -> None:
  1034. """Host-side gather + dequant for DISK mode, then H2D into `out`
  1035. ([T, local_heads, dim] bf16). Padded heads (beyond TOTAL_HEADS) and
  1036. rows outside this rank's range write zeros, matching the kernel.
  1037. In-forward fallback only (V1 runner / eager). Under the V2 runner the
  1038. rows are staged by EngramDiskStager before the forward instead."""
  1039. if torch.cuda.is_available() and torch.cuda.is_current_stream_capturing():
  1040. raise RuntimeError(
  1041. "Engram DISK lookup reached a CUDA-graph capture: it needs a host "
  1042. "round trip. Stage rows before the forward (EngramDiskStager, V2 "
  1043. "runner + patched nvidia/model_state.py) or run --enforce-eager."
  1044. )
  1045. T = indices.shape[0]
  1046. L = self.part_n_hash_cols
  1047. ids = indices.detach().to("cpu", dtype=torch.int64)
  1048. head_end = min(self.head_start + L, self.n_hash_cols)
  1049. rel, owned = self.disk_rel_owned(ids[:, self.head_start:head_end])
  1050. assert self.disk is not None
  1051. deq = self.disk.gather_dequant(rel, owned)
  1052. out[:T].copy_(deq.view(T, L, self.dim))
  1053. def forward(self, indices: torch.Tensor) -> torch.Tensor:
  1054. """indices: [num_tokens, n_hash_cols] -> [num_tokens, n_hash_cols, dim]
  1055. bf16, gathered from all TP shards."""
  1056. out = torch.empty(
  1057. (indices.shape[0], self.part_n_hash_cols, self.dim),
  1058. dtype=torch.bfloat16,
  1059. device=indices.device,
  1060. )
  1061. self.lookup(indices, out)
  1062. if self.tp_size > 1:
  1063. out = tensor_model_parallel_all_gather(out, dim=1)
  1064. out = out[:, : self.n_hash_cols]
  1065. return out
  1066. @triton.jit(do_not_specialize=["num_kv_tokens"])
  1067. def _fused_engram_post_wkv_kernel(
  1068. hidden_states,
  1069. kv,
  1070. q_weight,
  1071. k_weight,
  1072. token_mask,
  1073. output,
  1074. num_kv_tokens,
  1075. hidden_stride_t,
  1076. hidden_stride_h,
  1077. hidden_stride_d,
  1078. kv_stride_t,
  1079. kv_stride_d,
  1080. q_stride_h,
  1081. q_stride_d,
  1082. k_stride_h,
  1083. k_stride_d,
  1084. mask_stride,
  1085. output_stride_t,
  1086. output_stride_h,
  1087. output_stride_d,
  1088. eps,
  1089. clamp_value,
  1090. DIM: tl.constexpr,
  1091. HC_MULT: tl.constexpr,
  1092. BLOCK_SIZE: tl.constexpr,
  1093. HAS_MASK: tl.constexpr,
  1094. ):
  1095. program_idx = tl.program_id(0)
  1096. token_idx = program_idx // HC_MULT
  1097. hc_idx = program_idx % HC_MULT
  1098. token_idx = token_idx.to(tl.int64)
  1099. source_idx = token_idx
  1100. source_valid = source_idx < num_kv_tokens
  1101. dim_offsets = tl.arange(0, BLOCK_SIZE)
  1102. dim_valid = dim_offsets < DIM
  1103. hidden = tl.load(
  1104. hidden_states
  1105. + token_idx * hidden_stride_t
  1106. + hc_idx * hidden_stride_h
  1107. + dim_offsets * hidden_stride_d,
  1108. mask=dim_valid,
  1109. other=0.0,
  1110. ).to(tl.float32)
  1111. key = tl.load(
  1112. kv + source_idx * kv_stride_t + (hc_idx * DIM + dim_offsets) * kv_stride_d,
  1113. mask=source_valid & dim_valid,
  1114. other=0.0,
  1115. ).to(tl.float32)
  1116. q = tl.load(
  1117. q_weight + hc_idx * q_stride_h + dim_offsets * q_stride_d,
  1118. mask=dim_valid,
  1119. other=0.0,
  1120. ).to(tl.float32)
  1121. k = tl.load(
  1122. k_weight + hc_idx * k_stride_h + dim_offsets * k_stride_d,
  1123. mask=dim_valid,
  1124. other=0.0,
  1125. ).to(tl.float32)
  1126. hidden_rms = tl.rsqrt(tl.sum(hidden * hidden, axis=0) / DIM + eps)
  1127. key_rms = tl.rsqrt(tl.sum(key * key, axis=0) / DIM + eps)
  1128. dot = tl.sum(hidden * q * k * key, axis=0)
  1129. dot *= hidden_rms * key_rms * tl.rsqrt(DIM * 1.0)
  1130. gate_input = tl.sqrt(tl.maximum(tl.abs(dot), clamp_value))
  1131. gate_input = tl.where(dot < 0.0, -gate_input, gate_input)
  1132. gate = tl.sigmoid(gate_input)
  1133. if HAS_MASK:
  1134. active = tl.load(
  1135. token_mask + source_idx * mask_stride,
  1136. mask=source_valid,
  1137. other=0,
  1138. )
  1139. gate = tl.where(active, gate, 0.0)
  1140. value = tl.load(
  1141. kv + source_idx * kv_stride_t + (HC_MULT * DIM + dim_offsets) * kv_stride_d,
  1142. mask=source_valid & dim_valid,
  1143. other=0.0,
  1144. ).to(tl.float32)
  1145. tl.store(
  1146. output
  1147. + token_idx * output_stride_t
  1148. + hc_idx * output_stride_h
  1149. + dim_offsets * output_stride_d,
  1150. hidden + gate * value,
  1151. mask=dim_valid,
  1152. )
  1153. @triton.jit(do_not_specialize=["num_tokens", "token_start", "num_elements"])
  1154. def _engram_sp_rows_kernel(
  1155. gathered,
  1156. output,
  1157. num_tokens,
  1158. token_start,
  1159. num_elements,
  1160. LOCAL_WIDTH: tl.constexpr,
  1161. WIDTH: tl.constexpr,
  1162. BLOCK_SIZE: tl.constexpr,
  1163. ):
  1164. offsets = tl.program_id(0).to(tl.int64) * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
  1165. tokens = token_start + offsets // WIDTH
  1166. cols = offsets % WIDTH
  1167. source = (cols // LOCAL_WIDTH * num_tokens + tokens) * LOCAL_WIDTH
  1168. source += cols % LOCAL_WIDTH
  1169. values = tl.load(
  1170. gathered + source, (offsets < num_elements) & (tokens < num_tokens), other=0
  1171. )
  1172. tl.store(output + offsets, values, offsets < num_elements)
  1173. class Engram(nn.Module):
  1174. """Writes an n-gram lookup into the residual stream, gated by how well it
  1175. matches that stream.
  1176. The hash ids fetch `n_hash_cols` rows; `wkv` turns them into one key per
  1177. hc copy plus a shared value. The gate is a normalized dot product of the
  1178. stream against the key, signed-sqrt'ed before the sigmoid (matching the
  1179. training kernel).
  1180. """
  1181. def __init__(
  1182. self,
  1183. config,
  1184. quant_config: QuantizationConfig | None,
  1185. layout: EngramLayout,
  1186. layer_hash_index: int,
  1187. use_sequence_parallel: bool,
  1188. prefix: str,
  1189. ) -> None:
  1190. super().__init__()
  1191. self.layer_hash_index = layer_hash_index
  1192. self.dim = config.hidden_size
  1193. self.hc_mult = config.hc_mult
  1194. self.eps = config.rms_norm_eps
  1195. self.clamp_value = 1e-6
  1196. self.use_sequence_parallel = use_sequence_parallel
  1197. # Named ``embed_tokens`` so the checkpoint's ``engram.embed.weight``
  1198. # survives the mapper's ``embed.weight`` -> ``embed_tokens.weight``
  1199. # suffix rule.
  1200. engram_config = get_current_vllm_config().engram_config
  1201. self.embed_tokens = ParallelEngramEmbedding(
  1202. layout.num_embeddings[layer_hash_index],
  1203. layout.head_dim,
  1204. tuple(size for order in layout.primes[layer_hash_index] for size in order),
  1205. cpu_offload=engram_config.cpu_offload if engram_config else True,
  1206. model_dir=get_current_vllm_config().model_config.model,
  1207. layer_id=layout.layer_ids[layer_hash_index],
  1208. )
  1209. n_hash_cols = (layout.max_ngram_size - 1) * layout.n_heads
  1210. self.wkv = ReplicatedLinear(
  1211. n_hash_cols * layout.head_dim,
  1212. self.dim * (self.hc_mult + 1),
  1213. bias=False,
  1214. quant_config=quant_config,
  1215. return_bias=False,
  1216. prefix=f"{prefix}.wkv",
  1217. )
  1218. self.q_weight = nn.Parameter(
  1219. torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
  1220. requires_grad=False,
  1221. )
  1222. self.k_weight = nn.Parameter(
  1223. torch.empty(self.hc_mult, self.dim, dtype=torch.bfloat16),
  1224. requires_grad=False,
  1225. )
  1226. max_tokens = get_current_vllm_config().scheduler_config.max_num_batched_tokens
  1227. # Tech2Wild/Kai 2026-09-10 (CUDA graphs): under the V2 runner a DISK
  1228. # table is staged by the model state (EngramDiskStager) BEFORE the
  1229. # forward, so the forward holds no host round trip and can be captured
  1230. # (FULL decode graphs included). The V1 runner keeps the in-forward path.
  1231. self.prestage = self.embed_tokens.disk is not None and bool(
  1232. getattr(get_current_vllm_config(), "use_v2_model_runner", False)
  1233. )
  1234. # Keep lookup results alive across breakable graph segments. Zeroed so
  1235. # rows of cudagraph padding tokens (never staged) stay finite.
  1236. self.staged_rows = torch.zeros(
  1237. max_tokens,
  1238. self.embed_tokens.part_n_hash_cols,
  1239. layout.head_dim,
  1240. dtype=torch.bfloat16,
  1241. )
  1242. def prepare_embeddings(self, hash_ids: torch.Tensor) -> None:
  1243. """Gather this layer's rows on the main stream before decoder layers."""
  1244. if self.prestage:
  1245. # Already staged for this step by EngramDiskStager.stage().
  1246. return
  1247. self.embed_tokens.lookup(hash_ids, self.staged_rows[: hash_ids.shape[0]])
  1248. def embed(self, hash_ids: torch.Tensor) -> torch.Tensor:
  1249. """Gather heads, returning only local tokens when SP is enabled."""
  1250. rows = self.staged_rows[: hash_ids.shape[0]]
  1251. if self.embed_tokens.tp_size == 1:
  1252. return rows
  1253. if self.use_sequence_parallel:
  1254. tp_size = self.embed_tokens.tp_size
  1255. num_tokens, local_heads, dim = rows.shape
  1256. gathered = tensor_model_parallel_all_gather(rows, dim=0)
  1257. chunk = (num_tokens + tp_size - 1) // tp_size
  1258. rows = rows.new_empty((chunk, self.embed_tokens.n_hash_cols, dim))
  1259. _engram_sp_rows_kernel[(triton.cdiv(rows.numel(), 1024),)](
  1260. gathered,
  1261. rows,
  1262. num_tokens,
  1263. get_tensor_model_parallel_rank() * chunk,
  1264. rows.numel(),
  1265. local_heads * dim,
  1266. self.embed_tokens.n_hash_cols * dim,
  1267. BLOCK_SIZE=1024,
  1268. )
  1269. return rows
  1270. rows = tensor_model_parallel_all_gather(rows, dim=1)
  1271. return rows[:, : self.embed_tokens.n_hash_cols]
  1272. def forward(
  1273. self,
  1274. hidden_states: torch.Tensor,
  1275. hash_ids: torch.Tensor,
  1276. token_mask: torch.Tensor | None = None,
  1277. ) -> torch.Tensor:
  1278. # Stage 7: phase "engram_fwd" of the prefill budget (inert unless enabled).
  1279. from vllm.model_executor.kernels.attention.dsa.candidate_blocks import _PrefillTimer
  1280. with _PrefillTimer("engram_fwd", int(hash_ids.shape[0])):
  1281. return self._forward_impl(hidden_states, hash_ids, token_mask)
  1282. def _forward_impl(
  1283. self,
  1284. hidden_states: torch.Tensor,
  1285. hash_ids: torch.Tensor,
  1286. token_mask: torch.Tensor | None = None,
  1287. ) -> torch.Tensor:
  1288. """hidden_states: [T, hc_mult, dim]; hash_ids: [T, n_hash_cols] (all
  1289. tokens, pre sequence-parallel shard); token_mask: [T], False shuts
  1290. the gate so those positions pass through untouched."""
  1291. kv = self.wkv(self.embed(hash_ids).flatten(-2))
  1292. num_kv_tokens = hash_ids.shape[0]
  1293. assert token_mask is None or token_mask.shape == (num_kv_tokens,)
  1294. if self.use_sequence_parallel:
  1295. tp_size = get_tensor_model_parallel_world_size()
  1296. tp_rank = get_tensor_model_parallel_rank()
  1297. shard_size = (num_kv_tokens + tp_size - 1) // tp_size
  1298. assert hidden_states.shape[0] == shard_size
  1299. start = min(tp_rank * shard_size, num_kv_tokens)
  1300. num_kv_tokens = min(shard_size, num_kv_tokens - start)
  1301. if token_mask is not None:
  1302. token_mask = token_mask[start : start + num_kv_tokens]
  1303. num_tokens, hc_mult, dim = hidden_states.shape
  1304. assert hc_mult == self.hc_mult and dim == self.dim
  1305. assert kv.ndim == 2 and kv.shape[1] == (hc_mult + 1) * dim
  1306. output = torch.empty_like(hidden_states)
  1307. if num_tokens == 0:
  1308. return output
  1309. block_size = triton.next_power_of_2(dim)
  1310. num_warps = 8 if block_size >= 2048 else 4
  1311. mask = token_mask if token_mask is not None else hidden_states
  1312. _fused_engram_post_wkv_kernel[(num_tokens * hc_mult,)](
  1313. hidden_states,
  1314. kv,
  1315. self.q_weight,
  1316. self.k_weight,
  1317. mask,
  1318. output,
  1319. num_kv_tokens,
  1320. hidden_states.stride(0),
  1321. hidden_states.stride(1),
  1322. hidden_states.stride(2),
  1323. kv.stride(0),
  1324. kv.stride(1),
  1325. self.q_weight.stride(0),
  1326. self.q_weight.stride(1),
  1327. self.k_weight.stride(0),
  1328. self.k_weight.stride(1),
  1329. token_mask.stride(0) if token_mask is not None else 0,
  1330. output.stride(0),
  1331. output.stride(1),
  1332. output.stride(2),
  1333. self.eps,
  1334. self.clamp_value,
  1335. DIM=dim,
  1336. HC_MULT=hc_mult,
  1337. BLOCK_SIZE=block_size,
  1338. HAS_MASK=token_mask is not None,
  1339. num_warps=num_warps,
  1340. )
  1341. return output
  1342. class EngramDiskStager:
  1343. """Tech2Wild/Kai 2026-09-10 (CUDA graphs): stage DISK-mode Engram rows
  1344. OUTSIDE the model forward.
  1345. The V2 model state calls :meth:`stage` from ``prepare_inputs`` once per
  1346. step, after the runner has written this step's input ids, positions and
  1347. query_start_loc (all on the current stream). It hashes the step on the GPU
  1348. (same kernel, same inputs as the forward), copies only this rank's hash
  1349. columns to pinned host memory, waits on ONE event, reads every row of every
  1350. engram layer in one parallel batch, dequantizes on the CPU exactly like the
  1351. in-forward path, and copies (async, from pinned memory) into each layer's
  1352. persistent ``staged_rows``. The forward (eager, breakable PIECEWISE or FULL
  1353. graph) only reads ``staged_rows``, so it never needs the host.
  1354. """
  1355. def __init__(self, hash_state: NgramHashState, engrams: list["Engram"]) -> None:
  1356. assert engrams and all(e.embed_tokens.disk is not None for e in engrams)
  1357. self.hash_state = hash_state
  1358. self.engrams = sorted(engrams, key=lambda e: e.layer_hash_index)
  1359. emb = self.engrams[0].embed_tokens
  1360. self.local_heads = emb.part_n_hash_cols
  1361. self.head_start = emb.head_start
  1362. self.head_end = min(emb.head_start + emb.part_n_hash_cols, emb.n_hash_cols)
  1363. self.dim = emb.dim
  1364. self.max_tokens = self.engrams[0].staged_rows.shape[0]
  1365. num_layers = hash_state.multipliers.shape[0]
  1366. self.hash_host = torch.empty(
  1367. (self.max_tokens, num_layers, self.head_end - self.head_start),
  1368. dtype=torch.int32,
  1369. device="cpu",
  1370. pin_memory=True,
  1371. )
  1372. self.rows_host = [
  1373. torch.empty(
  1374. (self.max_tokens, self.local_heads, self.dim),
  1375. dtype=torch.bfloat16,
  1376. device="cpu",
  1377. pin_memory=True,
  1378. )
  1379. for _ in self.engrams
  1380. ]
  1381. self.hashes_ready = torch.cuda.Event()
  1382. self.num_staged = 0
  1383. # DSV41_ENGRAM_TIMING=1: host wall-clock of stage() phases (diagnostic).
  1384. self._timing = _kai_os.environ.get("DSV41_ENGRAM_TIMING", "0") == "1"
  1385. # two buckets (decode: n <= 16 tokens, prefill: n > 16): sync, gather+dequant, tail, total, calls, rows, unique rows
  1386. self._t_acc = [[0.0, 0.0, 0.0, 0.0, 0, 0, 0] for _ in range(2)]
  1387. self._t_last_total_ms = 0.0 # stage 7 reads it after each timed prefill step
  1388. # Stage 8: hash-ahead prefetch state (see _hash_ahead).
  1389. self._prefetch = _KAI_PREFETCH
  1390. if self._prefetch:
  1391. self.hash_host_next = torch.empty_like(self.hash_host)
  1392. self.hashes_next_ready = torch.cuda.Event()
  1393. self.last_pos_host = torch.zeros(256, dtype=torch.int64, device="cpu", pin_memory=True)
  1394. self._prefetch_pool = _KaiPool(
  1395. max_workers=_KAI_PREFETCH_THREADS, thread_name_prefix="engram-prefetch"
  1396. )
  1397. self._prefetch_futs: list = []
  1398. self._prefetch_stats = [0, 0, 0.0] # lookaheads, rows, host seconds
  1399. logger.info(
  1400. "Engram DISK prefetch: hash-ahead of the next prefill chunk (%s, %d threads)",
  1401. _KAI_PREFETCH_MODE, _KAI_PREFETCH_THREADS,
  1402. )
  1403. logger.info(
  1404. "Engram DISK rows staged before the forward (graph-safe): %d layers, "
  1405. "heads [%d, %d) of %d, up to %d tokens/step, %d read threads",
  1406. len(self.engrams),
  1407. self.head_start,
  1408. self.head_end,
  1409. emb.n_hash_cols,
  1410. self.max_tokens,
  1411. _KAI_THREADS,
  1412. )
  1413. def _hash_ahead(self, input_batch, req_states, positions, query_start_loc, lookback_token_ids) -> None:
  1414. """Stage 8: for the batch's longest still-prefilling request, hash the
  1415. NEXT chunk of its (known) prompt with the same kernel and warm this
  1416. rank's rows in the page cache while the current step computes."""
  1417. if torch.cuda.is_current_stream_capturing():
  1418. return
  1419. import time as _kai_time
  1420. from .mm_preprocess import image_sentinel_mask
  1421. t0 = _kai_time.perf_counter()
  1422. n_reqs = int(input_batch.num_reqs)
  1423. if n_reqs <= 0:
  1424. return
  1425. idx_np = input_batch.idx_mapping_np[:n_reqs]
  1426. prompt_len = req_states.prompt_len.np[idx_np]
  1427. next_start = self.last_pos_host[:n_reqs].numpy() + 1 # valid after hashes_ready.synchronize()
  1428. remaining = prompt_len - next_start
  1429. i = int(remaining.argmax())
  1430. if remaining[i] <= 0:
  1431. return
  1432. req_idx = int(idx_np[i])
  1433. s = int(next_start[i])
  1434. L = int(min(remaining[i], self.max_tokens))
  1435. tok = req_states.all_token_ids.gpu
  1436. ids = tok[req_idx, s : s + L]
  1437. pos = torch.arange(s, s + L, dtype=positions.dtype, device=positions.device)
  1438. qsl = torch.tensor([0, L], dtype=query_start_loc.dtype, device=query_start_loc.device)
  1439. depth = int(lookback_token_ids.shape[1])
  1440. lb = torch.full((1, depth), -1, dtype=lookback_token_ids.dtype, device=lookback_token_ids.device)
  1441. k = min(depth, s)
  1442. if k > 0:
  1443. lb[0, :k] = tok[req_idx, s - k : s].flip(0).to(lb.dtype)
  1444. hashes = self.hash_state(
  1445. ids, pos, qsl, image_sentinel_mask(ids), lb, image_sentinel_mask(lb), None, None
  1446. )
  1447. host = self.hash_host_next[:L]
  1448. host.copy_(hashes[:, :, self.head_start : self.head_end], non_blocking=True)
  1449. self.hashes_next_ready.record()
  1450. self.hashes_next_ready.synchronize()
  1451. self._prefetch_futs = [f for f in self._prefetch_futs if not f.done()]
  1452. total_rows = 0
  1453. for engram in self.engrams:
  1454. local = host[:, engram.layer_hash_index, :].to(torch.int64)
  1455. rel, owned = engram.embed_tokens.disk_rel_owned(local)
  1456. rows = torch.unique(rel[owned]).tolist()
  1457. total_rows += len(rows)
  1458. table = engram.embed_tokens.disk
  1459. for lo in range(0, len(rows), _KAI_PREFETCH_TASK_ROWS):
  1460. self._prefetch_futs.append(
  1461. self._prefetch_pool.submit(
  1462. _kai_prefetch_rows, table, rows[lo : lo + _KAI_PREFETCH_TASK_ROWS]
  1463. )
  1464. )
  1465. st = self._prefetch_stats
  1466. st[0] += 1
  1467. st[1] += total_rows
  1468. st[2] += _kai_time.perf_counter() - t0
  1469. if st[0] % 16 == 0 or L >= 4096:
  1470. logger.info(
  1471. "Engram prefetch: next chunk %d tokens from %d (prompt %d), %d unique rows queued, "
  1472. "host %.1f ms (%d lookaheads so far, avg %.0f rows, %.1f ms)",
  1473. L, s, int(prompt_len[i]), total_rows, 1e3 * (_kai_time.perf_counter() - t0),
  1474. st[0], st[1] / st[0], 1e3 * st[2] / st[0],
  1475. )
  1476. @torch.inference_mode()
  1477. def stage(
  1478. self,
  1479. input_ids: torch.Tensor,
  1480. positions: torch.Tensor,
  1481. query_start_loc: torch.Tensor,
  1482. lookback_token_ids: torch.Tensor,
  1483. num_tokens: int,
  1484. input_batch=None,
  1485. req_states=None,
  1486. ) -> int:
  1487. """Stage rows for the first `num_tokens` (unpadded) tokens of this step.
  1488. Returns the number staged; 0 while the KV cache is unbound (profiling),
  1489. matching the forward, which skips engram then."""
  1490. n = min(int(num_tokens), self.max_tokens)
  1491. if n <= 0 or not self.hash_state.ensure_cache():
  1492. return 0
  1493. from .mm_preprocess import image_sentinel_mask
  1494. timing = self._timing
  1495. if timing:
  1496. import time as _kai_time
  1497. _t0 = _kai_time.perf_counter()
  1498. ids = input_ids[:n]
  1499. hashes = self.hash_state(
  1500. ids,
  1501. positions[:n],
  1502. query_start_loc,
  1503. image_sentinel_mask(ids),
  1504. lookback_token_ids,
  1505. image_sentinel_mask(lookback_token_ids),
  1506. None,
  1507. None,
  1508. )
  1509. host = self.hash_host[:n]
  1510. host.copy_(hashes[:, :, self.head_start : self.head_end], non_blocking=True)
  1511. prefetch = self._prefetch and input_batch is not None and req_states is not None
  1512. if prefetch:
  1513. # last position of every request in this step -> next chunk start
  1514. n_reqs = int(input_batch.num_reqs)
  1515. if n_reqs > self.last_pos_host.shape[0]:
  1516. self.last_pos_host = torch.zeros(
  1517. n_reqs * 2, dtype=torch.int64, device="cpu", pin_memory=True
  1518. )
  1519. ends = torch.as_tensor(
  1520. input_batch.query_start_loc_np[1 : n_reqs + 1].astype("int64") - 1,
  1521. device=positions.device,
  1522. )
  1523. self.last_pos_host[:n_reqs].copy_(positions[ends], non_blocking=True)
  1524. self.hashes_ready.record()
  1525. # The one host sync per step. It also orders this step's rewrite of the
  1526. # pinned buffers after the previous step's async H2D copies from them.
  1527. self.hashes_ready.synchronize()
  1528. if timing:
  1529. _t1 = _kai_time.perf_counter()
  1530. requests = []
  1531. for engram in self.engrams:
  1532. local = host[:, engram.layer_hash_index, :].to(torch.int64)
  1533. rel, owned = engram.embed_tokens.disk_rel_owned(local)
  1534. requests.append((engram.embed_tokens.disk, rel, owned))
  1535. rows_per_layer = gather_dequant_many(requests)
  1536. if timing:
  1537. _t2 = _kai_time.perf_counter()
  1538. _acc = self._t_acc[0 if n <= 16 else 1]
  1539. _acc[0] += _t1 - _t0
  1540. _acc[1] += _t2 - _t1
  1541. _acc[4] += 1
  1542. _rows = sum(int(r[1].numel()) for r in requests)
  1543. _uniq = sum(int(torch.unique(r[1]).numel()) for r in requests)
  1544. _acc[5] += _rows
  1545. _acc[6] += _uniq
  1546. # the H2D tail is timed by the caller-visible end of stage(): record
  1547. # the total when the call returns (see the return sites below)
  1548. self._t_mid = _t2
  1549. self._t_last = (_rows, _uniq, _t2 - _t1)
  1550. for engram, rows, buf in zip(self.engrams, rows_per_layer, self.rows_host):
  1551. staged = buf[:n]
  1552. staged.copy_(rows.view(n, self.local_heads, self.dim))
  1553. engram.staged_rows[:n].copy_(staged, non_blocking=True)
  1554. self.num_staged = n
  1555. if timing:
  1556. _t3 = _kai_time.perf_counter()
  1557. _acc = self._t_acc[0 if n <= 16 else 1]
  1558. _acc[2] += _t3 - self._t_mid
  1559. _acc[3] += _t3 - _t0
  1560. self._t_last_total_ms = 1e3 * (_t3 - _t0)
  1561. # stage 6 defines _KAI_MMAP (memmap gather); resolved at call time
  1562. _mode = (
  1563. "mmap"
  1564. if globals().get("_KAI_MMAP", False)
  1565. and self._t_last[0] >= globals().get("_KAI_MMAP_MIN_ROWS", 0) * len(self.engrams)
  1566. else "preadv"
  1567. )
  1568. if n >= 1024:
  1569. logger.info(
  1570. "Engram prefill stage [%s]: %d tokens, %d rows (%d unique), "
  1571. "gather+dequant %.1f ms, total %.1f ms",
  1572. _mode, n, self._t_last[0], self._t_last[1],
  1573. 1e3 * self._t_last[2], 1e3 * (_t3 - _t0),
  1574. )
  1575. if _acc[4] % (200 if n <= 16 else 20) == 0:
  1576. logger.info(
  1577. "Engram stage timing [%s, %s]: total %.2f ms/step (sync wait %.2f, "
  1578. "disk+dequant %.2f, h2d+rest %.2f), %.1f rows/step (%.1f unique), "
  1579. "avg of %d steps",
  1580. _mode, "decode" if n <= 16 else "prefill",
  1581. 1e3 * _acc[3] / _acc[4],
  1582. 1e3 * _acc[0] / _acc[4],
  1583. 1e3 * _acc[1] / _acc[4],
  1584. 1e3 * _acc[2] / _acc[4],
  1585. _acc[5] / _acc[4],
  1586. _acc[6] / _acc[4],
  1587. _acc[4],
  1588. )
  1589. for i in range(7):
  1590. _acc[i] = 0
  1591. if prefetch:
  1592. try:
  1593. self._hash_ahead(input_batch, req_states, positions, query_start_loc, lookback_token_ids)
  1594. except Exception as e: # noqa: BLE001 - prefetch is best effort
  1595. logger.warning_once("Engram prefetch disabled after error: %r", e)
  1596. self._prefetch = False
  1597. return n