replicated_split.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. """Column-split of chosen ReplicatedLinear layers over the TP group, bit-identical or not at all.
  2. DSV41_REPLICATED_SPLIT=wqkv_a (comma list of prefix suffixes). A ReplicatedLinear makes every TP
  3. rank stream the whole weight for the same output. Here each rank runs the SAME quantized linear on
  4. its slice of 128-row weight tiles (tiles spread as evenly as possible, e.g. 4/4/3/3 of 14) and the
  5. columns are all-gathered (padded to the widest slice; RoCE at small M, a byte copy).
  6. Exactness is checked, not assumed: at the first eager call with M <= DSV41_REPLICATED_SPLIT_MAX_M,
  7. each rank runs the stock layer and its slice on the same random inputs and compares the columns bit
  8. for bit; the ranks agree over the TP group (MIN) and either all use the split for that layer or
  9. all keep the stock path. Graph capture only ever sees the decided path.
  10. """
  11. import copy
  12. import os
  13. import torch
  14. SPEC = [s.strip() for s in os.environ.get("DSV41_REPLICATED_SPLIT", "").split(",") if s.strip()]
  15. MAX_M = int(os.environ.get("DSV41_REPLICATED_SPLIT_MAX_M", "96"))
  16. TILE = 128
  17. _LOG = {"built": 0, "on": 0}
  18. def _scale_params(layer):
  19. n, k = layer.weight.shape
  20. out = []
  21. for name, p in layer.named_parameters(recurse=False):
  22. if name == "weight" or p is None or p.dim() < 1:
  23. continue
  24. if p.dim() >= 2 and p.shape[0] == n:
  25. out.append((name, "rows"))
  26. elif p.dim() == 1 and p.numel() == n * (k // 32):
  27. out.append((name, "flat")) # 128x4-swizzled: 128-row tiles are contiguous
  28. return out
  29. def _ranges(n, world):
  30. tiles = n // TILE
  31. base, extra = divmod(tiles, world)
  32. counts = [base + (1 if r < extra else 0) for r in range(world)]
  33. starts = [sum(counts[:r]) * TILE for r in range(world)]
  34. return [(s, s + c * TILE) for s, c in zip(starts, counts)], max(counts) * TILE
  35. def _proxy(layer, n0, n1):
  36. proxy = copy.copy(layer)
  37. proxy._parameters = dict(layer._parameters)
  38. proxy._buffers = dict(layer._buffers)
  39. proxy._modules = dict(layer._modules)
  40. k = layer.weight.shape[1]
  41. proxy._parameters["weight"] = torch.nn.Parameter(layer.weight.data[n0:n1].contiguous(), requires_grad=False)
  42. for name, kind in _scale_params(layer):
  43. p = layer._parameters[name]
  44. sl = p.data[n0:n1] if kind == "rows" else p.data[n0 * (k // 32):n1 * (k // 32)]
  45. proxy._parameters[name] = torch.nn.Parameter(sl.contiguous(), requires_grad=False)
  46. for attr in ("output_size", "output_size_per_partition"):
  47. if hasattr(layer, attr):
  48. setattr(proxy, attr, n1 - n0)
  49. proxy._dsv41_split = None
  50. return proxy
  51. def _rows(x):
  52. """M of a plain activation or of a pre-quantized Mxfp8SwizzledInput (data, scales)."""
  53. if isinstance(x, torch.Tensor):
  54. return x.shape[0] if x.dim() == 2 else -1
  55. data = getattr(x, "data", None)
  56. return data.shape[0] if isinstance(data, torch.Tensor) and data.dim() == 2 else -1
  57. def _build(layer, orig_forward, group, x_real):
  58. world, rank = group.world_size, group.rank_in_group
  59. n, k = layer.weight.shape
  60. ok = n % TILE == 0 and n // TILE >= world and layer.weight.dtype == torch.float8_e4m3fn
  61. state = None
  62. if ok:
  63. ranges, width = _ranges(n, world)
  64. n0, n1 = ranges[rank]
  65. try:
  66. proxy = _proxy(layer, n0, n1)
  67. dev = layer.weight.device
  68. g = torch.Generator(device=dev).manual_seed(4321)
  69. inputs = [x_real] + [torch.randn((rows, k), generator=g, device=dev, dtype=torch.bfloat16)
  70. for rows in (1, 6, 16)]
  71. for x in inputs:
  72. full = orig_forward(layer, x)[0]
  73. part = orig_forward(proxy, x)[0]
  74. ok = ok and bool(torch.equal(full[:, n0:n1], part))
  75. state = (proxy, ranges, width)
  76. except Exception as exc:
  77. print(f"[replicated_split] {layer._dsv41_prefix}: slice failed on rank {rank}: {exc!r}", flush=True)
  78. ok = False
  79. flag = torch.tensor([1 if ok else 0], dtype=torch.int32, device=layer.weight.device)
  80. torch.distributed.all_reduce(flag, op=torch.distributed.ReduceOp.MIN, group=group.device_group)
  81. agreed = bool(flag.item())
  82. _LOG["built"] += 1
  83. _LOG["on"] += int(agreed)
  84. if rank == 0 and (_LOG["built"] <= 2 or not agreed):
  85. print(f"[replicated_split] {layer._dsv41_prefix} {n}x{k}: split {'ON' if agreed else 'OFF'} "
  86. f"(rank 0 bit-exact {ok}; {_LOG['on']}/{_LOG['built']} layers on so far)", flush=True)
  87. return state if agreed else False
  88. def install(linear_module):
  89. """sglang.srt.layers.linear: ReplicatedLinear, per-instance by prefix."""
  90. if not SPEC:
  91. return
  92. cls = linear_module.ReplicatedLinear
  93. orig_init, orig_forward = cls.__init__, cls.forward
  94. def __init__(self, *a, **kw):
  95. orig_init(self, *a, **kw)
  96. prefix = kw.get("prefix", "") or ""
  97. self._dsv41_prefix = prefix
  98. self._dsv41_split = None if any(prefix.endswith(s) for s in SPEC) else False
  99. def forward(self, x, *a, **kw):
  100. st = getattr(self, "_dsv41_split", False)
  101. m = _rows(x) if st is not False else -1
  102. if st is False or a or kw or m <= 0 or m > MAX_M:
  103. return orig_forward(self, x, *a, **kw)
  104. from sglang.srt.distributed import get_tp_group
  105. group = get_tp_group()
  106. if group.world_size == 1:
  107. return orig_forward(self, x)
  108. if st is None:
  109. if torch.cuda.is_current_stream_capturing():
  110. return orig_forward(self, x)
  111. st = self._dsv41_split = _build(self, orig_forward, group, x)
  112. if st is False:
  113. return orig_forward(self, x)
  114. proxy, ranges, width = st
  115. local = orig_forward(proxy, x)[0]
  116. if local.shape[1] < width:
  117. local = torch.nn.functional.pad(local, (0, width - local.shape[1]))
  118. gathered = group.all_gather(local.contiguous(), dim=-1) # [M, world * width]
  119. if all(b - a == width for a, b in ranges):
  120. return gathered, None
  121. parts = [gathered[:, r * width:r * width + (b - a)] for r, (a, b) in enumerate(ranges)]
  122. return torch.cat(parts, dim=-1), None
  123. cls.__init__ = __init__
  124. cls.forward = forward
  125. print(f"[replicated_split] armed for {SPEC} (rows <= {MAX_M})", flush=True)