remdm-planner-minihack / tests /test_gpu_agreement.py
AnonMLuser's picture
Anonymous artefact release
e0eb79a verified
Raw
History Blame Contribute Delete
5.66 kB
"""GPU-gated agreement and restore tests (step 8).
Sources: spec-training §5 (recipe runs with use_amp and torch_compile
enabled - their numerics must agree with the plain fp32 eager path),
spec-config §6.3/§6.4 (published checkpoint layout). Skipped cleanly
without CUDA or without the downloaded released checkpoints. The
craftax twin checks CUDA-vs-CPU agreement of its JAX loss/sampler and
the released PPO-expert restore.
"""
from __future__ import annotations
from pathlib import Path
from types import SimpleNamespace
import pytest
import torch
from src.diffusion.loss import mdlm_loss
from src.diffusion.schedules import get_schedule
from src.models.denoiser import make_model
from tests.conftest import requires_cuda
PROJECT_ROOT = Path(__file__).resolve().parents[1]
_HF_ONLINE = (
PROJECT_ROOT
/ "checkpoints/hf/checkpoints/online/Minihack-Online-Diffusion-DAgger-100M"
)
def _recipe_cfg():
"""The shipped recipe config (the model's stream geometry is coupled
to the real crop/map dims, so tests use the real architecture)."""
from src.config import load_config
return load_config(None, {"use_wandb": False})
def _batch(cfg, device, b=8):
g = torch.Generator().manual_seed(0)
local = torch.randint(0, 100, (b, 9, 9), generator=g).to(device)
glob = torch.randint(0, 100, (b, 21, 79), generator=g).to(device)
zt = torch.randint(0, cfg.action_dim + 1, (b, cfg.seq_len), generator=g).to(device)
t = torch.full((b,), 5, dtype=torch.long, device=device)
return local, glob, zt, t
@requires_cuda
def test_amp_forward_agrees_with_fp32():
"""The recipe trains under AMP autocast (use_amp: true,
spec-training §5); the autocast forward must agree with the fp32
forward on identical inputs within half-precision tolerance
(fp16 has ~1e-3 relative epsilon; bound 2e-2 on the logits).
"""
torch.manual_seed(0)
cfg = _recipe_cfg()
model = make_model(cfg).cuda().eval()
local, glob, zt, t = _batch(cfg, "cuda")
with torch.no_grad():
full = model(local, glob, zt, t)["actions"].float()
with torch.amp.autocast("cuda"):
amp = model(local, glob, zt, t)["actions"].float()
denom = full.abs().max().clamp(min=1.0)
assert float((full - amp).abs().max() / denom) < 2e-2
@requires_cuda
def test_loss_agrees_between_cuda_and_cpu():
"""mdlm_loss on identical explicit inputs (no RNG inside) agrees
between CPU and CUDA within float32 reassociation tolerance
(rel 1e-4): the estimator's mathematics is backend-invariant
(spec-method §3.1/§3.4)."""
torch.manual_seed(0)
cfg = SimpleNamespace(action_dim=6, mask_token=6, pad_token=7, seq_len=16)
b = 8
g = torch.Generator().manual_seed(3)
logits = torch.randn(b, cfg.seq_len, cfg.action_dim + 2, generator=g)
x0 = torch.randint(0, cfg.action_dim, (b, cfg.seq_len), generator=g)
zt = torch.where(
torch.rand(b, cfg.seq_len, generator=g) < 0.5, cfg.mask_token, x0
)
t = torch.rand(b, generator=g)
args = {"mask_token": cfg.mask_token, "pad_token": cfg.pad_token,
"schedule_fn": get_schedule("cosine")}
cpu = float(mdlm_loss(logits, x0, zt, t, **args))
gpu = float(
mdlm_loss(logits.cuda(), x0.cuda(), zt.cuda(), t.cuda(), **args)
)
assert gpu == pytest.approx(cpu, rel=1e-4)
@requires_cuda
@pytest.mark.slow
def test_compiled_forward_agrees_with_eager(monkeypatch):
"""The recipe trains with torch_compile: true (spec-training §5);
the compiled forward must agree with eager on identical inputs
(rel 1e-3, allowing TF32-class matmul rounding differences in
inductor kernels).
The conftest isolation fixture blanks CUDA_VISIBLE_DEVICES (the
parent process keeps its pre-initialised CUDA context, but
inductor's triton compile workers are fresh subprocesses and would
see no GPU), so the variable is restored for this test.
"""
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "0")
torch.manual_seed(0)
cfg = _recipe_cfg()
model = make_model(cfg).cuda().eval()
local, glob, zt, t = _batch(cfg, "cuda")
with torch.no_grad():
eager = model(local, glob, zt, t)["actions"].float()
compiled = torch.compile(model)
out = compiled(local, glob, zt, t)["actions"].float()
denom = eager.abs().max().clamp(min=1.0)
assert float((eager - out).abs().max() / denom) < 1e-3
@requires_cuda
@pytest.mark.skipif(
not (_HF_ONLINE / "iter563.pth").exists(),
reason="released checkpoints not downloaded to checkpoints/hf/",
)
def test_released_checkpoint_restores_and_runs_on_gpu():
"""The released DAgger checkpoint restores on GPU with its own
config snapshot and produces a finite forward pass (spec-config
§6.3: evaluate with the checkpoint's own config snapshot; step-7
live-service check)."""
import yaml
snap = yaml.safe_load((_HF_ONLINE / "config.yaml").read_text())
cfg = SimpleNamespace(**snap)
model = make_model(cfg).cuda().eval()
ckpt = torch.load(_HF_ONLINE / "iter563.pth", map_location="cuda",
weights_only=False)
assert "model_state_dict" in ckpt
model.load_state_dict(ckpt["model_state_dict"])
b = 2
local = torch.zeros(b, 9, 9, dtype=torch.long, device="cuda")
glob = torch.zeros(b, 21, 79, dtype=torch.long, device="cuda")
zt = torch.full((b, cfg.seq_len), cfg.mask_token, dtype=torch.long,
device="cuda")
t = torch.zeros(b, dtype=torch.long, device="cuda")
with torch.no_grad():
out = model(local, glob, zt, t)["actions"]
assert bool(torch.isfinite(out).all())