Buckets:
| """Distributed direct/interleaved evaluation on an indexed Zebra task subset.""" | |
| from __future__ import annotations | |
| import argparse | |
| import fcntl | |
| import gc | |
| import hashlib | |
| import json | |
| import os | |
| import re | |
| import time | |
| import uuid | |
| from collections import Counter, OrderedDict | |
| from datetime import timedelta | |
| from pathlib import Path | |
| import torch | |
| import torch.distributed as dist | |
| from artifacts import is_timestamped_run_name | |
| from PIL import Image | |
| from sft.runtime import claim_run_directory | |
| from peft import PeftModel | |
| from transformers import AutoProcessor | |
| from uniar import UniARForConditionalGeneration, UniARVisualDecoder, configure_processor | |
| from .cached_rollout import CachedInterleavedRollout | |
| from .evaluate_corpus import _load_shard, _readable, _target | |
| from .generated_codes import load_generated_codes, save_generated_codes | |
| from .scoring import score_answer | |
| SUBSET_FIELD = "evaluation_subset_index" | |
| def _write_json(path: Path, value) -> None: | |
| temporary = path.with_name(f".{path.name}.tmp-{uuid.uuid4().hex}") | |
| try: | |
| temporary.write_text( | |
| json.dumps(value, ensure_ascii=False, indent=2, default=str) + "\n", | |
| encoding="utf-8", | |
| ) | |
| os.replace(temporary, path) | |
| finally: | |
| temporary.unlink(missing_ok=True) | |
| def _write_text(path: Path, value: str) -> None: | |
| temporary = path.with_name(f".{path.name}.tmp-{uuid.uuid4().hex}") | |
| try: | |
| temporary.write_text(value, encoding="utf-8") | |
| os.replace(temporary, path) | |
| finally: | |
| temporary.unlink(missing_ok=True) | |
| def _validate_sample_commit(samples_dir: Path, row: dict) -> None: | |
| sample_dir = samples_dir / row["sample_id"] | |
| for name in ("target.txt", "output.txt", "result.json", "generated_codes.pt"): | |
| if not (sample_dir / name).is_file(): | |
| raise FileNotFoundError(sample_dir / name) | |
| result = json.loads((sample_dir / "result.json").read_text(encoding="utf-8")) | |
| if ( | |
| result.get("sample_id") != row.get("sample_id") | |
| or result.get("evaluation_index") != row.get("evaluation_index") | |
| ): | |
| raise ValueError(f"sample commit provenance mismatch: {sample_dir}") | |
| load_generated_codes(sample_dir / "generated_codes.pt") | |
| def _wait_for_adapter(args: argparse.Namespace, sleep=time.sleep) -> None: | |
| """Wait on the atomic SFT adapter marker without changing retry argv.""" | |
| if not args.wait_for_adapter: | |
| return | |
| if args.mode != "interleaved" or args.adapter_dir is None: | |
| raise ValueError("--wait-for-adapter requires interleaved mode and adapter-dir") | |
| marker = args.adapter_dir / "COMPLETE" | |
| announced = False | |
| while not marker.is_file(): | |
| if int(os.environ.get("LOCAL_RANK", "0")) == 0 and not announced: | |
| print(f"WAITING_FOR_ADAPTER {marker}", flush=True) | |
| announced = True | |
| sleep(15.0) | |
| def _load_prediction_rows( | |
| path: Path, *, resume: bool, samples_dir: Path | None = None | |
| ) -> list[dict]: | |
| if not path.exists(): | |
| return [] | |
| data = path.read_bytes() | |
| boundary = data.rfind(b"\n") + 1 | |
| changed = boundary != len(data) | |
| if changed: | |
| if not resume: | |
| raise ValueError(f"prediction journal has a torn tail: {path}") | |
| data = data[:boundary] | |
| parsed = [ | |
| (line, json.loads(line.decode("utf-8"))) | |
| for line in data.splitlines(keepends=True) | |
| if line.strip() | |
| ] | |
| rows = [row for _, row in parsed] | |
| if resume and samples_dir is not None: | |
| committed = [] | |
| committed_lines = [] | |
| for line, row in parsed: | |
| try: | |
| _validate_sample_commit(samples_dir, row) | |
| except (FileNotFoundError, OSError, RuntimeError, ValueError): | |
| changed = True | |
| break | |
| committed.append(row) | |
| committed_lines.append(line) | |
| rows = committed | |
| parsed = list(zip(committed_lines, committed, strict=True)) | |
| if changed: | |
| temporary = path.with_name(f".{path.name}.recovery-{os.getpid()}") | |
| try: | |
| with temporary.open("wb") as handle: | |
| for line, _ in parsed: | |
| handle.write(line) | |
| handle.flush() | |
| os.fsync(handle.fileno()) | |
| os.replace(temporary, path) | |
| finally: | |
| temporary.unlink(missing_ok=True) | |
| return rows | |
| def _acquire_output_lock(output_dir: Path): | |
| """Hold an advisory lock so only one evaluation writes a run directory.""" | |
| lock_path = output_dir / ".active-writer.lock" | |
| handle = lock_path.open("a+", encoding="utf-8") | |
| try: | |
| fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) | |
| except BlockingIOError as error: | |
| handle.seek(0) | |
| holder = handle.read().strip() or "unknown writer" | |
| handle.close() | |
| raise RuntimeError( | |
| f"evaluation output already has an active writer: {holder}" | |
| ) from error | |
| handle.seek(0) | |
| handle.truncate() | |
| json.dump( | |
| { | |
| "pid": os.getpid(), | |
| "slurm_job_id": os.environ.get("SLURM_JOB_ID"), | |
| "host": os.uname().nodename, | |
| }, | |
| handle, | |
| ) | |
| handle.write("\n") | |
| handle.flush() | |
| return handle | |
| def _worker_device_index( | |
| local_rank: int, visible_devices: int, workers_per_gpu: int | |
| ) -> int: | |
| if local_rank < 0 or visible_devices < 1 or workers_per_gpu < 1: | |
| raise ValueError("local rank, visible devices, and workers per GPU are invalid") | |
| capacity = visible_devices * workers_per_gpu | |
| if local_rank >= capacity: | |
| raise ValueError( | |
| f"local rank {local_rank} exceeds capacity {capacity} for " | |
| f"{visible_devices} visible GPU(s) and {workers_per_gpu} worker(s) per GPU" | |
| ) | |
| return local_rank // workers_per_gpu | |
| def _distributed(workers_per_gpu: int) -> tuple[int, int, int, torch.device]: | |
| rank = int(os.environ.get("RANK", "0")) | |
| world = int(os.environ.get("WORLD_SIZE", "1")) | |
| local_rank = int(os.environ.get("LOCAL_RANK", "0")) | |
| local_world = int(os.environ.get("LOCAL_WORLD_SIZE", "1")) | |
| visible_devices = torch.cuda.device_count() | |
| expected_local_world = visible_devices * workers_per_gpu | |
| if local_world != expected_local_world: | |
| raise ValueError( | |
| f"LOCAL_WORLD_SIZE={local_world}, expected {expected_local_world} for " | |
| f"{visible_devices} visible GPU(s) and {workers_per_gpu} worker(s) per GPU" | |
| ) | |
| device_index = _worker_device_index( | |
| local_rank, visible_devices, workers_per_gpu | |
| ) | |
| if world > 1: | |
| # Rollout lengths are highly imbalanced. A finished rank may wait well | |
| # over NCCL's default timeout while another completes a long cyclic | |
| # sample, even though both are healthy. | |
| backend = "gloo" if workers_per_gpu > 1 else "nccl" | |
| dist.init_process_group(backend, timeout=timedelta(hours=6)) | |
| torch.cuda.set_device(device_index) | |
| return rank, world, local_rank, torch.device("cuda", device_index) | |
| def _barrier(world: int) -> None: | |
| if world > 1: | |
| dist.barrier() | |
| def _control_collective_device(world: int, device: torch.device) -> torch.device: | |
| if world > 1 and dist.get_backend() == "gloo": | |
| return torch.device("cpu") | |
| return device | |
| def _manifest_rows(cache_root: Path, selection: str = "indexed") -> list[dict]: | |
| rows = [] | |
| with (cache_root / "manifest.jsonl").open(encoding="utf-8") as handle: | |
| for line in handle: | |
| row = json.loads(line) | |
| if selection == "indexed" and row.get(SUBSET_FIELD) is not None: | |
| if row["split"] != "test": | |
| raise ValueError("evaluation subset contains a non-test row") | |
| rows.append(row) | |
| elif selection in {"validation", "test"} and row["split"] == selection: | |
| rows.append(row) | |
| if selection == "indexed": | |
| rows.sort(key=lambda row: row[SUBSET_FIELD]) | |
| actual = [row[SUBSET_FIELD] for row in rows] | |
| if actual != list(range(len(rows))): | |
| raise ValueError(f"{SUBSET_FIELD} must be unique and contiguous") | |
| else: | |
| rows.sort(key=lambda row: row["sample_id"]) | |
| for index, row in enumerate(rows): | |
| row["evaluation_index"] = index | |
| if not rows: | |
| raise ValueError(f"evaluation selection {selection!r} is empty") | |
| return rows | |
| def _limit_manifest_rows( | |
| rows: list[dict], max_samples: int | None, sample_offset: int = 0 | |
| ) -> list[dict]: | |
| if sample_offset < 0: | |
| raise ValueError("sample_offset must be nonnegative") | |
| if max_samples is None and sample_offset == 0: | |
| return rows | |
| if max_samples is None: | |
| return rows[sample_offset:] | |
| if max_samples <= 0: | |
| raise ValueError("max_samples must be positive") | |
| return rows[sample_offset : sample_offset + max_samples] | |
| def _items(cache_root: Path, rows: list[dict]) -> list[dict]: | |
| by_shard = {} | |
| for position, row in enumerate(rows): | |
| by_shard.setdefault(row["cache_shard"], []).append((position, row)) | |
| items = [None] * len(rows) | |
| for relative, selected in by_shard.items(): | |
| shard = _load_shard(cache_root / relative) | |
| for position, row in selected: | |
| item = shard[row["cache_index"]] | |
| if item["sample_id"] != row["sample_id"]: | |
| raise ValueError(f"cache provenance mismatch for {row['sample_id']}") | |
| items[position] = item | |
| return items | |
| class _ShardItemLoader: | |
| """Small mmap-backed shard LRU for dynamically claimed evaluation rows.""" | |
| def __init__(self, cache_root: Path, max_open_shards: int = 2): | |
| self.cache_root = cache_root | |
| self.max_open_shards = max_open_shards | |
| self.shards = OrderedDict() | |
| def load(self, row: dict) -> dict: | |
| relative = row["cache_shard"] | |
| if relative not in self.shards: | |
| self.shards[relative] = _load_shard(self.cache_root / relative) | |
| if len(self.shards) > self.max_open_shards: | |
| self.shards.popitem(last=False) | |
| self.shards.move_to_end(relative) | |
| item = self.shards[relative][row["cache_index"]] | |
| if item["sample_id"] != row["sample_id"]: | |
| raise ValueError(f"cache provenance mismatch for {row['sample_id']}") | |
| return item | |
| def _explicit_final(text: str) -> bool: | |
| return re.search(r"final\s+answer\s*:", text, flags=re.IGNORECASE) is not None | |
| def _result_row( | |
| item: dict, | |
| manifest_row: dict, | |
| result: dict, | |
| *, | |
| mode: str, | |
| model_label: str, | |
| ) -> tuple[dict, list[torch.Tensor]]: | |
| raw_codes = result.pop("raw_codes", []) | |
| result.pop("target_codes", None) | |
| result.pop("intervention_images", None) | |
| sequence_ids = result.pop("sequence_ids").tolist() | |
| decoded = result["decoded"] | |
| score = score_answer(decoded, _target(item)) | |
| has_final = _explicit_final(decoded) if mode == "interleaved" else True | |
| if mode == "understanding": | |
| events = [ | |
| { | |
| "type": "text", | |
| "event": result["stop_reason"], | |
| "token_ids": sequence_ids, | |
| } | |
| ] | |
| else: | |
| events = result["events"] | |
| row = { | |
| **result, | |
| "sample_id": item["sample_id"], | |
| "evaluation_index": manifest_row["evaluation_index"], | |
| "evaluation_subset_index": manifest_row.get(SUBSET_FIELD), | |
| "config": item["config"], | |
| "mode": mode, | |
| "model_label": model_label, | |
| "target": _target(item), | |
| "sequence_ids": sequence_ids, | |
| "readable_sequence": _readable(decoded), | |
| "events": events, | |
| "score": score, | |
| "has_final_answer": has_final, | |
| "strict_correct": bool(score["correct"] and has_final), | |
| } | |
| return row, raw_codes | |
| def _summarize(rows: list[dict], args: argparse.Namespace, world: int) -> dict: | |
| strict = sum(row["strict_correct"] for row in rows) | |
| image_rows = [row for row in rows if "expected_images" in row] | |
| by_expected_images = {} | |
| for count in sorted({int(row["expected_images"]) for row in image_rows}): | |
| selected = [ | |
| row for row in image_rows if int(row["expected_images"]) == count | |
| ] | |
| correct = sum(row["strict_correct"] for row in selected) | |
| by_expected_images[str(count)] = { | |
| "samples": len(selected), | |
| "correct": correct, | |
| "accuracy": correct / len(selected), | |
| "expected_images": sum(int(row["expected_images"]) for row in selected), | |
| "generated_images": sum(int(row["generated_images"]) for row in selected), | |
| } | |
| complete_grids = sum( | |
| row.get("expected_grids") == row.get("generated_grids") | |
| for row in image_rows | |
| ) | |
| text_tokens = sum( | |
| len(event.get("token_ids", [])) | |
| for row in rows | |
| for event in row["events"] | |
| if event.get("type") == "text" | |
| ) | |
| visual_tokens = sum( | |
| int(event.get("serialized_positions", 0)) | |
| for row in rows | |
| for event in row["events"] | |
| if event.get("type") == "image" | |
| ) | |
| return { | |
| "mode": args.mode, | |
| "model_label": args.model_label, | |
| "model_path": str(args.model_path), | |
| "adapter_dir": str(args.adapter_dir) if args.adapter_dir else None, | |
| "selection": args.selection, | |
| "subset_field": ( | |
| SUBSET_FIELD if args.selection == "indexed" else f"split:{args.selection}" | |
| ), | |
| "samples": len(rows), | |
| "world_size": world, | |
| "workers_per_gpu": args.workers_per_gpu, | |
| "scheduling": args.scheduling, | |
| "text_cycle_max_period": args.text_cycle_max_period, | |
| "text_cycle_min_tokens": args.text_cycle_min_tokens, | |
| "correct": strict, | |
| "accuracy": strict / len(rows), | |
| "raw_scorer_correct": sum(row["score"]["correct"] for row in rows), | |
| "stop_reasons": dict(Counter(row["stop_reason"] for row in rows)), | |
| "expected_images": sum(int(row["expected_images"]) for row in image_rows), | |
| "generated_images": sum(int(row["generated_images"]) for row in image_rows), | |
| "by_expected_images": by_expected_images, | |
| "complete_grid_sequences": complete_grids, | |
| "complete_grid_sequence_rate": ( | |
| complete_grids / len(image_rows) if image_rows else None | |
| ), | |
| "assistant_tokens": { | |
| "text": text_tokens, | |
| "visual": visual_tokens, | |
| "total": text_tokens + visual_tokens, | |
| "mean_text_per_sample": text_tokens / len(rows), | |
| "mean_visual_per_sample": visual_tokens / len(rows), | |
| }, | |
| "decoder": { | |
| "enabled": bool(args.decode_media and args.mode == "interleaved"), | |
| "steps": args.decoder_steps, | |
| "guidance": args.decoder_guidance, | |
| "upsampling_ratio": args.decoder_upsampling, | |
| }, | |
| } | |
| def _merge_rank_shards( | |
| output_dir: Path, | |
| manifest_rows: list[dict], | |
| world: int, | |
| scheduling: str = "static", | |
| ) -> list[dict]: | |
| """Merge complete rank shards and enforce their scheduling contract.""" | |
| expected_by_id = {row["sample_id"]: row for row in manifest_rows} | |
| merged = [] | |
| for shard_rank in range(world): | |
| path = output_dir / "shards" / f"rank-{shard_rank:02d}" / "predictions.jsonl" | |
| shard_rows = [ | |
| json.loads(line) for line in path.read_text().splitlines() if line | |
| ] | |
| if scheduling == "static": | |
| expected_indices = [ | |
| row["evaluation_index"] for row in manifest_rows[shard_rank::world] | |
| ] | |
| actual_indices = [row["evaluation_index"] for row in shard_rows] | |
| if actual_indices != expected_indices: | |
| raise ValueError( | |
| f"rank {shard_rank} boundary mismatch: " | |
| f"expected={expected_indices}, actual={actual_indices}" | |
| ) | |
| for row in shard_rows: | |
| expected = expected_by_id.get(row["sample_id"]) | |
| if expected is None or row["evaluation_index"] != expected["evaluation_index"]: | |
| raise ValueError(f"manifest mismatch for {row['sample_id']}") | |
| merged.extend(shard_rows) | |
| merged.sort(key=lambda row: row["evaluation_index"]) | |
| expected_indices = [row["evaluation_index"] for row in manifest_rows] | |
| actual_indices = [row["evaluation_index"] for row in merged] | |
| actual_ids = [row["sample_id"] for row in merged] | |
| expected_ids = [row["sample_id"] for row in manifest_rows] | |
| if actual_indices != expected_indices or actual_ids != expected_ids: | |
| raise ValueError("merged evaluation does not exactly match the manifest") | |
| return merged | |
| def _publish_evaluation( | |
| output_dir: Path, | |
| manifest_rows: list[dict], | |
| args: argparse.Namespace, | |
| world: int, | |
| ) -> dict: | |
| merged = _merge_rank_shards(output_dir, manifest_rows, world, args.scheduling) | |
| with (output_dir / "predictions.jsonl").open("w", encoding="utf-8") as handle: | |
| for row in merged: | |
| handle.write(json.dumps(row, ensure_ascii=False) + "\n") | |
| summary = _summarize(merged, args, world) | |
| _write_json(output_dir / "summary.json", summary) | |
| print("EVALUATION_COMPLETE " + json.dumps(summary), flush=True) | |
| return summary | |
| def _global_completed_ids(local_ids: list[str], world: int) -> set[str]: | |
| if world == 1: | |
| gathered = [local_ids] | |
| else: | |
| gathered = [None] * world | |
| dist.all_gather_object(gathered, local_ids) | |
| completed = [sample_id for rank_ids in gathered for sample_id in rank_ids] | |
| if len(completed) != len(set(completed)): | |
| raise ValueError("dynamic resume predictions contain duplicate samples") | |
| return set(completed) | |
| def _dynamic_indices( | |
| output_dir: Path, | |
| pending_indices: list[int], | |
| rank: int, | |
| world: int, | |
| ): | |
| """Yield indices claimed from an atomic queue shared by local workers.""" | |
| if world == 1: | |
| yield from pending_indices | |
| return | |
| queue_path = output_dir / ".worker-queue" | |
| if rank == 0: | |
| queue_path.unlink(missing_ok=True) | |
| _barrier(world) | |
| store = dist.FileStore(str(queue_path), world) | |
| if rank == 0: | |
| store.set("next", "0") | |
| _barrier(world) | |
| while True: | |
| position = store.add("next", 1) - 1 | |
| if position >= len(pending_indices): | |
| break | |
| yield pending_indices[position] | |
| _barrier(world) | |
| del store | |
| _barrier(world) | |
| if rank == 0: | |
| queue_path.unlink(missing_ok=True) | |
| _barrier(world) | |
| def _decode_media( | |
| args: argparse.Namespace, | |
| device: torch.device, | |
| rows: list[dict], | |
| shard_dir: Path, | |
| rank: int, | |
| ) -> None: | |
| if args.mode != "interleaved" or not args.decode_media: | |
| return | |
| decoder = None | |
| total = sum(row.get("generated_images", 0) for row in rows) | |
| completed = 0 | |
| errors = [] | |
| for row in rows: | |
| sample_dir = shard_dir / "samples" / row["sample_id"] | |
| codes_per_step = load_generated_codes(sample_dir / "generated_codes.pt") | |
| for step, (codes, grid) in enumerate( | |
| zip(codes_per_step, row.get("generated_grids", []), strict=True), start=1 | |
| ): | |
| completed += 1 | |
| destination = sample_dir / f"generated_{step:02d}.png" | |
| temporary = None | |
| try: | |
| if destination.exists(): | |
| try: | |
| with Image.open(destination) as existing: | |
| existing.verify() | |
| except (OSError, ValueError): | |
| pass | |
| else: | |
| print( | |
| f"MEDIA rank={rank} {completed}/{total} existing", | |
| flush=True, | |
| ) | |
| continue | |
| if decoder is None: | |
| decoder = UniARVisualDecoder.from_pretrained( | |
| args.model_path, device=device | |
| ) | |
| generator = torch.Generator(device=device).manual_seed( | |
| int.from_bytes( | |
| hashlib.sha256( | |
| f"20260827:{row['sample_id']}:{step}".encode() | |
| ).digest()[:8], | |
| "big", | |
| ) | |
| ) | |
| binary = codes.to(device=device, dtype=torch.bfloat16) | |
| indices = decoder.bsq_encoder.bsq.codes_to_indexes(binary * 2 - 1) | |
| image = decoder.decode( | |
| indices, | |
| ar_height=grid[0] * 32, | |
| ar_width=grid[1] * 32, | |
| upsampling_ratio=args.decoder_upsampling, | |
| num_inference_steps=args.decoder_steps, | |
| guidance_scale=args.decoder_guidance, | |
| generator=generator, | |
| )[0] | |
| temporary = destination.with_name( | |
| f".{destination.name}.tmp-{os.getpid()}" | |
| ) | |
| image.save(temporary, format="PNG") | |
| os.replace(temporary, destination) | |
| except (OSError, RuntimeError, ValueError) as error: | |
| # Preserve completed rollout statistics if RGB decoding fails. | |
| errors.append( | |
| { | |
| "sample_id": row["sample_id"], | |
| "step": step, | |
| "error": repr(error), | |
| } | |
| ) | |
| if temporary is not None: | |
| temporary.unlink(missing_ok=True) | |
| torch.cuda.empty_cache() | |
| print(f"MEDIA rank={rank} {completed}/{total}", flush=True) | |
| if errors: | |
| with (shard_dir / "decode_errors.jsonl").open("w", encoding="utf-8") as handle: | |
| for error in errors: | |
| handle.write(json.dumps(error, ensure_ascii=False) + "\n") | |
| def evaluate(args: argparse.Namespace) -> None: | |
| _wait_for_adapter(args) | |
| args.resume = args.resume or (args.output_dir / ".run-claim.json").is_file() | |
| rank, world, _, device = _distributed(args.workers_per_gpu) | |
| if not is_timestamped_run_name(args.output_dir.name): | |
| raise ValueError("output directory must use a timestamped run name") | |
| if rank == 0: | |
| claim_run_directory(args.output_dir, resume=args.resume) | |
| output_lock = _acquire_output_lock(args.output_dir) | |
| arguments_path = args.output_dir / "arguments.json" | |
| current_arguments = { | |
| key: str(value) if isinstance(value, Path) else value | |
| for key, value in vars(args).items() | |
| if key != "resume" | |
| } | |
| current_arguments["_world_size"] = world | |
| current_arguments["_local_world_size"] = int( | |
| os.environ.get("LOCAL_WORLD_SIZE", "1") | |
| ) | |
| if args.resume: | |
| if not arguments_path.exists(): | |
| raise FileNotFoundError("resume output has no arguments.json") | |
| previous_arguments = json.loads(arguments_path.read_text()) | |
| mismatched = { | |
| key: (previous_arguments.get(key), value) | |
| for key, value in current_arguments.items() | |
| if previous_arguments.get(key) != value | |
| } | |
| if mismatched: | |
| raise ValueError(f"resume arguments differ: {mismatched}") | |
| else: | |
| _write_json(arguments_path, current_arguments) | |
| _barrier(world) | |
| shard_dir = args.output_dir / "shards" / f"rank-{rank:02d}" | |
| shard_dir.mkdir(parents=True, exist_ok=args.resume) | |
| all_manifest_rows = _limit_manifest_rows( | |
| _manifest_rows(args.cache_root, args.selection), | |
| args.max_samples, | |
| args.sample_offset, | |
| ) | |
| dynamic = args.scheduling == "dynamic" | |
| manifest_rows = all_manifest_rows if dynamic else all_manifest_rows[rank::world] | |
| predictions_path = shard_dir / "predictions.jsonl" | |
| rows = _load_prediction_rows( | |
| predictions_path, | |
| resume=args.resume, | |
| samples_dir=shard_dir / "samples", | |
| ) | |
| completed_ids = [row["sample_id"] for row in rows] | |
| if len(completed_ids) != len(set(completed_ids)): | |
| raise ValueError(f"rank {rank} resume predictions contain duplicate samples") | |
| expected_ids = {row["sample_id"] for row in manifest_rows} | |
| unexpected = sorted(set(completed_ids) - expected_ids) | |
| if unexpected: | |
| raise ValueError(f"rank {rank} resume has unexpected samples: {unexpected}") | |
| manifest_by_id = {row["sample_id"]: row for row in manifest_rows} | |
| for row in rows: | |
| expected = manifest_by_id[row["sample_id"]] | |
| if row.get("evaluation_index") != expected["evaluation_index"]: | |
| raise ValueError(f"rank {rank} resume index mismatch for {row['sample_id']}") | |
| if dynamic: | |
| globally_completed = _global_completed_ids(completed_ids, world) | |
| remaining_indices = sorted( | |
| [ | |
| index | |
| for index, row in enumerate(all_manifest_rows) | |
| if row["sample_id"] not in globally_completed | |
| ], | |
| key=lambda index: ( | |
| all_manifest_rows[index]["cache_shard"], | |
| -int(all_manifest_rows[index]["sequence_length"]), | |
| all_manifest_rows[index]["sample_id"], | |
| ), | |
| ) | |
| remaining_count = len(remaining_indices) | |
| else: | |
| completed = set(completed_ids) | |
| remaining_rows = [ | |
| row for row in manifest_rows if row["sample_id"] not in completed | |
| ] | |
| remaining_count = len(remaining_rows) | |
| collective_device = _control_collective_device(world, device) | |
| remaining_tensor = torch.tensor( | |
| remaining_count, device=collective_device, dtype=torch.long | |
| ) | |
| if world > 1: | |
| dist.all_reduce(remaining_tensor, op=dist.ReduceOp.MAX if dynamic else dist.ReduceOp.SUM) | |
| if int(remaining_tensor) == 0: | |
| _barrier(world) | |
| if rank == 0: | |
| _publish_evaluation(args.output_dir, all_manifest_rows, args, world) | |
| _barrier(world) | |
| _decode_media(args, device, rows, shard_dir, rank) | |
| _barrier(world) | |
| if world > 1: | |
| dist.destroy_process_group() | |
| if rank == 0: | |
| output_lock.close() | |
| return | |
| processor = configure_processor( | |
| AutoProcessor.from_pretrained(args.model_path, use_fast=True) | |
| ) | |
| base = ( | |
| UniARForConditionalGeneration.from_pretrained( | |
| args.model_path, | |
| dtype=torch.bfloat16, | |
| attn_implementation=args.attn, | |
| low_cpu_mem_usage=True, | |
| ) | |
| .to(device) | |
| .eval() | |
| ) | |
| model = base | |
| if args.mode == "interleaved": | |
| if args.adapter_dir is None: | |
| raise ValueError("interleaved mode requires --adapter-dir") | |
| model = PeftModel.from_pretrained(base, args.adapter_dir).eval() | |
| if dynamic: | |
| loader = _ShardItemLoader(args.cache_root) | |
| remaining = ( | |
| (loader.load(all_manifest_rows[index]), all_manifest_rows[index]) | |
| for index in _dynamic_indices( | |
| args.output_dir, remaining_indices, rank, world | |
| ) | |
| ) | |
| else: | |
| remaining_items = _items(args.cache_root, remaining_rows) | |
| remaining = iter(zip(remaining_items, remaining_rows, strict=True)) | |
| print( | |
| f"MODEL_READY rank={rank} mode={args.mode} samples={len(manifest_rows)} " | |
| f"remaining={remaining_count} scheduling={args.scheduling} device={device}", | |
| flush=True, | |
| ) | |
| rollout = CachedInterleavedRollout(model, processor) | |
| file_mode = "a" if args.resume else "w" | |
| with predictions_path.open(file_mode, encoding="utf-8") as predictions: | |
| for local_index, (item, manifest_row) in enumerate(remaining, start=1): | |
| if args.mode == "understanding": | |
| result = rollout.direct_answer( | |
| item, max_new_tokens=args.max_understanding_tokens | |
| ) | |
| else: | |
| result = rollout.run( | |
| item, | |
| max_text_tokens=None, | |
| max_scaffold_tokens=None, | |
| extra_image_budget=None, | |
| cycle_repeats=args.cycle_repeats, | |
| text_cycle_max_period=args.text_cycle_max_period, | |
| text_cycle_min_tokens=args.text_cycle_min_tokens, | |
| include_target_codes=False, | |
| ) | |
| row, raw_codes = _result_row( | |
| item, | |
| manifest_row, | |
| result, | |
| mode=args.mode, | |
| model_label=args.model_label, | |
| ) | |
| rows.append(row) | |
| sample_dir = shard_dir / "samples" / item["sample_id"] | |
| sample_dir.mkdir(parents=True, exist_ok=args.resume) | |
| _write_text(sample_dir / "target.txt", item["target_render"]) | |
| _write_text(sample_dir / "output.txt", row["readable_sequence"]) | |
| _write_json(sample_dir / "result.json", row) | |
| save_generated_codes(sample_dir / "generated_codes.pt", raw_codes) | |
| predictions.write(json.dumps(row, ensure_ascii=False) + "\n") | |
| predictions.flush() | |
| os.fsync(predictions.fileno()) | |
| print( | |
| f"PROGRESS local={local_index} rank={rank} " | |
| f"index={row['evaluation_index']} correct={row['strict_correct']} " | |
| f"stop={row['stop_reason']} images={row.get('generated_images', 0)}", | |
| flush=True, | |
| ) | |
| _barrier(world) | |
| if rank == 0: | |
| _publish_evaluation(args.output_dir, all_manifest_rows, args, world) | |
| _barrier(world) | |
| del rollout, model, base | |
| gc.collect() | |
| torch.cuda.empty_cache() | |
| _decode_media(args, device, rows, shard_dir, rank) | |
| _barrier(world) | |
| if world > 1: | |
| dist.destroy_process_group() | |
| if rank == 0: | |
| output_lock.close() | |
| def build_parser() -> argparse.ArgumentParser: | |
| parser = argparse.ArgumentParser(description=__doc__) | |
| parser.add_argument( | |
| "--mode", choices=["understanding", "interleaved"], required=True | |
| ) | |
| parser.add_argument("--model-label", required=True) | |
| parser.add_argument("--model-path", type=Path, default=Path("models/UniAR-SFT")) | |
| parser.add_argument("--adapter-dir", type=Path) | |
| parser.add_argument( | |
| "--wait-for-adapter", | |
| action="store_true", | |
| help="wait for adapter/COMPLETE so a separately queued SFT can finish", | |
| ) | |
| parser.add_argument("--cache-root", type=Path, required=True) | |
| parser.add_argument("--output-dir", type=Path, required=True) | |
| parser.add_argument( | |
| "--selection", | |
| choices=["indexed", "validation", "test"], | |
| default="indexed", | |
| help="evaluate indexed test100 or a complete validation/test split", | |
| ) | |
| parser.add_argument( | |
| "--max-samples", | |
| type=int, | |
| default=None, | |
| help="take this many rows after --sample-offset", | |
| ) | |
| parser.add_argument( | |
| "--sample-offset", | |
| type=int, | |
| default=0, | |
| help="skip this many deterministic manifest-order rows", | |
| ) | |
| parser.add_argument("--attn", default="flash_attention_2") | |
| parser.add_argument("--max-understanding-tokens", type=int, default=256) | |
| parser.add_argument("--cycle-repeats", type=int, default=3) | |
| parser.add_argument("--text-cycle-max-period", type=int, default=128) | |
| parser.add_argument("--text-cycle-min-tokens", type=int, default=64) | |
| parser.add_argument( | |
| "--workers-per-gpu", | |
| type=int, | |
| default=1, | |
| help="share each visible GPU across this many torchrun worker processes", | |
| ) | |
| parser.add_argument( | |
| "--scheduling", | |
| choices=["static", "dynamic"], | |
| default="static", | |
| help="use strided rank shards or an atomic work queue", | |
| ) | |
| parser.add_argument( | |
| "--decode-media", action=argparse.BooleanOptionalAction, default=True | |
| ) | |
| parser.add_argument("--decoder-steps", type=int, default=28) | |
| parser.add_argument("--decoder-guidance", type=float, default=1.5) | |
| parser.add_argument("--decoder-upsampling", type=float, default=1.0) | |
| parser.add_argument( | |
| "--resume", | |
| action="store_true", | |
| help="append only missing samples to an interrupted output directory", | |
| ) | |
| return parser | |
| def main() -> None: | |
| evaluate(build_parser().parse_args()) | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 32.5 kB
- Xet hash:
- fd87c685f6b258619040a48096479a162cd8383a3757bfc1145c719713671e24
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.