forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_code_review_deferred.py
More file actions
177 lines (133 loc) · 5.72 KB
/
Copy pathtest_code_review_deferred.py
File metadata and controls
177 lines (133 loc) · 5.72 KB
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Regression tests for the 3 items deferred in the first code-review pass:
windowed EMA smoothing, the hardware-fit OOM preflight, and real MoD
token-dropping.
"""
from __future__ import annotations
import pytest
from soup_cli.config.loader import load_config_from_string
# ─────────────────────────── EMA smoothing window ───────────────────────────
def test_ema_smoothing_now_respects_window_size():
from soup_cli.utils.reward_hack_control import smooth_signal
# Windowed EMA folds alpha over the whole retained window, so a longer
# window (bounded by reward_hack_smoothing_window) changes the result.
assert smooth_signal(0.4, [0.1, 0.2], method="ema") == pytest.approx(0.275)
assert smooth_signal(0.4, [0.2], method="ema") == pytest.approx(0.3)
assert smooth_signal(1.0, [0.0], method="ema") != smooth_signal(
1.0, [1.0, 0.0, 0.0], method="ema"
)
# ─────────────────────────── hardware-fit OOM preflight ─────────────────────
_FIT_YAML = """
base: meta-llama/Llama-2-7b-hf
task: sft
data:
train: train.jsonl
max_length: 2048
training:
batch_size: 8
quantization: none
"""
_AUTO_BS_YAML = """
base: meta-llama/Llama-2-7b-hf
task: sft
data:
train: train.jsonl
max_length: 2048
training:
batch_size: auto
quantization: none
"""
def test_build_hardware_fit_input_from_config():
from soup_cli.commands.train import _build_hardware_fit_input
inp = _build_hardware_fit_input(load_config_from_string(_FIT_YAML))
assert inp is not None
assert inp.batch_size == 8
assert inp.seq_len == 2048
assert inp.params_b >= 6.0 # a 7B base
# batch_size="auto" isn't statically predictable -> skip the gate.
assert _build_hardware_fit_input(load_config_from_string(_AUTO_BS_YAML)) is None
def test_hardware_fit_preflight_gate_and_optout():
import typer
from soup_cli.commands import train as train_mod
cfg = load_config_from_string(_FIT_YAML)
# 4 GB can't hold a 7B model -> refuse (exit) by default.
with pytest.raises(typer.Exit):
train_mod._hardware_fit_preflight(
cfg, {"memory_total_bytes": int(4e9)}, allow_oom_attempt=False
)
# --allow-oom-attempt -> warn, don't refuse.
train_mod._hardware_fit_preflight(
cfg, {"memory_total_bytes": int(4e9)}, allow_oom_attempt=True
)
# No detectable VRAM (CPU / CI) -> skip silently.
train_mod._hardware_fit_preflight(
cfg, {"memory_total_bytes": 0}, allow_oom_attempt=False
)
# Plenty of VRAM -> fits, no refuse.
train_mod._hardware_fit_preflight(
cfg, {"memory_total_bytes": int(500e9)}, allow_oom_attempt=False
)
# ─────────────────────────── MoD real token-dropping ────────────────────────
def _mod_pieces(hidden: int, capacity_factor: float):
import torch.nn as nn
from soup_cli.utils.mod import _make_mod_forward
class _RecordingLayer(nn.Module):
def __init__(self) -> None:
super().__init__()
self.seqs: list = []
self.kw: list = []
self.lin = nn.Linear(hidden, hidden)
def forward(self, hs, *args, **kwargs):
self.seqs.append(hs.shape[1])
self.kw.append(kwargs)
return self.lin(hs) # per-token (no attention mixing) — plumbing test
layer = _RecordingLayer()
router = nn.Linear(hidden, 1, bias=False)
fwd = _make_mod_forward(layer.forward, router, capacity_factor)
return layer, router, fwd
def test_mod_forward_gathers_and_saves_compute():
torch = pytest.importorskip("torch")
hidden, seq, batch = 4, 8, 2
torch.manual_seed(0)
layer, router, fwd = _mod_pieces(hidden, 0.5) # cap = 4
x = torch.randn(batch, seq, hidden)
cap = 4
topk = torch.topk(router(x).squeeze(-1), k=cap, dim=-1).indices
topk, _ = torch.sort(topk, dim=-1)
out = fwd(x)
# The block ran on ONLY the cap tokens — the whole point (real savings).
assert layer.seqs == [cap]
assert out.shape == x.shape
# Unselected tokens pass through unchanged.
selected = torch.zeros(batch, seq, dtype=torch.bool)
selected.scatter_(1, topk, True)
for b in range(batch):
for t in range(seq):
if not selected[b, t]:
assert torch.allclose(out[b, t], x[b, t])
def test_mod_forward_gathers_positional_inputs():
torch = pytest.importorskip("torch")
hidden, seq, batch, head_dim, heads = 4, 8, 2, 6, 1
torch.manual_seed(1)
layer, router, fwd = _mod_pieces(hidden, 0.5) # cap = 4
x = torch.randn(batch, seq, hidden)
cos = torch.randn(batch, seq, head_dim)
sin = torch.randn(batch, seq, head_dim)
attn = torch.zeros(batch, heads, seq, seq)
fwd(x, position_embeddings=(cos, sin), attention_mask=attn)
kw = layer.kw[-1]
# RoPE + mask narrowed to the sub-sequence (cap), proving real savings with
# correctly-gathered positional inputs.
assert layer.seqs == [4]
assert kw["position_embeddings"][0].shape == (batch, 4, head_dim)
assert kw["attention_mask"].shape == (batch, heads, 4, 4)
def test_mod_forward_falls_back_on_positional_args():
torch = pytest.importorskip("torch")
hidden, seq, batch = 4, 8, 2
torch.manual_seed(2)
layer, router, fwd = _mod_pieces(hidden, 0.5)
x = torch.randn(batch, seq, hidden)
# A positional forward arg aborts the gather path -> full block (seq == T)
# + gate-blend fallback (correct, no savings).
out = fwd(x, object())
assert layer.seqs == [seq]
assert out.shape == x.shape