File size: 9,240 Bytes
bcc8d35 | 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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | """Regression tests for the 2026-10-14 audit findings U03-3, U03-4, U05-1, U11-8, U11-9, U11-18
(LoRA export fidelity, adapter-to-base binding on resume, export-check gating and GPU pinning).
Run: cd /apps/development/rlhf/align && CUDA_VISIBLE_DEVICES= .venv/bin/python -m pytest \
tests/test_audit_g2_lora_export_fixes.py -q -p no:cacheprovider
"""
from __future__ import annotations
import importlib.util
import logging
import os
import pytest
import torch
from test_qwen3_parity import make_tiny, rescale_
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def _load(name):
spec = importlib.util.spec_from_file_location(name, os.path.join(REPO, "scripts", f"{name}.py"))
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
return m
exp = _load("qwen3_export_check")
# ---------------------------------------------------------------------------
# U05-1 / U03-3: the bf16 merge cast is lossy; the export must MEASURE and (optionally) refuse
# ---------------------------------------------------------------------------
def _bf16_lora(b_scale: float):
from align.ppo.lora import LoRAConfig, inject_lora, lora_modules
m = make_tiny().to(torch.bfloat16)
inject_lora(m, LoRAConfig(r=4, alpha=8.0), adapter="sft", generator=torch.Generator().manual_seed(1))
g = torch.Generator().manual_seed(2)
with torch.no_grad():
for _, mod in lora_modules(m):
mod.adapters["sft"].B.copy_(b_scale * torch.randn(mod.adapters["sft"].B.shape, generator=g))
return m
def test_export_reports_retained_delta_and_warns_when_bf16_drops_most_of_it(caplog):
from align.ppo.lora import export_merged_state_dict
m = _bf16_lora(1e-5) # the shipped PPO lr=1e-6 regime: delta << half an ulp of W
rep: dict = {}
with caplog.at_level(logging.WARNING):
export_merged_state_dict(m, {k: v.detach().clone() for k, v in m.state_dict().items()}, "sft", report=rep)
assert rep["cosine"] < 0.5 and rep["dropped_fraction"] > 0.5, rep
assert rep["layers"] > 0 and rep["min_layer_cosine"] <= rep["cosine"] + 1e-6
assert any("bf16" in r.message and "cosine" in r.message for r in caplog.records)
def test_export_refuses_below_min_cosine_and_passes_a_well_resolved_delta(caplog):
from align.ppo.lora import export_merged_state_dict
full = lambda m: {k: v.detach().clone() for k, v in m.state_dict().items()} # noqa: E731
small = _bf16_lora(1e-5)
with pytest.raises(ValueError, match="cosine"):
export_merged_state_dict(small, full(small), "sft", min_cosine=0.9)
big = _bf16_lora(5.0) # delta >> ulp: the cast preserves it
rep: dict = {}
with caplog.at_level(logging.WARNING):
export_merged_state_dict(big, full(big), "sft", report=rep, min_cosine=0.9)
assert rep["cosine"] > 0.95
assert not any("cosine" in r.message for r in caplog.records)
def test_export_of_an_exactly_zero_adapter_is_reported_not_nan():
from align.ppo.lora import export_merged_state_dict
m = _bf16_lora(0.0)
rep: dict = {}
export_merged_state_dict(m, {k: v.detach().clone() for k, v in m.state_dict().items()}, "sft", report=rep)
assert rep["delta_norm"] == 0.0 and rep["cosine"] == 1.0
# ---------------------------------------------------------------------------
# U03-4: a LoRA resume binds the adapter to the base it was trained on
# ---------------------------------------------------------------------------
def test_lora_payload_carries_a_base_fingerprint_and_a_foreign_base_is_refused(tmp_workdir, monkeypatch):
import test_qwen3_stage as T
tr, _, _ = T._build(tmp_workdir, "a")
payload = tr.train_module.optim_state_dict()
assert isinstance(payload.get("base_fingerprint"), str) and len(payload["base_fingerprint"]) == 64
# the same base resumes fine
tr_same, _, _ = T._build(tmp_workdir, "same")
tr_same.train_module.load_optim_state_dict(payload)
# a different base with an identical key set / shapes (repointed snapshot) must not
monkeypatch.setattr(T, "make_tiny", lambda: rescale_(make_tiny(), seed=7))
tr_other, _, _ = T._build(tmp_workdir, "other")
with pytest.raises(RuntimeError, match="base"):
tr_other.train_module.load_optim_state_dict(payload)
def test_legacy_payload_without_a_fingerprint_still_resumes_with_a_warning(tmp_workdir, caplog):
import test_qwen3_stage as T
tr, _, _ = T._build(tmp_workdir, "a")
payload = tr.train_module.optim_state_dict()
payload.pop("base_fingerprint")
with caplog.at_level(logging.WARNING):
tr.train_module.load_optim_state_dict(payload)
assert any("fingerprint" in r.message for r in caplog.records)
def test_fingerprint_tracks_content_not_just_keys():
from align.train.qwen3_stage import base_fingerprint
a, b = make_tiny(), make_tiny()
assert base_fingerprint(a.state_dict()) == base_fingerprint(b.state_dict())
with torch.no_grad():
b.model.layers[0].attn.q_norm.weight.mul_(1.001)
assert base_fingerprint(a.state_dict()) != base_fingerprint(b.state_dict())
other = rescale_(make_tiny(), seed=7)
assert base_fingerprint(a.state_dict()) != base_fingerprint(other.state_dict())
# ---------------------------------------------------------------------------
# U11-8: the exit status gates every check the script advertises
# ---------------------------------------------------------------------------
def _good_report():
return {
"verdict": {"passed": True},
"merge_vs_trainer_file": {"bitwise_mismatches": 0},
"snapshot_mapping_roundtrip": {"bitwise_equal": True},
"merge_changed_tensors_vs_snapshot": 12,
"merge_relative_weight_change": {"n": 12, "min": 1e-4, "mean": 4e-4, "max": 1e-3},
"adapter_vs_merged": {"verdict": {"passed": True}},
}
def test_export_ok_passes_a_complete_report():
assert exp.export_ok(_good_report()) is True
@pytest.mark.parametrize("mutate", [
lambda r: r.update(snapshot_mapping_roundtrip={"bitwise_equal": False}),
lambda r: r.update(merge_changed_tensors_vs_snapshot=0), # zero adapter
lambda r: r.update(merge_relative_weight_change={"n": 12, "min": 0.0, "mean": 1e-4, "max": 1e-3}),
lambda r: r.pop("merge_changed_tensors_vs_snapshot"), # check did not run
lambda r: r.pop("snapshot_mapping_roundtrip"),
lambda r: r.update(adapter_vs_merged={"verdict": {"passed": False}}),
lambda r: r.pop("adapter_vs_merged"),
lambda r: r.update(merge_vs_trainer_file={"bitwise_mismatches": 2}),
lambda r: r.update(verdict={"passed": False}),
lambda r: r.pop("verdict"),
])
def test_export_ok_fails_each_unmet_check(mutate):
r = _good_report()
mutate(r)
assert exp.export_ok(r) is False
# ---------------------------------------------------------------------------
# U11-9: merged-bf16 vs adapter-applied forward
# ---------------------------------------------------------------------------
def _lp(t=64, v=50, seed=0):
g = torch.Generator().manual_seed(seed)
return torch.log_softmax(3 * torch.randn(t, v, generator=g), -1)
def test_adapter_vs_merged_passes_identical_and_fails_a_dropped_delta():
a = _lp()
same = exp.evaluate_adapter_vs_merged(exp.compare_adapter_vs_merged([a], [a.clone()]))
assert same["passed"] is True
other = _lp(seed=5) # an unrelated distribution: what a merge that lost the delta looks like
bad = exp.evaluate_adapter_vs_merged(exp.compare_adapter_vs_merged([a], [other]))
assert bad["passed"] is False
near = a + 0.02 * torch.randn(a.shape, generator=torch.Generator().manual_seed(3))
ok = exp.evaluate_adapter_vs_merged(exp.compare_adapter_vs_merged([a], [torch.log_softmax(near, -1)]))
assert ok["passed"] is True
def test_adapter_vs_merged_shape_mismatch_raises():
with pytest.raises(ValueError):
exp.compare_adapter_vs_merged([_lp(8)], [_lp(9)])
def test_merge_fidelity_stats_expose_the_dead_zone():
from align.ppo.lora import lora_state_dict
m = _bf16_lora(1e-5)
base = {k: v.detach().clone() for k, v in m.state_dict().items() if ".adapters." not in k}
lora = lora_state_dict(m, "sft")
merged = exp.merge_lora_state(base, lora, 2.0)
st = exp.merge_fidelity_stats(base, merged, lora, 2.0)
assert st["cosine"] < 0.5 and st["dropped_fraction"] > 0.5 and st["layers"] > 0
# ---------------------------------------------------------------------------
# U11-18: the vLLM child is pinned to the PARENT's first visible device
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("cvd,expect", [("2,3", "2"), ("3", "3"), (" 1 , 2", "1"), ("GPU-abc,GPU-def", "GPU-abc"),
(None, "0"), ("", "0")])
def test_first_visible_gpu_follows_the_parent_pinning(cvd, expect):
env = {} if cvd is None else {"CUDA_VISIBLE_DEVICES": cvd}
assert exp._first_visible_gpu(env) == expect
def test_rollout_env_uses_the_pinned_device_not_physical_zero(monkeypatch):
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "2,3")
assert exp._rollout_env(exp._first_visible_gpu())["CUDA_VISIBLE_DEVICES"] == "2"
|