engram_backend.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. """Bounded exact file-backed replacement for EngramEmbedding's owned-row gather.
  2. The original hash, gating, projections, TP all-reduce and model remain unchanged.
  3. Native callback executes IO inside CUDA graphs without calling the CUDA API.
  4. """
  5. import ctypes as C
  6. import glob
  7. import json
  8. import logging
  9. import os
  10. from pathlib import Path
  11. import struct
  12. import torch
  13. P, U = C.c_void_p, C.c_uint64
  14. _lib = C.CDLL(str(Path(__file__).with_name('librow_store.so')))
  15. _lib.row_store_open.argtypes = [C.c_char_p, U, U, U, U]
  16. _lib.row_store_open.restype = P
  17. _lib.row_store_range.argtypes = [P, U, U]
  18. _lib.row_store_stats.argtypes = [P, C.POINTER(U)]
  19. _lib.row_store_attach_packed.argtypes = [P, C.c_char_p, U]
  20. _lib.row_store_attach_packed.restype = C.c_int
  21. # Present only in images built with -DDSV41_ROW_STORE_DRM (the TP4 canary Dockerfiles).
  22. # The default image's librow_store.so does not export it, and ./start.sh never calls it.
  23. _drm_next = getattr(_lib, "row_store_next_cache_on_drm", None)
  24. if _drm_next is not None:
  25. _drm_next.argtypes = [C.c_char_p, U]
  26. _DRM_USED = [False]
  27. class Work(C.Structure):
  28. _fields_ = [('store', P), ('ids', P), ('weights', P), ('scales', P), ('count', U)]
  29. _STORES = []
  30. def _stats(store):
  31. out = (U * 9)()
  32. _lib.row_store_stats(store, out)
  33. hits, misses, reads, cache_bytes, slots, scale_bytes, ways, threads, packed = out
  34. return dict(hits=hits, misses=misses, reads=reads, cache_bytes=cache_bytes,
  35. slots=slots, scale_bytes=scale_bytes, ways=ways, threads=threads,
  36. packed=bool(packed))
  37. def _report():
  38. """Miss rate and read volume decide decode latency here; log them."""
  39. import threading
  40. import time
  41. period = float(os.getenv('DSV41_STATS_SECONDS', '60'))
  42. if period <= 0:
  43. return
  44. log = logging.getLogger(__name__)
  45. def loop():
  46. previous = {}
  47. while True:
  48. time.sleep(period)
  49. for store, layer_id in _STORES:
  50. now = _stats(store)
  51. was = previous.get(layer_id)
  52. previous[layer_id] = now
  53. if was is None:
  54. continue
  55. hits = now['hits'] - was['hits']
  56. misses = now['misses'] - was['misses']
  57. total = hits + misses
  58. if not total:
  59. continue
  60. log.info('Engram layer=%s lookups=%s hit_rate=%.1f%% reads=%s '
  61. 'cache=%.1fGiB(%s-way) scales=%.1fGiB threads=%s packed=%s',
  62. layer_id, total, 100.0*hits/total, now['reads']-was['reads'],
  63. now['cache_bytes']/2**30, now['ways'],
  64. now['scale_bytes']/2**30, now['threads'], now['packed'])
  65. threading.Thread(target=loop, daemon=True, name='engram-stats').start()
  66. def _register_store(store, layer_id):
  67. _STORES.append((store, layer_id))
  68. if len(_STORES) == 1:
  69. _report()
  70. def _load_cudart():
  71. paths = []
  72. paths += glob.glob('/usr/local/cuda*/targets/aarch64-linux/lib/libcudart.so*')
  73. paths += glob.glob('/usr/local/cuda*/targets/x86_64-linux/lib/libcudart.so*')
  74. paths += glob.glob('/usr/local/cuda/lib64/libcudart.so*')
  75. paths += glob.glob('/usr/local/lib/python*/dist-packages/nvidia/cuda_runtime/lib/libcudart.so*')
  76. paths += glob.glob('/usr/local/lib/python*/site-packages/nvidia/cuda_runtime/lib/libcudart.so*')
  77. seen, errors = set(), []
  78. for path in paths:
  79. if path in seen or path.endswith('.a') or not os.path.isfile(path):
  80. continue
  81. seen.add(path)
  82. try:
  83. lib = C.CDLL(path)
  84. lib.cudaLaunchHostFunc.argtypes = [P, P, P]
  85. lib.cudaLaunchHostFunc.restype = C.c_int
  86. return lib
  87. except OSError as exc:
  88. errors.append(f'{path}: {exc}')
  89. raise RuntimeError('libcudart.so not found (need CUDA runtime for Engram host callbacks). Tried: '
  90. + '; '.join(errors or paths or ['<none>']))
  91. _cuda = _load_cudart()
  92. def install(module):
  93. cls = module.EngramEmbedding
  94. def init(self, num_embeddings, dim, layer_id):
  95. torch.nn.Module.__init__(self)
  96. assert dim == 256
  97. self.dim, self.tp_size = dim, module.get_parallel().tp_size
  98. rank = module.get_parallel().tp_rank
  99. self.row_start = num_embeddings * rank // self.tp_size
  100. end = num_embeddings * (rank + 1) // self.tp_size
  101. self.rows, self.host_table = end - self.row_start, None
  102. root = Path(os.environ['DSV41_SOURCE'])
  103. index = json.loads((root/'model.safetensors.index.json').read_text())['weight_map']
  104. prefix = f'layers.{layer_id}.engram.embed.'
  105. filename = root/index[prefix+'weight']
  106. assert index[prefix+'weight'] == index[prefix+'scale']
  107. with filename.open('rb') as f:
  108. length = struct.unpack('<Q', f.read(8))[0]
  109. header = json.loads(f.read(length))
  110. w, s = header[prefix+'weight'], header[prefix+'scale']
  111. assert w['shape'] == [num_embeddings, 256] and w['dtype'] == 'F8_E4M3'
  112. assert s['shape'] == [num_embeddings, 8] and s['dtype'] == 'F8_E8M0'
  113. # DSV41_CACHE_GIB is the per-host budget, split across the Engram layers
  114. # and the ranks that share that host's RAM. Upstream ran every TP rank in
  115. # one box, so it divided by tp_size; on the Spark triangle each node runs
  116. # a single rank and dividing by tp_size silently gave each node a third of
  117. # the cache it was asked for.
  118. ranks_per_host = max(1, self.tp_size // max(1, int(os.getenv('NNODES', '1'))))
  119. budget = int(float(os.getenv('DSV41_CACHE_GIB', '64')) * 2**30) // (2*ranks_per_host)
  120. # start-tp4.sh only. DSV41_ENGRAM_DRM_NODE=/dev/dri/card0: one Engram layer
  121. # (DSV41_ENGRAM_DRM_LAYER, default the first opened) keeps its row cache in the GB10
  122. # display reservation, outside MemAvailable, sized to DSV41_ENGRAM_DRM_MIB
  123. # (<= 2032, multiple of 16). Its keys stay in ordinary memory.
  124. if os.environ.get('DSV41_LAUNCHER') == 'tp4':
  125. drm_node = os.getenv('DSV41_ENGRAM_DRM_NODE', '')
  126. drm_layer = os.getenv('DSV41_ENGRAM_DRM_LAYER', '')
  127. if (drm_node and os.path.exists(drm_node) and not _DRM_USED[0]
  128. and (not drm_layer or int(drm_layer) == layer_id)):
  129. if _drm_next is None:
  130. print('Engram: DSV41_ENGRAM_DRM_NODE is set, but this image has no '
  131. 'display-reservation cache (build Dockerfile.canary-roce)', flush=True)
  132. else:
  133. pool = int(os.getenv('DSV41_ENGRAM_DRM_MIB', '1792')) << 20
  134. budget = min(budget, pool * 272 // 264) # slots * 264 B of rows fit the pool
  135. _drm_next(drm_node.encode(), pool)
  136. _DRM_USED[0] = True
  137. self._store = _lib.row_store_open(str(filename).encode(), num_embeddings,
  138. 8+length+w['data_offsets'][0], 8+length+s['data_offsets'][0], budget)
  139. if not self._store:
  140. raise RuntimeError(f'Could not open Engram backing shard: {filename}')
  141. _lib.row_store_range(self._store, self.row_start, end)
  142. # A repacked local shard, when present, makes every miss a single local
  143. # read; without it we fall back to the checkpoint (NFS on a worker).
  144. packed_dir = os.environ.get('DSV41_PACKED_DIR', '')
  145. if packed_dir:
  146. shard = Path(packed_dir)/f'engram-l{layer_id}-r{rank}of{self.tp_size}.bin'
  147. if shard.exists():
  148. _lib.row_store_attach_packed(self._store, str(shard).encode(), layer_id)
  149. _register_store(self._store, layer_id)
  150. self._staging = {}
  151. self._works = {}
  152. # The loader sees names but never allocates/copies the complete tables.
  153. self.weight = torch.nn.Parameter(torch.empty(0, dtype=torch.float8_e4m3fn), requires_grad=False)
  154. self.scale = torch.nn.Parameter(torch.empty(0, dtype=torch.float8_e8m0fnu), requires_grad=False)
  155. def validate_weight(param, source):
  156. expected = w if param is self.weight else s
  157. if list(source.shape) != expected['shape']:
  158. raise ValueError('Engram checkpoint shape mismatch')
  159. self.weight.weight_loader = validate_weight
  160. self.scale.weight_loader = validate_weight
  161. stats = _stats(self._store)
  162. logging.getLogger(__name__).info(
  163. 'Exact %s Engram layer=%s rank=%s rows=[%s,%s) cache=%.1fGiB '
  164. '(%s slots, %s-way, %.1f%% of owned rows) scales=%.1fGiB io_threads=%s '
  165. 'packed=%s',
  166. os.getenv('OFFLOAD_MODE', 'nvme'), layer_id, rank, self.row_start, end,
  167. stats['cache_bytes']/2**30, stats['slots'], stats['ways'],
  168. 100.0*stats['slots']/max(1, self.rows), stats['scale_bytes']/2**30,
  169. stats['threads'], stats['packed'])
  170. def owned(self, indices):
  171. from sglang.kernels.ops.embeddings.engram_gather import engram_gather
  172. count = indices.numel()
  173. if not count:
  174. return self._empty(indices)
  175. capacity = 1 << (count - 1).bit_length()
  176. key = (indices.device.index, capacity)
  177. if key not in self._staging:
  178. if torch.cuda.is_current_stream_capturing():
  179. raise RuntimeError(f'Engram staging {key} must be warmed before graph capture')
  180. ids = torch.empty(capacity, dtype=torch.int64, pin_memory=True)
  181. w = torch.empty((capacity, 256), dtype=torch.uint8, pin_memory=True)
  182. s = torch.empty((capacity, 8), dtype=torch.uint8, pin_memory=True)
  183. dw, ds = w.to(indices.device), s.to(indices.device)
  184. sequential = torch.arange(capacity, dtype=torch.int64, device=indices.device)
  185. self._staging[key] = (ids, w, s, dw, ds, sequential)
  186. ids, w, s, dw, ds, sequential = self._staging[key]
  187. work_key = (indices.device.index, count)
  188. if work_key not in self._works:
  189. self._works[work_key] = Work(self._store, ids.data_ptr(), w.data_ptr(), s.data_ptr(), count)
  190. work = self._works[work_key]
  191. ids[:count].copy_(indices.reshape(-1), non_blocking=True)
  192. error = _cuda.cudaLaunchHostFunc(torch.cuda.current_stream().cuda_stream,
  193. C.cast(_lib.row_store_lookup, P), C.addressof(work))
  194. if error:
  195. raise RuntimeError(f'CUDA Engram host callback failed: {error}')
  196. dw[:count].copy_(w[:count], non_blocking=True)
  197. ds[:count].copy_(s[:count], non_blocking=True)
  198. out = self._empty(indices)
  199. engram_gather(dw.data_ptr(), ds.data_ptr(), sequential[:count], out.view(-1, 256), 256, 32)
  200. return out
  201. cls.__init__ = init
  202. cls._owned_rows = owned