pack_engram.py 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. """Repack this rank's owned Engram rows into a local contiguous shard.
  2. In the checkpoint a row's 256 B of weight and its 8 B of scale sit in two tensors
  3. about 98 GB apart, so a cache miss costs two reads into unrelated 4 KiB pages --
  4. and on a worker both of them are NFS round trips to the head. Packing the owned
  5. rows as 264 B records on local disk makes a miss one local read.
  6. Per rank: (rows/tp) * 264 B, about 34 GiB per Engram layer.
  7. Run inside the serving image, where /models is the checkpoint and the output
  8. directory is node-local storage:
  9. python3 scripts/pack_engram.py --rank 0 --tp 3 --out /engram
  10. """
  11. from __future__ import annotations
  12. import argparse
  13. import json
  14. import os
  15. from pathlib import Path
  16. import struct
  17. import sys
  18. import time
  19. import numpy
  20. MAGIC = 0x31344E4531565344 # "DSV41EN1"
  21. HEADER_BYTES = 4096
  22. ROW_BYTES = 264
  23. WEIGHT_BYTES = 256
  24. SCALE_BYTES = 8
  25. ENGRAM_LAYERS = (1, 14)
  26. def tensor_span(root: Path, layer_id: int):
  27. index = json.loads((root / 'model.safetensors.index.json').read_text())['weight_map']
  28. prefix = f'layers.{layer_id}.engram.embed.'
  29. name = index[prefix + 'weight']
  30. assert index[prefix + 'scale'] == name, 'weight and scale must share a shard'
  31. path = root / name
  32. with path.open('rb') as f:
  33. length = struct.unpack('<Q', f.read(8))[0]
  34. header = json.loads(f.read(length))
  35. weight, scale = header[prefix + 'weight'], header[prefix + 'scale']
  36. assert weight['dtype'] == 'F8_E4M3' and scale['dtype'] == 'F8_E8M0'
  37. assert weight['shape'][1] == WEIGHT_BYTES and scale['shape'][1] == SCALE_BYTES
  38. base = 8 + length
  39. return (path, weight['shape'][0],
  40. base + weight['data_offsets'][0], base + scale['data_offsets'][0])
  41. def pack_layer(root: Path, out_dir: Path, layer_id: int, rank: int, tp: int,
  42. chunk_rows: int) -> Path:
  43. path, rows, weight_off, scale_off = tensor_span(root, layer_id)
  44. lo, hi = rows * rank // tp, rows * (rank + 1) // tp
  45. count = hi - lo
  46. target = out_dir / f'engram-l{layer_id}-r{rank}of{tp}.bin'
  47. partial = target.with_suffix('.partial')
  48. expected = HEADER_BYTES + count * ROW_BYTES
  49. if target.exists() and target.stat().st_size == expected:
  50. print(f'layer {layer_id}: {target} already complete ({expected/2**30:.1f} GiB)',
  51. flush=True)
  52. return target
  53. header = bytearray(HEADER_BYTES)
  54. struct.pack_into('<6Q', header, 0, MAGIC, layer_id, lo, hi, rows, ROW_BYTES)
  55. started = time.monotonic()
  56. written = 0
  57. with open(path, 'rb', buffering=0) as src, open(partial, 'wb', buffering=0) as dst:
  58. dst.write(header)
  59. for start in range(lo, hi, chunk_rows):
  60. stop = min(start + chunk_rows, hi)
  61. n = stop - start
  62. src.seek(weight_off + start * WEIGHT_BYTES)
  63. weights = src.read(n * WEIGHT_BYTES)
  64. src.seek(scale_off + start * SCALE_BYTES)
  65. scales = src.read(n * SCALE_BYTES)
  66. if len(weights) != n * WEIGHT_BYTES or len(scales) != n * SCALE_BYTES:
  67. raise SystemExit(f'short read from {path} at row {start}')
  68. block = numpy.empty((n, ROW_BYTES), dtype=numpy.uint8)
  69. block[:, :WEIGHT_BYTES] = numpy.frombuffer(
  70. weights, dtype=numpy.uint8).reshape(n, WEIGHT_BYTES)
  71. block[:, WEIGHT_BYTES:] = numpy.frombuffer(
  72. scales, dtype=numpy.uint8).reshape(n, SCALE_BYTES)
  73. dst.write(block.tobytes())
  74. written += n
  75. elapsed = time.monotonic() - started
  76. rate = written * ROW_BYTES / max(elapsed, 1e-6)
  77. print(f'layer {layer_id}: {written}/{count} rows '
  78. f'({100.0*written/count:5.1f}%) {rate/2**20:.0f} MiB/s', flush=True)
  79. dst.flush()
  80. os.fsync(dst.fileno())
  81. partial.rename(target)
  82. print(f'layer {layer_id}: wrote {target} ({expected/2**30:.1f} GiB) in '
  83. f'{time.monotonic()-started:.0f}s', flush=True)
  84. return target
  85. def main() -> int:
  86. parser = argparse.ArgumentParser()
  87. parser.add_argument('--model', default=os.environ.get('DSV41_SOURCE', '/models/DeepSeek-V4.1-Flash'))
  88. parser.add_argument('--out', default=os.environ.get('DSV41_PACKED_DIR', '/engram'))
  89. parser.add_argument('--rank', type=int, default=int(os.environ.get('NODE_RANK', '0')))
  90. parser.add_argument('--tp', type=int, default=int(os.environ.get('TP_SIZE', '3')))
  91. parser.add_argument('--chunk-rows', type=int, default=1 << 18)
  92. parser.add_argument('--layers', default=','.join(str(x) for x in ENGRAM_LAYERS))
  93. args = parser.parse_args()
  94. root, out_dir = Path(args.model), Path(args.out)
  95. out_dir.mkdir(parents=True, exist_ok=True)
  96. layers = [int(x) for x in args.layers.split(',') if x.strip()]
  97. need = 0
  98. for layer_id in layers:
  99. _, rows, _, _ = tensor_span(root, layer_id)
  100. need += HEADER_BYTES + (rows * (args.rank + 1) // args.tp - rows * args.rank // args.tp) * ROW_BYTES
  101. free = os.statvfs(out_dir).f_bavail * os.statvfs(out_dir).f_frsize
  102. print(f'rank {args.rank}/{args.tp}: need {need/2**30:.1f} GiB, '
  103. f'{free/2**30:.1f} GiB free at {out_dir}', flush=True)
  104. if free < need:
  105. raise SystemExit(f'not enough space at {out_dir}')
  106. for layer_id in layers:
  107. pack_layer(root, out_dir, layer_id, args.rank, args.tp, args.chunk_rows)
  108. return 0
  109. if __name__ == '__main__':
  110. sys.exit(main())