Spaces:
Sleeping
Sleeping
Download swarm/compression.py from abhimittal/swarm-server: direct link, hf CLI and curl.
- Browser
- Download file 5.46 kB
-
https://huggingface.co/spaces/abhimittal/swarm-server/resolve/main/swarm/compression.py
- Command line
-
hf download hf://spaces/abhimittal/swarm-server/swarm/compression.py
-
curl -L -o compression.py https://huggingface.co/spaces/abhimittal/swarm-server/resolve/main/swarm/compression.py
5.46 kB
| """Gradient compression β the bandwidth kernel. | |
| On a real GPU cluster the interconnect is NVLink at ~600 GB/s. Here it is HTTP | |
| over the public internet at maybe 10 MB/s, so **bytes on the wire are the | |
| bottleneck**, not FLOPs. A worker that spends 200 ms computing a gradient and | |
| 8 s uploading it is 97% idle. | |
| The fix is standard in distributed training but rarely built from scratch: | |
| * **Top-K sparsification** β only the ``ratio`` fraction of gradient entries with | |
| the largest magnitude are transmitted (as index/value pairs). At ratio=0.01 | |
| that is a ~50x reduction in payload. | |
| * **Error feedback** β the entries we *dropped* are not discarded; they are kept | |
| in a local residual buffer and added to the next step's gradient. A small | |
| consistent gradient direction therefore accumulates until it becomes large | |
| enough to be sent. Without this, sparsification biases training and | |
| convergence stalls; with it, Top-K is near-lossless in practice. | |
| (Seide et al. 2014; Lin et al., "Deep Gradient Compression", 2018.) | |
| * **int8 quantization** β the surviving values are optionally quantized to 8-bit | |
| with a per-tensor scale, for another ~4x on top. | |
| The server reconstructs a dense gradient before aggregating, so compression is | |
| entirely transparent to :class:`~server.aggregator.GradientAggregator`. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from typing import Dict, Mapping, Optional, Sequence, Tuple | |
| import torch | |
| # Wire-format suffixes. safetensors payloads are flat {name: tensor} maps, so a | |
| # compressed tensor is split across a few synthetic keys. | |
| IDX = "::idx" | |
| VAL = "::val" | |
| SCALE = "::scale" | |
| # Values for the X-Compression header. | |
| NONE = "none" | |
| TOPK = "topk" | |
| TOPK_INT8 = "topk-int8" | |
| MODES = (NONE, TOPK, TOPK_INT8) | |
| def _numel(shape: Sequence[int]) -> int: | |
| return int(math.prod(shape)) if len(shape) else 1 | |
| def topk_count(numel: int, ratio: float) -> int: | |
| """How many entries survive Top-K for a tensor of ``numel`` elements.""" | |
| if ratio >= 1.0: | |
| return numel | |
| return max(1, min(numel, int(numel * ratio))) | |
| def compress( | |
| tensors: Mapping[str, torch.Tensor], | |
| mode: str = TOPK, | |
| ratio: float = 0.01, | |
| residual: Optional[Dict[str, torch.Tensor]] = None, | |
| ) -> Dict[str, torch.Tensor]: | |
| """Compress ``{name: dense_gradient}`` into a sparse wire payload. | |
| ``residual`` (error feedback) is read *and updated in place* when provided: | |
| dropped mass is carried into the next call so no gradient signal is lost, | |
| only delayed. | |
| """ | |
| if mode == NONE: | |
| return dict(tensors) | |
| if mode not in MODES: | |
| raise ValueError(f"unknown compression mode {mode!r}; expected one of {MODES}") | |
| quantize = mode == TOPK_INT8 | |
| out: Dict[str, torch.Tensor] = {} | |
| for name, grad in tensors.items(): | |
| g = grad.detach().to(torch.float32) | |
| if residual is not None: | |
| prev = residual.get(name) | |
| if prev is not None: | |
| g = g + prev | |
| flat = g.reshape(-1) | |
| k = topk_count(flat.numel(), ratio) | |
| # Select by magnitude; sorted=False is cheaper and order is irrelevant. | |
| _, idx = torch.topk(flat.abs(), k, sorted=False) | |
| vals = flat[idx] | |
| if residual is not None: | |
| # Everything we did NOT send stays behind for the next step. | |
| carry = flat.clone() | |
| carry[idx] = 0.0 | |
| residual[name] = carry.view_as(grad) | |
| if quantize: | |
| peak = vals.abs().max() | |
| scale = (peak / 127.0) if peak > 0 else torch.tensor(1.0) | |
| out[name + VAL] = torch.round(vals / scale).clamp_(-127, 127).to(torch.int8) | |
| out[name + SCALE] = scale.reshape(1).to(torch.float32) | |
| else: | |
| out[name + VAL] = vals | |
| out[name + IDX] = idx.to(torch.int64) | |
| return out | |
| def decompress( | |
| payload: Mapping[str, torch.Tensor], | |
| shapes: Mapping[str, torch.Size], | |
| mode: str = TOPK, | |
| ) -> Dict[str, torch.Tensor]: | |
| """Rebuild dense gradients from a sparse payload. | |
| Shapes come from the server's own model, so they never travel on the wire. | |
| """ | |
| if mode == NONE: | |
| return dict(payload) | |
| if mode not in MODES: | |
| raise ValueError(f"unknown compression mode {mode!r}; expected one of {MODES}") | |
| out: Dict[str, torch.Tensor] = {} | |
| for name, shape in shapes.items(): | |
| try: | |
| idx = payload[name + IDX] | |
| val = payload[name + VAL] | |
| except KeyError as e: # pragma: no cover - surfaced as a 400 by the app | |
| raise KeyError(f"compressed payload missing entry for {name!r}: {e}") from e | |
| scale = payload.get(name + SCALE) | |
| if scale is not None: | |
| val = val.to(torch.float32) * float(scale.reshape(-1)[0]) | |
| flat = torch.zeros(_numel(shape), dtype=torch.float32) | |
| flat[idx.to(torch.int64)] = val.to(torch.float32) | |
| out[name] = flat.view(shape) | |
| return out | |
| def payload_bytes(payload: Mapping[str, torch.Tensor]) -> int: | |
| """Total tensor bytes in a payload (excludes safetensors' small header).""" | |
| return sum(t.numel() * t.element_size() for t in payload.values()) | |
| def compression_report( | |
| dense: Mapping[str, torch.Tensor], wire: Mapping[str, torch.Tensor] | |
| ) -> Tuple[int, int, float]: | |
| """(dense_bytes, wire_bytes, ratio) β ratio is how many times smaller the wire is.""" | |
| d = payload_bytes(dense) | |
| w = payload_bytes(wire) | |
| return d, w, (d / w if w else 1.0) | |