07-models_deepseek_v4_1_common_engram.py.diff 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. --- baked-ours/models/deepseek_v4_1/common/engram.py 2026-09-10 20:13:20.000000000 +0200
  2. +++ baked/models/deepseek_v4_1/common/engram.py 2026-09-11 10:13:47.000000000 +0200
  3. @@ -634,15 +634,105 @@
  4. _KAI_THREADS = int(_kai_os.environ.get("DSV41_ENGRAM_DISK_THREADS", "32"))
  5. _KAI_CHUNK = int(_kai_os.environ.get("DSV41_ENGRAM_DISK_CHUNK", "16"))
  6. _KAI_POOL: _KaiPool | None = None
  7. +# Stage 6: gather rows through a read-only memory map (one NumPy fancy-index copy
  8. +# per chunk) instead of one preadv syscall per row.
  9. +_KAI_MMAP = _kai_os.environ.get("DSV41_ENGRAM_MMAP", "0") == "1"
  10. +# The gather releases the GIL, so more threads = more page faults in flight
  11. +# (3 of 4 ranks fault rows in over NFS; each cold row is one round trip).
  12. +_KAI_MMAP_THREADS = int(_kai_os.environ.get("DSV41_ENGRAM_MMAP_THREADS", "96"))
  13. +# Stage 8: hash-ahead prefetch of the next prefill chunk's rows into the page
  14. +# cache while the current chunk computes (the prompt is known up front).
  15. +_KAI_PREFETCH = _kai_os.environ.get("DSV41_ENGRAM_PREFETCH", "0") == "1"
  16. +_KAI_PREFETCH_MODE = _kai_os.environ.get("DSV41_ENGRAM_PREFETCH_MODE", "fadvise") # fadvise | pread
  17. +_KAI_PREFETCH_THREADS = int(_kai_os.environ.get("DSV41_ENGRAM_PREFETCH_THREADS", "32"))
  18. +_KAI_PREFETCH_TASK_ROWS = 512
  19. +# Gathers smaller than this many unique rows (per table) keep the preadv path
  20. +# (0 = memmap for every size). Lets decode steps (72 rows) and prefill chunks
  21. +# (tens of thousands) use different paths.
  22. +_KAI_MMAP_MIN_ROWS = int(_kai_os.environ.get("DSV41_ENGRAM_MMAP_MIN_ROWS", "0"))
  23. +# v3 (window 8: 72 decode rows cost 5-6 ms = one NVMe latency per row, serialized by
  24. +# the per-task faults): gathers up to this many rows skip the pool -- first an async
  25. +# readahead of every row page (posix_fadvise WILLNEED, ~2 us each, all reads in
  26. +# flight together), then ONE fancy-index gather per table on the calling thread.
  27. +_KAI_MMAP_INLINE_ROWS = int(_kai_os.environ.get("DSV41_ENGRAM_MMAP_INLINE_ROWS", "2048"))
  28. def _kai_pool() -> _KaiPool:
  29. global _KAI_POOL
  30. if _KAI_POOL is None:
  31. - _KAI_POOL = _KaiPool(max_workers=_KAI_THREADS, thread_name_prefix="engram-disk")
  32. + _KAI_POOL = _KaiPool(
  33. + max_workers=_KAI_MMAP_THREADS if _KAI_MMAP else _KAI_THREADS,
  34. + thread_name_prefix="engram-disk",
  35. + )
  36. return _KAI_POOL
  37. +def _kai_prefetch_rows(table, rows: list) -> None:
  38. + """Warm rank-local rows `rows` of `table` in the page cache: weight rows by
  39. + exact byte range, scale rows by 4 KiB page (32 rows share one)."""
  40. + if not rows:
  41. + return
  42. + dim, sb = table.dim, table.sb
  43. + if _KAI_PREFETCH_MODE == "pread":
  44. + buf = bytearray(dim)
  45. + for r in rows:
  46. + try:
  47. + _kai_os.preadv(table.w_fd, [buf], table.w_off + r * dim)
  48. + except OSError:
  49. + pass
  50. + sbuf = bytearray(4096)
  51. + for p in {(table.s_off + r * sb) >> 12 for r in rows}:
  52. + try:
  53. + _kai_os.preadv(table.s_fd, [sbuf], p << 12)
  54. + except OSError:
  55. + pass
  56. + return
  57. + adv = _kai_os.POSIX_FADV_WILLNEED
  58. + for r in rows:
  59. + try:
  60. + _kai_os.posix_fadvise(table.w_fd, table.w_off + r * dim, dim, adv)
  61. + except OSError:
  62. + pass
  63. + for p in {(table.s_off + r * sb) >> 12 for r in rows}:
  64. + try:
  65. + _kai_os.posix_fadvise(table.s_fd, p << 12, 4096, adv)
  66. + except OSError:
  67. + pass
  68. +
  69. +
  70. +def _kai_map_file(path: str):
  71. + """Read-only shared mapping of a whole shard with readahead DISABLED
  72. + (MADV_RANDOM, the mmap twin of the preadv path's POSIX_FADV_RANDOM): a
  73. + fault brings exactly the 4 KiB page of the row. Window 7 (2026-09-11)
  74. + showed why: without it every cold-row fault pulled an NFS readahead window
  75. + (rsize 1 MiB) — 9K prefill 14.7 s vs 8.1 s, code decode 53 vs 61 tok/s."""
  76. + import mmap as _mmap
  77. + import numpy as _np
  78. +
  79. + fd = _kai_os.open(path, _kai_os.O_RDONLY)
  80. + try:
  81. + mm = _mmap.mmap(fd, 0, access=_mmap.ACCESS_READ)
  82. + finally:
  83. + _kai_os.close(fd)
  84. + try:
  85. + mm.madvise(_mmap.MADV_RANDOM)
  86. + except (AttributeError, OSError): # pragma: no cover - old kernels
  87. + pass
  88. + return mm, _np.frombuffer(mm, dtype=_np.uint8)
  89. +
  90. +
  91. +def _kai_mmap_rows(view, rel, lo: int, hi: int, row_bytes: int, buf) -> None:
  92. + """Copy rows rel[lo:hi] of the [num_rows, row_bytes] memmap view into buf
  93. + (a flat uint8 memoryview). One fancy-index gather: C memcpy for resident
  94. + pages, page faults for cold ones; the GIL is released inside NumPy."""
  95. + import numpy as _np
  96. +
  97. + dst = _np.frombuffer(buf, dtype=_np.uint8)[lo * row_bytes : hi * row_bytes].reshape(
  98. + hi - lo, row_bytes
  99. + )
  100. + _np.take(view, _np.asarray(rel[lo:hi], dtype=_np.int64), axis=0, out=dst)
  101. +
  102. +
  103. def _kai_pread_rows(
  104. fd: int, base: int, rel: list, lo: int, hi: int, row_bytes: int, buf
  105. ) -> None:
  106. @@ -665,18 +755,41 @@
  107. total = sum(len(job[2]) for job in jobs)
  108. if total == 0:
  109. return
  110. +
  111. + def _run(job, lo, hi):
  112. + fd, base, rel, row_bytes, buf = job[:5]
  113. + view = job[5] if len(job) > 5 else None
  114. + if view is not None:
  115. + _kai_mmap_rows(view, rel, lo, hi, row_bytes, buf)
  116. + else:
  117. + _kai_pread_rows(fd, base, rel, lo, hi, row_bytes, buf)
  118. +
  119. if total == 1:
  120. - for fd, base, rel, row_bytes, buf in jobs:
  121. - _kai_pread_rows(fd, base, rel, 0, len(rel), row_bytes, buf)
  122. + for job in jobs:
  123. + _run(job, 0, len(job[2]))
  124. return
  125. - chunk = max(1, min(_KAI_CHUNK, -(-total // _KAI_THREADS)))
  126. + # Memmap gathers: spread the rows over every pool thread (cold rows fault
  127. + # in sequentially within a task, so more tasks = more reads in flight).
  128. + if any(len(job) > 5 and job[5] is not None for job in jobs):
  129. + if total <= _KAI_MMAP_INLINE_ROWS and all(len(job) > 5 and job[5] is not None for job in jobs):
  130. + adv = _kai_os.POSIX_FADV_WILLNEED
  131. + for fd, base, rel, row_bytes, buf, view in jobs:
  132. + for r in rel:
  133. + try:
  134. + _kai_os.posix_fadvise(fd, base + r * row_bytes, row_bytes, adv)
  135. + except OSError:
  136. + pass
  137. + for fd, base, rel, row_bytes, buf, view in jobs:
  138. + _kai_mmap_rows(view, rel, 0, len(rel), row_bytes, buf)
  139. + return
  140. + chunk = max(1, -(-total // _KAI_MMAP_THREADS))
  141. + else:
  142. + chunk = max(1, min(_KAI_CHUNK, -(-total // _KAI_THREADS)))
  143. pool = _kai_pool()
  144. futs = [
  145. - pool.submit(
  146. - _kai_pread_rows, fd, base, rel, lo, min(lo + chunk, len(rel)), row_bytes, buf
  147. - )
  148. - for fd, base, rel, row_bytes, buf in jobs
  149. - for lo in range(0, len(rel), chunk)
  150. + pool.submit(_run, job, lo, min(lo + chunk, len(job[2])))
  151. + for job in jobs
  152. + for lo in range(0, len(job[2]), chunk)
  153. ]
  154. for fut in futs:
  155. fut.result()
  156. @@ -734,6 +847,8 @@
  157. sname = f"layers.{layer_id}.engram.embed.scale"
  158. self.w_fd, self.w_off, self.w_shape = self._open(model_dir, weight_map[wname], wname)
  159. self.s_fd, self.s_off, self.s_shape = self._open(model_dir, weight_map[sname], sname)
  160. + self._w_path = _kai_os.path.join(model_dir, weight_map[wname])
  161. + self._s_path = _kai_os.path.join(model_dir, weight_map[sname])
  162. self.dim = dim
  163. self.sb = dim // block_size
  164. assert self.w_shape[1] == dim, (self.w_shape, dim)
  165. @@ -750,6 +865,21 @@
  166. self.s_off += row_start * self.sb
  167. self.row_start = row_start
  168. self.num_rows = num_rows
  169. + # Stage 6: [num_rows, row_bytes] uint8 views of this rank's rows through a
  170. + # read-only memory map of each shard (opened once; pages come and go
  171. + # with the OS page cache, nothing is pinned).
  172. + self.w_view = self.s_view = None
  173. + if _KAI_MMAP:
  174. + import numpy as _np
  175. +
  176. + self._w_mm, w_buf = _kai_map_file(self._w_path)
  177. + self._s_mm, s_buf = _kai_map_file(self._s_path)
  178. + self.w_view = _np.ndarray(
  179. + (num_rows, dim), dtype=_np.uint8, buffer=w_buf, offset=self.w_off
  180. + )
  181. + self.s_view = _np.ndarray(
  182. + (num_rows, self.sb), dtype=_np.uint8, buffer=s_buf, offset=self.s_off
  183. + )
  184. self.threads = _KAI_THREADS
  185. self.chunk = _KAI_CHUNK
  186. self.pool = _kai_pool() # shared by all tables (was one pool per table)
  187. @@ -780,6 +910,11 @@
  188. def read_jobs(self, rel_l: list, w: torch.Tensor, s: torch.Tensor) -> list:
  189. """Read jobs for rank-local rows `rel_l` into uint8 w [R, dim], s [R, sb]."""
  190. + if self.w_view is not None and len(rel_l) >= _KAI_MMAP_MIN_ROWS:
  191. + return [
  192. + (self.w_fd, self.w_off, rel_l, self.dim, memoryview(w.numpy()).cast("B"), self.w_view),
  193. + (self.s_fd, self.s_off, rel_l, self.sb, memoryview(s.numpy()).cast("B"), self.s_view),
  194. + ]
  195. return [
  196. (self.w_fd, self.w_off, rel_l, self.dim, memoryview(w.numpy()).cast("B")),
  197. (self.s_fd, self.s_off, rel_l, self.sb, memoryview(s.numpy()).cast("B")),
  198. @@ -1251,6 +1386,18 @@
  199. hash_ids: torch.Tensor,
  200. token_mask: torch.Tensor | None = None,
  201. ) -> torch.Tensor:
  202. + # Stage 7: phase "engram_fwd" of the prefill budget (inert unless enabled).
  203. + from vllm.model_executor.kernels.attention.dsa.candidate_blocks import _PrefillTimer
  204. +
  205. + with _PrefillTimer("engram_fwd", int(hash_ids.shape[0])):
  206. + return self._forward_impl(hidden_states, hash_ids, token_mask)
  207. +
  208. + def _forward_impl(
  209. + self,
  210. + hidden_states: torch.Tensor,
  211. + hash_ids: torch.Tensor,
  212. + token_mask: torch.Tensor | None = None,
  213. + ) -> torch.Tensor:
  214. """hidden_states: [T, hc_mult, dim]; hash_ids: [T, n_hash_cols] (all
  215. tokens, pre sequence-parallel shard); token_mask: [T], False shuts
  216. the gate so those positions pass through untouched."""
  217. @@ -1352,6 +1499,27 @@
  218. ]
  219. self.hashes_ready = torch.cuda.Event()
  220. self.num_staged = 0
  221. + # DSV41_ENGRAM_TIMING=1: host wall-clock of stage() phases (diagnostic).
  222. + self._timing = _kai_os.environ.get("DSV41_ENGRAM_TIMING", "0") == "1"
  223. + # two buckets (decode: n <= 16 tokens, prefill: n > 16): sync, gather+dequant, tail, total, calls, rows, unique rows
  224. + self._t_acc = [[0.0, 0.0, 0.0, 0.0, 0, 0, 0] for _ in range(2)]
  225. + self._t_last_total_ms = 0.0 # stage 7 reads it after each timed prefill step
  226. + # Stage 8: hash-ahead prefetch state (see _hash_ahead).
  227. + self._prefetch = _KAI_PREFETCH
  228. + if self._prefetch:
  229. + self.hash_host_next = torch.empty_like(self.hash_host)
  230. + self.hashes_next_ready = torch.cuda.Event()
  231. + self.last_pos_host = torch.zeros(256, dtype=torch.int64, device="cpu", pin_memory=True)
  232. + self._prefetch_pool = _KaiPool(
  233. + max_workers=_KAI_PREFETCH_THREADS, thread_name_prefix="engram-prefetch"
  234. + )
  235. + self._prefetch_futs: list = []
  236. + self._prefetch_stats = [0, 0, 0.0] # lookaheads, rows, host seconds
  237. + logger.info(
  238. + "Engram DISK prefetch: hash-ahead of the next prefill chunk (%s, %d threads)",
  239. + _KAI_PREFETCH_MODE, _KAI_PREFETCH_THREADS,
  240. + )
  241. +
  242. logger.info(
  243. "Engram DISK rows staged before the forward (graph-safe): %d layers, "
  244. "heads [%d, %d) of %d, up to %d tokens/step, %d read threads",
  245. @@ -1363,6 +1531,72 @@
  246. _KAI_THREADS,
  247. )
  248. + def _hash_ahead(self, input_batch, req_states, positions, query_start_loc, lookback_token_ids) -> None:
  249. + """Stage 8: for the batch's longest still-prefilling request, hash the
  250. + NEXT chunk of its (known) prompt with the same kernel and warm this
  251. + rank's rows in the page cache while the current step computes."""
  252. + if torch.cuda.is_current_stream_capturing():
  253. + return
  254. + import time as _kai_time
  255. +
  256. + from .mm_preprocess import image_sentinel_mask
  257. +
  258. + t0 = _kai_time.perf_counter()
  259. + n_reqs = int(input_batch.num_reqs)
  260. + if n_reqs <= 0:
  261. + return
  262. + idx_np = input_batch.idx_mapping_np[:n_reqs]
  263. + prompt_len = req_states.prompt_len.np[idx_np]
  264. + next_start = self.last_pos_host[:n_reqs].numpy() + 1 # valid after hashes_ready.synchronize()
  265. + remaining = prompt_len - next_start
  266. + i = int(remaining.argmax())
  267. + if remaining[i] <= 0:
  268. + return
  269. + req_idx = int(idx_np[i])
  270. + s = int(next_start[i])
  271. + L = int(min(remaining[i], self.max_tokens))
  272. + tok = req_states.all_token_ids.gpu
  273. + ids = tok[req_idx, s : s + L]
  274. + pos = torch.arange(s, s + L, dtype=positions.dtype, device=positions.device)
  275. + qsl = torch.tensor([0, L], dtype=query_start_loc.dtype, device=query_start_loc.device)
  276. + depth = int(lookback_token_ids.shape[1])
  277. + lb = torch.full((1, depth), -1, dtype=lookback_token_ids.dtype, device=lookback_token_ids.device)
  278. + k = min(depth, s)
  279. + if k > 0:
  280. + lb[0, :k] = tok[req_idx, s - k : s].flip(0).to(lb.dtype)
  281. + hashes = self.hash_state(
  282. + ids, pos, qsl, image_sentinel_mask(ids), lb, image_sentinel_mask(lb), None, None
  283. + )
  284. + host = self.hash_host_next[:L]
  285. + host.copy_(hashes[:, :, self.head_start : self.head_end], non_blocking=True)
  286. + self.hashes_next_ready.record()
  287. + self.hashes_next_ready.synchronize()
  288. + self._prefetch_futs = [f for f in self._prefetch_futs if not f.done()]
  289. + total_rows = 0
  290. + for engram in self.engrams:
  291. + local = host[:, engram.layer_hash_index, :].to(torch.int64)
  292. + rel, owned = engram.embed_tokens.disk_rel_owned(local)
  293. + rows = torch.unique(rel[owned]).tolist()
  294. + total_rows += len(rows)
  295. + table = engram.embed_tokens.disk
  296. + for lo in range(0, len(rows), _KAI_PREFETCH_TASK_ROWS):
  297. + self._prefetch_futs.append(
  298. + self._prefetch_pool.submit(
  299. + _kai_prefetch_rows, table, rows[lo : lo + _KAI_PREFETCH_TASK_ROWS]
  300. + )
  301. + )
  302. + st = self._prefetch_stats
  303. + st[0] += 1
  304. + st[1] += total_rows
  305. + st[2] += _kai_time.perf_counter() - t0
  306. + if st[0] % 16 == 0 or L >= 4096:
  307. + logger.info(
  308. + "Engram prefetch: next chunk %d tokens from %d (prompt %d), %d unique rows queued, "
  309. + "host %.1f ms (%d lookaheads so far, avg %.0f rows, %.1f ms)",
  310. + L, s, int(prompt_len[i]), total_rows, 1e3 * (_kai_time.perf_counter() - t0),
  311. + st[0], st[1] / st[0], 1e3 * st[2] / st[0],
  312. + )
  313. +
  314. @torch.inference_mode()
  315. def stage(
  316. self,
  317. @@ -1371,6 +1605,8 @@
  318. query_start_loc: torch.Tensor,
  319. lookback_token_ids: torch.Tensor,
  320. num_tokens: int,
  321. + input_batch=None,
  322. + req_states=None,
  323. ) -> int:
  324. """Stage rows for the first `num_tokens` (unpadded) tokens of this step.
  325. Returns the number staged; 0 while the KV cache is unbound (profiling),
  326. @@ -1380,6 +1616,12 @@
  327. return 0
  328. from .mm_preprocess import image_sentinel_mask
  329. + timing = self._timing
  330. + if timing:
  331. + import time as _kai_time
  332. +
  333. + _t0 = _kai_time.perf_counter()
  334. +
  335. ids = input_ids[:n]
  336. hashes = self.hash_state(
  337. ids,
  338. @@ -1393,19 +1635,90 @@
  339. )
  340. host = self.hash_host[:n]
  341. host.copy_(hashes[:, :, self.head_start : self.head_end], non_blocking=True)
  342. + prefetch = self._prefetch and input_batch is not None and req_states is not None
  343. + if prefetch:
  344. + # last position of every request in this step -> next chunk start
  345. + n_reqs = int(input_batch.num_reqs)
  346. + if n_reqs > self.last_pos_host.shape[0]:
  347. + self.last_pos_host = torch.zeros(
  348. + n_reqs * 2, dtype=torch.int64, device="cpu", pin_memory=True
  349. + )
  350. + ends = torch.as_tensor(
  351. + input_batch.query_start_loc_np[1 : n_reqs + 1].astype("int64") - 1,
  352. + device=positions.device,
  353. + )
  354. + self.last_pos_host[:n_reqs].copy_(positions[ends], non_blocking=True)
  355. self.hashes_ready.record()
  356. # The one host sync per step. It also orders this step's rewrite of the
  357. # pinned buffers after the previous step's async H2D copies from them.
  358. self.hashes_ready.synchronize()
  359. + if timing:
  360. + _t1 = _kai_time.perf_counter()
  361. requests = []
  362. for engram in self.engrams:
  363. local = host[:, engram.layer_hash_index, :].to(torch.int64)
  364. rel, owned = engram.embed_tokens.disk_rel_owned(local)
  365. requests.append((engram.embed_tokens.disk, rel, owned))
  366. rows_per_layer = gather_dequant_many(requests)
  367. + if timing:
  368. + _t2 = _kai_time.perf_counter()
  369. + _acc = self._t_acc[0 if n <= 16 else 1]
  370. + _acc[0] += _t1 - _t0
  371. + _acc[1] += _t2 - _t1
  372. + _acc[4] += 1
  373. + _rows = sum(int(r[1].numel()) for r in requests)
  374. + _uniq = sum(int(torch.unique(r[1]).numel()) for r in requests)
  375. + _acc[5] += _rows
  376. + _acc[6] += _uniq
  377. + # the H2D tail is timed by the caller-visible end of stage(): record
  378. + # the total when the call returns (see the return sites below)
  379. + self._t_mid = _t2
  380. + self._t_last = (_rows, _uniq, _t2 - _t1)
  381. for engram, rows, buf in zip(self.engrams, rows_per_layer, self.rows_host):
  382. staged = buf[:n]
  383. staged.copy_(rows.view(n, self.local_heads, self.dim))
  384. engram.staged_rows[:n].copy_(staged, non_blocking=True)
  385. self.num_staged = n
  386. + if timing:
  387. + _t3 = _kai_time.perf_counter()
  388. + _acc = self._t_acc[0 if n <= 16 else 1]
  389. + _acc[2] += _t3 - self._t_mid
  390. + _acc[3] += _t3 - _t0
  391. + self._t_last_total_ms = 1e3 * (_t3 - _t0)
  392. + # stage 6 defines _KAI_MMAP (memmap gather); resolved at call time
  393. + _mode = (
  394. + "mmap"
  395. + if globals().get("_KAI_MMAP", False)
  396. + and self._t_last[0] >= globals().get("_KAI_MMAP_MIN_ROWS", 0) * len(self.engrams)
  397. + else "preadv"
  398. + )
  399. + if n >= 1024:
  400. + logger.info(
  401. + "Engram prefill stage [%s]: %d tokens, %d rows (%d unique), "
  402. + "gather+dequant %.1f ms, total %.1f ms",
  403. + _mode, n, self._t_last[0], self._t_last[1],
  404. + 1e3 * self._t_last[2], 1e3 * (_t3 - _t0),
  405. + )
  406. + if _acc[4] % (200 if n <= 16 else 20) == 0:
  407. + logger.info(
  408. + "Engram stage timing [%s, %s]: total %.2f ms/step (sync wait %.2f, "
  409. + "disk+dequant %.2f, h2d+rest %.2f), %.1f rows/step (%.1f unique), "
  410. + "avg of %d steps",
  411. + _mode, "decode" if n <= 16 else "prefill",
  412. + 1e3 * _acc[3] / _acc[4],
  413. + 1e3 * _acc[0] / _acc[4],
  414. + 1e3 * _acc[1] / _acc[4],
  415. + 1e3 * _acc[2] / _acc[4],
  416. + _acc[5] / _acc[4],
  417. + _acc[6] / _acc[4],
  418. + _acc[4],
  419. + )
  420. + for i in range(7):
  421. + _acc[i] = 0
  422. + if prefetch:
  423. + try:
  424. + self._hash_ahead(input_batch, req_states, positions, query_start_loc, lookback_token_ids)
  425. + except Exception as e: # noqa: BLE001 - prefetch is best effort
  426. + logger.warning_once("Engram prefetch disabled after error: %r", e)
  427. + self._prefetch = False
  428. return n