Download test_cluster_mixed.py from DaisyChainAI/DaisyChain-Train: direct link, hf CLI and curl.
- Browser
- Download file 3.83 kB
-
https://huggingface.co/DaisyChainAI/DaisyChain-Train/resolve/main/test_cluster_mixed.py
- Command line
-
hf download hf://DaisyChainAI/DaisyChain-Train/test_cluster_mixed.py
-
curl -L -o test_cluster_mixed.py https://huggingface.co/DaisyChainAI/DaisyChain-Train/resolve/main/test_cluster_mixed.py
3.83 kB
| """Mixed CPU + GPU fleet: replicas must stay bit-identical (2026-10-14). | |
| Real 2-rank gloo run: rank 0 trains on CUDA, rank 1 on CPU, with Adam. | |
| * MEASURED first: CUDA Adam and CPU Adam on identical gradients differ in ~2% of weights after | |
| 20 steps, so with the optimizer on each rank's own device the replicas fork at step 1. | |
| With DAISY_CPU_OPT=0 (the old behaviour) the new per-interval replica check must STOP the run. | |
| * Default: GPU ranks step the optimizer on CPU master copies, so every rank runs the same | |
| implementation; the run completes and the final parameter bits agree exactly. | |
| Also: pin_fp_mode turns flush-to-zero off even if something switched it on. | |
| Needs a CUDA device; skips (exit 0, says so) without one. | |
| """ | |
| import os | |
| import sys | |
| import tempfile | |
| import torch | |
| import torch.distributed as dist | |
| import torch.multiprocessing as mp | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from daisychain import cluster as C # noqa: E402 | |
| FAILURES = [] | |
| def ck(name, cond, detail=""): | |
| print((" ok " if cond else " FAIL ") + name + ("" if cond else " [%s]" % (detail,))) | |
| if not cond: | |
| FAILURES.append(name) | |
| class Task: | |
| def __init__(self, rank): | |
| self.g = torch.Generator().manual_seed(1000 + rank) | |
| def sample(self, n): | |
| X = torch.randn(n, 32, generator=self.g) | |
| return X, X[:, :1] * 0.5 - X[:, 1:2] | |
| def loss(self, model, X, y): | |
| return torch.nn.functional.mse_loss(model(X), y) | |
| def worker(rank, world, init_file, out_dir, cpu_opt): | |
| if cpu_opt is not None: | |
| os.environ["DAISY_CPU_OPT"] = cpu_opt | |
| dist.init_process_group("gloo", init_method="file://" + init_file, rank=rank, world_size=world) | |
| cl = C.DaisyCluster.__new__(C.DaisyCluster) | |
| cl.threads, cl.rank, cl.world = 2, rank, world | |
| cl.device = torch.device("cuda" if rank == 0 else "cpu") | |
| cl.plan = {"devices": ["cuda", "cpu"], "my_weight": 0.5, "my_local_batch": 64, | |
| "weights": [0.5, 0.5], "local_batches": [64, 64], "total_cores": 2, "total_ram_gb": None} | |
| torch.manual_seed(3) | |
| model = torch.nn.Sequential(torch.nn.Linear(32, 64), torch.nn.ReLU(), torch.nn.Linear(64, 1)) | |
| err = None | |
| try: | |
| cl.fit(model, Task(rank), steps=40, lr=1e-3, optimizer="adam", check_every=5) | |
| except RuntimeError as e: | |
| err = str(e) | |
| torch.save({"err": err, "hash": C.param_hash(list(model.parameters()))}, | |
| os.path.join(out_dir, "r%d.pt" % rank)) | |
| dist.destroy_process_group() | |
| def run(cpu_opt): | |
| with tempfile.TemporaryDirectory() as td: | |
| mp.spawn(worker, args=(2, os.path.join(td, "init"), td, cpu_opt), nprocs=2, join=True) | |
| return [torch.load(os.path.join(td, "r%d.pt" % r)) for r in range(2)] | |
| def main(): | |
| if not C._cuda_usable(): | |
| print("SKIPPED: no CUDA device (this test needs one GPU rank and one CPU rank)") | |
| return 0 | |
| print("old behaviour (DAISY_CPU_OPT=0): optimizer on each rank's own device") | |
| old = run("0") | |
| ck("the replica check stops the run on both ranks", all(r["err"] and "diverged" in r["err"] for r in old), | |
| [r["err"] for r in old]) | |
| print("default: GPU ranks step on CPU master copies") | |
| new = run(None) | |
| ck("the run completes on both ranks", all(r["err"] is None for r in new), [r["err"] for r in new]) | |
| ck("final parameter bits are identical across the CUDA and CPU ranks", new[0]["hash"] == new[1]["hash"]) | |
| print("pin_fp_mode") | |
| torch.set_flush_denormal(True) | |
| C.pin_fp_mode() | |
| ck("subnormals survive after pin_fp_mode (FTZ off)", | |
| float(torch.tensor([1e-39], dtype=torch.float32) * 1.0) != 0.0) | |
| print() | |
| if FAILURES: | |
| print("FAILED: %d" % len(FAILURES)) | |
| return 1 | |
| print("all checks passed") | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |