kharki's picture
Upload folder using huggingface_hub
6f75f79 verified
Raw History Blame Contribute Delete
5.53 kB
"""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
}