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"