engram_prefetch.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. """Engram rows fetched on a side stream as soon as the hash ids exist.
  2. Measured 2026-09-18 with the live profiler at c1: 2.2-2.4 ms of every 48 ms step was GPU idle
  3. before the two `_engram_gather_kernel` launches on real text (1.0 ms on sparkDash prose). The
  4. stall is NVMe latency: the host callback that serves the lookup reads every cache miss
  5. (O_DIRECT 8 KB pread, ~200 us) while the graph waits, once per Engram layer, and the all-reduce
  6. then waits for the slowest rank.
  7. The hash ids for both Engram layers are computed at the start of the forward, ~1 ms of GPU work
  8. before layer 1 needs them and ~15 ms before layer 14 does. This adapter forks a side stream
  9. right after the hasher returns, runs the ids copy, the host lookup and the row copies for every
  10. Engram layer there, and joins the side stream only when the layer's gather runs. Inside a CUDA
  11. graph that is an ordinary fork/join branch with a host node on it; the model, the hasher and
  12. the row store are unchanged, and the gather reads the same staging buffers as before.
  13. Gate ``DSV41_ENGRAM_PREFETCH=1``. ``DSV41_ENGRAM_PREFETCH_CHECK=1`` additionally runs the old
  14. synchronous lookup after each prefetched gather and accumulates the number of gathers whose
  15. output differed (logged every DSV41_STATS_SECONDS; expected 0).
  16. """
  17. import ctypes as C
  18. import logging
  19. import os
  20. import threading
  21. import time
  22. import torch
  23. import engram_backend as eb
  24. logger = logging.getLogger(__name__)
  25. _EMBEDS = []
  26. _state = {"disabled": False}
  27. def enabled():
  28. return os.environ.get("DSV41_ENGRAM_PREFETCH", "0").strip() in ("1", "on", "true")
  29. def _checking():
  30. return os.environ.get("DSV41_ENGRAM_PREFETCH_CHECK", "0").strip() == "1"
  31. def _staging(self, indices, count):
  32. """Same staging layout as engram_backend.owned, created here when the prefetch is first."""
  33. capacity = 1 << (count - 1).bit_length()
  34. key = (indices.device.index, capacity)
  35. if key not in self._staging:
  36. if torch.cuda.is_current_stream_capturing():
  37. raise RuntimeError(f"Engram staging {key} must be warmed before graph capture")
  38. ids = torch.empty(capacity, dtype=torch.int64, pin_memory=True)
  39. w = torch.empty((capacity, 256), dtype=torch.uint8, pin_memory=True)
  40. s = torch.empty((capacity, 8), dtype=torch.uint8, pin_memory=True)
  41. dw, ds = w.to(indices.device), s.to(indices.device)
  42. sequential = torch.arange(capacity, dtype=torch.int64, device=indices.device)
  43. self._staging[key] = (ids, w, s, dw, ds, sequential)
  44. ids, w, s, dw, ds, sequential = self._staging[key]
  45. work_key = (indices.device.index, count)
  46. if work_key not in self._works:
  47. self._works[work_key] = eb.Work(self._store, ids.data_ptr(), w.data_ptr(), s.data_ptr(), count)
  48. return ids, w, s, dw, ds, sequential, self._works[work_key]
  49. def _prefetch(self, indices):
  50. count = indices.numel()
  51. self._pending = None
  52. if not count or self.rows == 0:
  53. return
  54. if self._side is None:
  55. if torch.cuda.is_current_stream_capturing():
  56. raise RuntimeError("Engram prefetch stream must exist before graph capture")
  57. self._side = torch.cuda.Stream(device=indices.device)
  58. ids, w, s, dw, ds, sequential, work = _staging(self, indices, count)
  59. main = torch.cuda.current_stream(indices.device)
  60. self._side.wait_stream(main) # fork: the hash ids are ready on main
  61. with torch.cuda.stream(self._side):
  62. ids[:count].copy_(indices.reshape(-1), non_blocking=True)
  63. error = eb._cuda.cudaLaunchHostFunc(self._side.cuda_stream,
  64. C.cast(eb._lib.row_store_lookup, eb.P), C.addressof(work))
  65. if error:
  66. raise RuntimeError(f"CUDA Engram host callback failed: {error}")
  67. dw[:count].copy_(w[:count], non_blocking=True)
  68. ds[:count].copy_(s[:count], non_blocking=True)
  69. self._pending = (indices.data_ptr(), tuple(indices.shape), count, dw, ds, sequential)
  70. def _prefetch_all(hash_ids):
  71. if _state["disabled"] or hash_ids.dim() != 3 or hash_ids.shape[0] == 0:
  72. return
  73. for emb in _EMBEDS:
  74. if emb._hash_index is None:
  75. continue
  76. _prefetch(emb, hash_ids[:, emb._hash_index])
  77. def install(module):
  78. if not enabled():
  79. return
  80. Emb, Eng, Hasher = module.EngramEmbedding, module.Engram, module.EngramHasher
  81. for name in ("_staging", "_works", "_store", "rows"):
  82. pass # set by engram_backend.install's __init__
  83. orig_init = Emb.__init__
  84. def init(self, *args, **kwargs):
  85. orig_init(self, *args, **kwargs)
  86. self._hash_index = None
  87. self._pending = None
  88. self._side = torch.cuda.Stream() if torch.cuda.is_available() else None
  89. self._check_acc = None
  90. _EMBEDS.append(self)
  91. Emb.__init__ = init
  92. orig_eng_init = Eng.__init__
  93. def eng_init(self, *args, **kwargs):
  94. orig_eng_init(self, *args, **kwargs)
  95. self.embed._hash_index = self.layer_hash_index
  96. Eng.__init__ = eng_init
  97. orig_owned = Emb._owned_rows
  98. check = _checking()
  99. def owned(self, indices):
  100. p = self._pending
  101. if not p or p[0] != indices.data_ptr() or p[1] != tuple(indices.shape) or p[2] != indices.numel():
  102. return orig_owned(self, indices)
  103. self._pending = None
  104. from sglang.kernels.ops.embeddings.engram_gather import engram_gather
  105. _, _, count, dw, ds, sequential = p
  106. torch.cuda.current_stream(indices.device).wait_stream(self._side) # join
  107. out = self._empty(indices)
  108. engram_gather(dw.data_ptr(), ds.data_ptr(), sequential[:count], out.view(-1, 256), 256, 32)
  109. if check:
  110. reference = orig_owned(self, indices)
  111. if self._check_acc is None:
  112. self._check_acc = torch.zeros((), dtype=torch.int64, device=indices.device)
  113. self._check_acc += (out != reference).any().to(torch.int64)
  114. return out
  115. Emb._owned_rows = owned
  116. orig_forward = Hasher.forward
  117. def forward(self, input_ids, forward_batch):
  118. hash_ids = orig_forward(self, input_ids, forward_batch)
  119. try:
  120. _prefetch_all(hash_ids)
  121. except Exception as exc:
  122. if not _state["disabled"]:
  123. _state["disabled"] = True
  124. logger.warning("DSV41 Engram prefetch disabled after error: %r", exc)
  125. for emb in _EMBEDS:
  126. emb._pending = None
  127. return hash_ids
  128. Hasher.forward = forward
  129. logger.warning("DSV41 Engram prefetch ARMED: rows fetched on a side stream after the hasher%s",
  130. " (CHECK mode: old path re-run and compared)" if check else "")
  131. if check:
  132. period = float(os.getenv("DSV41_STATS_SECONDS", "60"))
  133. def loop():
  134. while True:
  135. time.sleep(period)
  136. counts = [int(e._check_acc.item()) if e._check_acc is not None else -1 for e in _EMBEDS]
  137. logger.warning("DSV41 Engram prefetch CHECK: gathers differing from the synchronous path per layer = %s", counts)
  138. threading.Thread(target=loop, daemon=True, name="engram-prefetch-check").start()