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
    }