feat(research): support CUDA convergence runs

Allow the sealed ResNet and YOLO convergence protocol to run unchanged on the remote NVIDIA accelerator while retaining CPU and MPS support.

Constraint: Preserve fixed seeds, exact workloads, and one-device-per-run semantics

Rejected: Reuse mixed MPS and CUDA results | invalid cross-device production matrix

Confidence: high

Scope-risk: narrow

Not-tested: Full production matrix on RTX 3080 Ti
This commit is contained in:
2026-09-07 22:09:45 +09:00
parent 813433000a
commit 89850f592e
3 changed files with 18 additions and 9 deletions
+7 -3
View File
@@ -128,8 +128,8 @@ class StudyConfig:
raise ProtocolError("residual bounds and initialization radius are fixed") raise ProtocolError("residual bounds and initialization radius are fixed")
if tuple(self.objective_checkpoints) != OBJECTIVE_CHECKPOINTS: if tuple(self.objective_checkpoints) != OBJECTIVE_CHECKPOINTS:
raise ProtocolError("objective checkpoints are fixed") raise ProtocolError("objective checkpoints are fixed")
if self.device not in {"cpu", "mps"}: if self.device not in {"cpu", "mps", "cuda"}:
raise ProtocolError("device must be cpu or mps") raise ProtocolError("device must be cpu, mps, or cuda")
if not self.workload_ids: if not self.workload_ids:
raise ProtocolError("at least one workload must be registered") raise ProtocolError("at least one workload must be registered")
@@ -1303,7 +1303,11 @@ def persist_state(run_root: str | os.PathLike[str], state_machine: StudyStateMac
def build_cli_parser() -> argparse.ArgumentParser: def build_cli_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Post-training ResNet/YOLO convergence protocol") parser = argparse.ArgumentParser(description="Post-training ResNet/YOLO convergence protocol")
parser.add_argument("--phase", choices=["prepare", "smoke", "develop", "confirm", "publish", "all"], required=True) parser.add_argument("--phase", choices=["prepare", "smoke", "develop", "confirm", "publish", "all"], required=True)
parser.add_argument("--device", choices=["mps", "cpu"], default="cpu") parser.add_argument(
"--device",
choices=["cuda", "mps", "cpu"],
default="cpu",
)
parser.add_argument("--data-root", type=Path, default=Path("result/cache")) parser.add_argument("--data-root", type=Path, default=Path("result/cache"))
parser.add_argument("--run-root", type=Path, required=True) parser.add_argument("--run-root", type=Path, required=True)
parser.add_argument("--allow-download", action="store_true") parser.add_argument("--allow-download", action="store_true")
+4 -2
View File
@@ -790,8 +790,10 @@ class ResNetConvergenceAdapter:
self.data_root = Path(data_root) self.data_root = Path(data_root)
self.device = torch.device(device) self.device = torch.device(device)
self.allow_download = bool(allow_download) self.allow_download = bool(allow_download)
if str(self.device) not in {"cpu", "mps"}: if str(self.device) not in {"cpu", "mps", "cuda"}:
raise ProtocolError("ResNet device must be cpu or mps") raise ProtocolError(
"ResNet device must be cpu, mps, or cuda"
)
def _result_path(self) -> Path: def _result_path(self) -> Path:
return self.run_root / "workloads" / self.workload_id / "result.json" return self.run_root / "workloads" / self.workload_id / "result.json"
+7 -4
View File
@@ -1047,9 +1047,10 @@ class StrictScratchTrainer:
batch: int = 16, batch: int = 16,
epochs: int = 100, epochs: int = 100,
) -> None: ) -> None:
if device not in {"cpu", "mps"}: if device not in {"cpu", "mps", "cuda"}:
raise YoloProtocolError( raise YoloProtocolError(
"device must remain cpu or mps for the complete run" "device must remain cpu, mps, or cuda for the "
"complete run"
) )
if epochs not in {2, 100}: if epochs not in {2, 100}:
raise YoloProtocolError("trainer epochs must be smoke 2 or production 100") raise YoloProtocolError("trainer epochs must be smoke 2 or production 100")
@@ -1519,8 +1520,10 @@ class YoloConvergenceAdapter:
def __init__(self, *, workload_id: str, config: StudyConfig, run_root: str | os.PathLike[str], data_root: str | os.PathLike[str], device: str | torch.device, allow_download: bool) -> None: def __init__(self, *, workload_id: str, config: StudyConfig, run_root: str | os.PathLike[str], data_root: str | os.PathLike[str], device: str | torch.device, allow_download: bool) -> None:
if workload_id != WORKLOAD_ID: if workload_id != WORKLOAD_ID:
raise YoloProtocolError(f"unsupported workload id: {workload_id}") raise YoloProtocolError(f"unsupported workload id: {workload_id}")
if str(device) not in {"cpu", "mps"}: if str(device) not in {"cpu", "mps", "cuda"}:
raise YoloProtocolError("device must be cpu or mps") raise YoloProtocolError(
"device must be cpu, mps, or cuda"
)
self.workload_id, self.config = workload_id, config self.workload_id, self.config = workload_id, config
self.run_root, self.data_root, self.device = Path(run_root), Path(data_root), str(device) self.run_root, self.data_root, self.device = Path(run_root), Path(data_root), str(device)
self.allow_download = bool(allow_download) self.allow_download = bool(allow_download)