Spaces:
Paused
Paused
File size: 5,531 Bytes
6f75f79 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | """Memory accounting + a peak-memory gate that anyone can run on stock PyTorch.
Two things live here:
* memory_report(model): static byte accounting of persistent state (params + buffers),
plus what the counter weights would cost packed to 6 bits.
* peak_training_memory(step_fn): the *dynamic* peak during a real train step, measured
with torch.cuda.max_memory_allocated on CUDA. This is the portable analogue of the
engine's memory truth gate: it lets a user verify the counter optimizer actually uses
less training memory than a dense AdamW baseline, on their own GPU, with no custom build.
Honesty: the pure-PyTorch counter layer decodes states to dense tensors around the GEMM, so
its *measured* peak is not yet the 0.75 byte/weight figure -- it still beats dense+Adam on
the optimizer-state pool (no FP master, no Adam moments), which is what these helpers show.
The sub-byte training peak needs the Triton/CUDA packed kernel (see README "Roadmap").
"""
from __future__ import annotations
from typing import Callable, Iterable
import torch
import torch.nn as nn
from .counter import CompactCounterLinear
__all__ = ["memory_report", "fmt_bytes", "peak_training_memory", "compare_training_peak"]
def fmt_bytes(n: float) -> str:
units = ["B", "KiB", "MiB", "GiB", "TiB"]
i = 0
while n >= 1024 and i < len(units) - 1:
n /= 1024
i += 1
return f"{n:.2f} {units[i]}"
def _unique(items: Iterable[torch.Tensor]) -> list[torch.Tensor]:
seen: set[int] = set()
out: list[torch.Tensor] = []
for t in items:
if t is None:
continue
ptr = int(t.untyped_storage().data_ptr()) if t.numel() else id(t)
if ptr not in seen:
seen.add(ptr)
out.append(t)
return out
def memory_report(model: nn.Module) -> dict[str, float]:
params = _unique(model.parameters())
buffers = _unique(model.buffers())
counter = [m for m in model.modules() if isinstance(m, CompactCounterLinear)]
# logical weight count (in*out), independent of whether state is stored 1 byte/weight
# (CompactCounterLinear) or packed 0.75 byte/weight (PackedRMSCounterLinear).
counter_weights = sum(m.in_features * m.out_features for m in counter)
fp_params = sum(int(p.numel()) for p in params)
parameter_bytes = sum(p.numel() * p.element_size() for p in params)
buffer_bytes = sum(b.numel() * b.element_size() for b in buffers)
return {
"counter_weights": float(counter_weights),
"fp_parameters": float(fp_params),
"parameter_bytes": float(parameter_bytes),
"buffer_bytes": float(buffer_bytes),
"persistent_bytes": float(parameter_bytes + buffer_bytes),
"counter_packed_6bit_bytes": float(counter_weights * 6 / 8),
"dense_bf16_adam_rule_of_thumb_bytes": float(16 * (counter_weights + fp_params)),
}
def peak_training_memory(step_fn: Callable[[], None], device: torch.device) -> int:
"""Run one training step and return the peak allocated bytes during it.
On CUDA this uses torch.cuda.max_memory_allocated (a true device peak). On CPU torch has
no allocator peak API, so this returns 0 and callers should fall back to memory_report.
"""
if device.type == "cuda":
torch.cuda.synchronize(device)
torch.cuda.reset_peak_memory_stats(device)
step_fn()
torch.cuda.synchronize(device)
return int(torch.cuda.max_memory_allocated(device))
step_fn()
return 0
def compare_training_peak(build_counter: Callable[[], nn.Module],
build_dense: Callable[[], "tuple[nn.Module, torch.optim.Optimizer]"],
step_inputs: Callable[[], tuple],
device: torch.device) -> dict[str, float]:
"""Measure training-step peak memory for a counter model vs a dense+AdamW baseline.
build_counter() -> counter model (self-updating in backward, no optimizer).
build_dense() -> (dense model, its AdamW optimizer).
step_inputs() -> (x, y) batch on `device`.
Returns a dict with both peaks (CUDA) or both persistent-byte reports (CPU fallback).
"""
import torch.nn.functional as F
# counter
cm = build_counter().to(device).train()
x, y = step_inputs()
def counter_step():
logits = cm(x)
loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), y.reshape(-1))
loss.backward() # counter layers self-update here; no optimizer.step()
# dense + AdamW
dm, opt = build_dense()
dm = dm.to(device).train()
def dense_step():
opt.zero_grad(set_to_none=True)
logits = dm(x)
loss = F.cross_entropy(logits.reshape(-1, logits.size(-1)), y.reshape(-1))
loss.backward()
opt.step()
if device.type == "cuda":
counter_peak = peak_training_memory(counter_step, device)
dense_peak = peak_training_memory(dense_step, device)
return {
"counter_peak_bytes": float(counter_peak),
"dense_adam_peak_bytes": float(dense_peak),
"ratio_dense_over_counter": float(dense_peak / max(counter_peak, 1)),
}
# CPU: no allocator peak; report persistent state instead.
counter_step(); dense_step()
cr, dr = memory_report(cm), memory_report(dm)
return {
"counter_persistent_bytes": cr["persistent_bytes"],
"dense_persistent_bytes": dr["persistent_bytes"],
"note": float("nan"), # CPU: see memory_report; peak needs CUDA
}
|