Fastfill-test / code /fastfill /v2 /tests /test_active_objective_distributed.py
XXXpilar's picture
FastFill v4 base (step 6552): LoRA adapter, geometry heads, tokenizer, training code, predict.py
e6f80f3 verified
Raw History Blame Contribute Delete
5.56 kB
"""Real Accelerator/Gloo entry-point tests with empty ranks and windows."""
from copy import deepcopy
from datetime import timedelta
import json
import os
from pathlib import Path
import sys
import pytest
import torch
from fastfill.v2.tests.test_active_objective_training import _row, _yaw_config
def _worker(rank, rendezvous, directory, mode):
from torch import distributed as dist
from fastfill.v2 import train
os.environ.update(RANK=str(rank), WORLD_SIZE="2", LOCAL_RANK=str(rank), LOCAL_WORLD_SIZE="2",
OMP_NUM_THREADS="1", MASTER_ADDR="127.0.0.1", MASTER_PORT="29513")
dist.init_process_group("gloo", init_method=rendezvous, rank=rank, world_size=2,
timeout=timedelta(seconds=20))
patches = pytest.MonkeyPatch()
try:
if sys.platform == "darwin":
# torch 2.13 barrier(device_ids=[...]) chooses the available MPS
# accelerator even for a CPU/Gloo group. This CPU test explicitly
# selects CPU discovery; collectives/backward remain real Gloo.
patches.setattr(torch._C, "_get_accelerator", lambda: torch.device("cpu"))
root = Path(directory)
rows_count = {"peer_empty": 2, "last_empty": 4, "empty_then_valid": 8, "all_empty": 4}[mode]
data = root / "train.jsonl"
if rank == 0:
data.write_text("".join(json.dumps(_row(str(i))) + "\n" for i in range(rows_count)))
dist.barrier()
original_collate, original_preflight, original_loader = train.collate_samples, train._preflight, train.DataLoader
state = {"after_preflight": False}
def collate(rows, *args, **kwargs):
batch = original_collate(rows, *args, **kwargs)
if state["after_preflight"]:
yaw = batch["validity"]["yaw"].clone()
for i, row in enumerate(rows):
identity = int(row["provenance"]["scene_id"])
empty = (mode == "all_empty" or (mode == "peer_empty" and rank == 0)
or (mode == "last_empty" and identity >= 2)
or (mode == "empty_then_valid" and identity < 4))
if empty:
yaw[i] = False
batch = {**batch, "validity": {**batch["validity"], "yaw": yaw}}
return batch
def preflight(*args, **kwargs):
state["after_preflight"] = False
result = original_preflight(*args, **kwargs)
state["after_preflight"] = True
return result
def loader(rows, **kwargs):
return original_loader(rows, **{**kwargs, "shuffle": False})
models, before = [], []
original_build = train.build_model
def build(config):
model = original_build(config)
models.append(model)
before.append(deepcopy(model.state_dict()))
return model
patches.setattr(train, "collate_samples", collate)
patches.setattr(train, "_preflight", preflight)
patches.setattr(train, "DataLoader", loader)
patches.setattr(train, "build_model", build)
config = _yaw_config()
config["training"]["gradient_accumulation_steps"] = 1 if mode == "peer_empty" else 2
if mode == "all_empty":
with pytest.raises(RuntimeError, match="no active objective"):
train.run_training(config, data, root / "run")
changed = sum(not torch.equal(before[0][name], value) for name, value in models[0].state_dict().items())
assert changed == 0
observation = {"optimizer_updates": 0, "changed_parameter_tensors": changed}
else:
logs = train.run_training(config, data, root / "run")
assert len(logs) == 1 and logs[0]["step"] == 1
assert logs[0]["active_objective_count"] == {"peer_empty": 1, "last_empty": 2, "empty_then_valid": 4}[mode]
assert logs[0]["gradient_norms"]["yaw_logits_head"] > 0
changed = sum(not torch.equal(before[0][name], value) for name, value in models[0].state_dict().items())
assert changed > 0
observation = {"optimizer_updates": 1, "active_objective_count": logs[0]["active_objective_count"],
"changed_parameter_tensors": changed}
if rank == 0:
manifest = json.loads((root / "run/run_manifest.json").read_text())
assert manifest["world_size"] == 2
assert manifest["skipped_no_objective_windows"] == int(mode == "empty_then_valid")
(root / f"rank-{rank}.json").write_text(json.dumps(observation, indent=2) + "\n")
finally:
patches.undo()
if dist.is_initialized():
dist.destroy_process_group()
@pytest.mark.skipif(not torch.distributed.is_available() or not torch.distributed.is_gloo_available(), reason="requires Gloo")
@pytest.mark.parametrize("mode", ["peer_empty", "last_empty", "empty_then_valid", "all_empty"])
def test_actual_two_rank_objective_windows(tmp_path, monkeypatch, mode):
monkeypatch.setenv("GLOO_SOCKET_IFNAME", "lo0" if sys.platform == "darwin" else "lo")
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "") # CPU Gloo ranks: with one visible GPU, rank 1 would ask for cuda:1
torch.multiprocessing.spawn(_worker, args=("file://" + str(tmp_path / "rendezvous"), str(tmp_path), mode),
nprocs=2, join=True)
assert all((tmp_path / f"rank-{rank}.json").exists() for rank in range(2))