Spaces:
Paused
Paused
Download src/memory_native/memory.py from kharki/mn-zerogpu: direct link, hf CLI and curl.
- Browser
- Download file 5.53 kB
-
https://huggingface.co/spaces/kharki/mn-zerogpu/resolve/main/src/memory_native/memory.py
- Command line
-
hf download hf://spaces/kharki/mn-zerogpu/src/memory_native/memory.py
-
curl -L -o memory.py https://huggingface.co/spaces/kharki/mn-zerogpu/resolve/main/src/memory_native/memory.py
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 | |
| } | |