Fastfill-test / code /fastfill /v2 /tests /test_execution.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
6.18 kB
import json
import pytest
import torch
from fastfill.v2.io import ensure_disjoint, read_samples, safe_output
from fastfill.v2.losses import GeometryCriterion, LossConfig
from fastfill.v2.tests.test_supervision import batch, prediction
def sample():
return {"schema_version": "fastfill.v2", "condition": {"schema_version": "fastfill.v2", "room": {
"frame": "right_handed_z_up", "floor_polygon_xy_m": [[0, 0], [5, 0], [5, 4], [0, 4]],
"floor_z_m": 0., "floor_known": True, "height_m": 2.8},
"objects": [{"id": "a", "category": "chair", "description": "a chair", "support_parent": "floor"}], "constraints": []},
"target": {"schema_version": "fastfill.v2", "objects": [{"id": "a", "target_size_local_m": [1., 1., 1.],
"bottom_center_m": [2., 1., 0.], "yaw_rad": 0.}]},
"validity": {"position": [[True] * 3], "size": [[True] * 3], "yaw": [True], "yaw_symmetry_order": [1]},
"provenance": {"source": "fixture", "house_id": "h1", "scene_id": "s1", "split": "train"}}
def test_io_source_protection_split_guard(tmp_path):
with pytest.raises(ValueError):
safe_output("/Volumes/harddisk/3D_Room_Collections/test-output")
with pytest.raises(FileExistsError):
safe_output(tmp_path)
row = sample()
with pytest.raises(ValueError):
ensure_disjoint([row], [row])
path = tmp_path / "data.jsonl"
path.write_text(json.dumps({**row, "provenance": {**row["provenance"], "split": "test"}}) + "\n")
with pytest.raises(ValueError):
read_samples(path, training=True)
def test_optional_regularizers_are_separate_and_differentiable():
b = batch()
b["conditions"] = [{"objects": b["objects"][0], "constraints": [], "room": {
"floor_polygon_xy_m": [[0, 0], [1, 0], [1, 1], [0, 1]], "boundary_known": True}}]
p = prediction(b)
result = GeometryCriterion(LossConfig(collision=1., boundary=1., box=1.))(p, b)
assert result["collision"] > 0
assert result["boundary"] > 0
assert result["box"] > 0
result["loss"].backward()
assert torch.isfinite(p["position_normalized"].grad).all()
b["conditions"][0]["room"]["boundary_known"] = False
with pytest.raises(ValueError, match="unknown"):
GeometryCriterion(LossConfig(boundary=1.))(p, b)
def test_train_checkpoint_inference_evaluation_end_to_end(tmp_path):
from fastfill.v2.train import run_training
from fastfill.v2.evaluate import run_evaluation
row = sample()
path = tmp_path / "train.jsonl"
path.write_text(json.dumps(row) + "\n")
config = {"model": {"backbone": "tiny", "decoder_dim": 16, "decoder_heads": 2,
"decoder_layers": 1, "tiny_hidden_size": 16, "lora_rank": 0},
"training": {"cpu": True, "steps": 20, "batch_size": 1, "learning_rate": .01,
"max_length": 4096, "checkpoint_every": 0, "validate_every": 0, "cpu_threads": 1}}
run = tmp_path / "trained"
logs = run_training(config, path, run)
assert logs[-1]["loss"] < logs[0]["loss"]
assert all(logs[0]["gradient_norms"][name] > 0 for name in ("decoder", "backbone", "size_head", "position_head", "yaw_logits_head", "yaw_residual_head"))
test_path = tmp_path / "test.jsonl"
test_path.write_text(json.dumps({**row, "provenance": {**row["provenance"], "split": "test", "house_id": "h2"}}) + "\n")
report = run_evaluation(test_path, tmp_path / "evaluation", checkpoint=run / "model")
assert report["requests"] == 1
assert report["model"]["schema_success"] == 1
assert report["model"]["reference"]["log_size_error"]["mean"] is not None
assert report["model"]["reference"]["bev_iou"]["valid_objects"] == 1
with pytest.raises(FileExistsError):
run_training(config, path, run, dry_run=True)
def test_evaluation_keeps_failed_requests_and_atomic_asset_stages(tmp_path):
from fastfill.v2.evaluate import run_evaluation
row = sample()
data = tmp_path / "evaluation.jsonl"
data.write_text(json.dumps(row) + "\n" + json.dumps(row) + "\n")
layouts = tmp_path / "predictions.jsonl"
layouts.write_text(json.dumps(row["target"]) + "\n" + json.dumps({"schema_version": "fastfill.v2", "objects": []}) + "\n")
catalog = tmp_path / "catalog.json"
catalog.write_text(json.dumps([{"ref": "chair-asset", "category": "chair", "actual_size_local_m": [1, 1, 1],
"semantic_front_local": [1, 0, 0], "capabilities": []}]))
report = run_evaluation(data, tmp_path / "results", predictions=layouts, catalog=catalog, commit_in_memory=True)
assert report["requests"] == 2
assert report["model"]["schema_success"] == .5
assert report["system"]["final_commit"] == .5
assert report["failed_requests"] == 1
assert report["model"]["reference"]["bottom_center_error_m"]["mean"] == 0
records = [json.loads(line) for line in (tmp_path / "results/outcomes.jsonl").read_text().splitlines()]
assert records[0]["runtime"]["raw_model_output"] == row["target"]
assert records[0]["runtime"]["metrics"]["asset"]["first_pass_actual_geometry_validation"]
def test_global_count_compensates_ddp_gradient_average(monkeypatch):
from fastfill.v2.losses import _mean
monkeypatch.setattr(torch.distributed, "is_initialized", lambda: True)
monkeypatch.setattr(torch.distributed, "get_world_size", lambda: 2)
# Local rank has 1 label; peer has 3: global denominator is 4.
monkeypatch.setattr(torch.distributed, "all_reduce", lambda t: t.add_(3))
local_sum = torch.tensor(6., requires_grad=True)
value, count = _mean(local_sum, 1)
assert count == 4
value.backward()
assert local_sum.grad.item() == .5 # DDP later averages by 2 -> 1/4 per-label gradient.
def test_invalid_target_geometry_is_failure_with_schema_success():
from fastfill.v2.evaluate import summarize
report = summarize([{"model": {"schema_success": True, "requested_ids_exactly_once": True,
"positive_valid_size": True, "target_geometry_valid": False},
"evaluation_wall_time_ms": 1.}])
assert report["failed_requests"] == 1
assert report["inference_failed_requests"] == 0