Spaces:
Running
Running
| """Route 3: CPU calibration for the unresolved full GAN campaign. | |
| Synthetic tensors are intentional here: this script measures the cost of the | |
| reported image shapes, batch size, architectures, WGAN-GP backward pass, and | |
| parameter-gradient L1 statistic. It does not claim to reproduce GAN quality. | |
| """ | |
| from __future__ import annotations | |
| import gc | |
| import json | |
| import math | |
| import os | |
| import time | |
| from pathlib import Path | |
| import torch | |
| from torch import nn | |
| from torch.nn import functional as F | |
| BATCH_SIZE = 64 | |
| LATENT_DIM = 128 | |
| BASE_CHANNELS = 16 | |
| LEARNING_RATE = 2e-4 | |
| GRADIENT_PENALTY = 10.0 | |
| HORIZON_FROM_PUBLISHED_PLOTS = 16000 | |
| CONFIGURATIONS = 44 # 7 beta + 4 rho settings, across four architecture/dataset pairs. | |
| SEED = 20260729 | |
| class CNNGenerator(nn.Module): | |
| def __init__(self, size: int): | |
| super().__init__() | |
| blocks = int(math.log2(size)) - 2 | |
| channels = BASE_CHANNELS * 4 | |
| self.fc = nn.Linear(LATENT_DIM, channels * 4 * 4) | |
| layers: list[nn.Module] = [] | |
| for _ in range(blocks): | |
| next_channels = max(BASE_CHANNELS, channels // 2) | |
| layers.extend( | |
| [ | |
| nn.ConvTranspose2d(channels, next_channels, 4, 2, 1), | |
| nn.BatchNorm2d(next_channels), | |
| nn.ReLU(), | |
| ] | |
| ) | |
| channels = next_channels | |
| layers.extend([nn.Conv2d(channels, 3, 3, 1, 1), nn.Tanh()]) | |
| self.net = nn.Sequential(*layers) | |
| def forward(self, z: torch.Tensor) -> torch.Tensor: | |
| return self.net(self.fc(z).reshape(z.shape[0], BASE_CHANNELS * 4, 4, 4)) | |
| class CNNDiscriminator(nn.Module): | |
| def __init__(self, size: int): | |
| super().__init__() | |
| blocks = int(math.log2(size)) - 2 | |
| channels = BASE_CHANNELS | |
| layers: list[nn.Module] = [nn.Conv2d(3, channels, 3, 1, 1), nn.LeakyReLU(0.2)] | |
| for _ in range(blocks): | |
| next_channels = min(BASE_CHANNELS * 4, channels * 2) | |
| layers.extend( | |
| [ | |
| nn.Conv2d(channels, next_channels, 4, 2, 1), | |
| nn.LeakyReLU(0.2), | |
| ] | |
| ) | |
| channels = next_channels | |
| self.net = nn.Sequential(*layers) | |
| self.head = nn.Linear(channels * 4 * 4, 1) | |
| def forward(self, image: torch.Tensor) -> torch.Tensor: | |
| features = self.net(image) | |
| return self.head(features.flatten(1)).flatten() | |
| class ResUp(nn.Module): | |
| def __init__(self, input_channels: int, output_channels: int): | |
| super().__init__() | |
| self.conv1 = nn.Conv2d(input_channels, output_channels, 3, 1, 1) | |
| self.conv2 = nn.Conv2d(output_channels, output_channels, 3, 1, 1) | |
| self.skip = nn.Conv2d(input_channels, output_channels, 1) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| residual = self.skip(F.interpolate(x, scale_factor=2, mode="nearest")) | |
| x = F.interpolate(x, scale_factor=2, mode="nearest") | |
| x = self.conv1(F.relu(x)) | |
| return residual + self.conv2(F.relu(x)) | |
| class ResDown(nn.Module): | |
| def __init__(self, input_channels: int, output_channels: int): | |
| super().__init__() | |
| self.conv1 = nn.Conv2d(input_channels, output_channels, 3, 1, 1) | |
| self.conv2 = nn.Conv2d(output_channels, output_channels, 3, 1, 1) | |
| self.skip = nn.Conv2d(input_channels, output_channels, 1) | |
| def forward(self, x: torch.Tensor) -> torch.Tensor: | |
| residual = F.avg_pool2d(self.skip(x), 2) | |
| x = self.conv1(F.leaky_relu(x, 0.2)) | |
| x = self.conv2(F.leaky_relu(x, 0.2)) | |
| return residual + F.avg_pool2d(x, 2) | |
| class ResGenerator(nn.Module): | |
| def __init__(self, size: int): | |
| super().__init__() | |
| blocks = int(math.log2(size)) - 2 | |
| channels = BASE_CHANNELS * 4 | |
| self.fc = nn.Linear(LATENT_DIM, channels * 4 * 4) | |
| layers: list[nn.Module] = [] | |
| for _ in range(blocks): | |
| next_channels = max(BASE_CHANNELS, channels // 2) | |
| layers.append(ResUp(channels, next_channels)) | |
| channels = next_channels | |
| self.blocks = nn.Sequential(*layers) | |
| self.final = nn.Conv2d(channels, 3, 3, 1, 1) | |
| def forward(self, z: torch.Tensor) -> torch.Tensor: | |
| x = self.fc(z).reshape(z.shape[0], BASE_CHANNELS * 4, 4, 4) | |
| return torch.tanh(self.final(F.relu(self.blocks(x)))) | |
| class ResDiscriminator(nn.Module): | |
| def __init__(self, size: int): | |
| super().__init__() | |
| blocks = int(math.log2(size)) - 2 | |
| channels = BASE_CHANNELS | |
| self.initial = nn.Conv2d(3, channels, 3, 1, 1) | |
| layers: list[nn.Module] = [] | |
| for _ in range(blocks): | |
| next_channels = min(BASE_CHANNELS * 4, channels * 2) | |
| layers.append(ResDown(channels, next_channels)) | |
| channels = next_channels | |
| self.blocks = nn.Sequential(*layers) | |
| self.head = nn.Linear(channels * 4 * 4, 1) | |
| def forward(self, image: torch.Tensor) -> torch.Tensor: | |
| features = F.leaky_relu(self.blocks(self.initial(image)), 0.2) | |
| return self.head(features.flatten(1)).flatten() | |
| def parameter_gradient_l1(module: nn.Module) -> float: | |
| return float( | |
| sum( | |
| parameter.grad.detach().abs().sum() | |
| for parameter in module.parameters() | |
| if parameter.grad is not None | |
| ) | |
| ) | |
| def make_models(architecture: str, size: int) -> tuple[nn.Module, nn.Module]: | |
| if architecture == "CNN": | |
| return CNNGenerator(size), CNNDiscriminator(size) | |
| return ResGenerator(size), ResDiscriminator(size) | |
| def joint_training_step( | |
| generator: nn.Module, | |
| discriminator: nn.Module, | |
| generator_optimizer: torch.optim.Optimizer, | |
| discriminator_optimizer: torch.optim.Optimizer, | |
| size: int, | |
| ) -> float: | |
| real = torch.rand(BATCH_SIZE, 3, size, size) * 2.0 - 1.0 | |
| z = torch.randn(BATCH_SIZE, LATENT_DIM) | |
| with torch.no_grad(): | |
| fake = generator(z) | |
| discriminator_optimizer.zero_grad(set_to_none=True) | |
| real_score = discriminator(real) | |
| fake_score = discriminator(fake) | |
| alpha = torch.rand(BATCH_SIZE, 1, 1, 1) | |
| interpolated = (alpha * real + (1.0 - alpha) * fake).requires_grad_(True) | |
| interpolated_score = discriminator(interpolated) | |
| input_gradient = torch.autograd.grad( | |
| interpolated_score.sum(), interpolated, create_graph=True | |
| )[0] | |
| penalty = ( | |
| input_gradient.flatten(1).norm(2, dim=1).sub(1.0).square().mean() | |
| * GRADIENT_PENALTY | |
| ) | |
| discriminator_loss = fake_score.mean() - real_score.mean() + penalty | |
| discriminator_loss.backward() | |
| discriminator_gradient = parameter_gradient_l1(discriminator) | |
| discriminator_optimizer.step() | |
| generator_optimizer.zero_grad(set_to_none=True) | |
| generated = generator(torch.randn(BATCH_SIZE, LATENT_DIM)) | |
| generator_loss = -discriminator(generated).mean() | |
| generator_loss.backward() | |
| generator_gradient = parameter_gradient_l1(generator) | |
| generator_optimizer.step() | |
| return discriminator_gradient + generator_gradient | |
| def cgroup_quota() -> tuple[str | None, int]: | |
| path = Path("/sys/fs/cgroup/cpu.max") | |
| raw = path.read_text().strip() if path.exists() else None | |
| if raw and not raw.startswith("max"): | |
| quota, period = map(int, raw.split()) | |
| return raw, max(1, quota // period) | |
| return raw, min(8, os.cpu_count() or 1) | |
| def run(output_dir: Path) -> dict[str, object]: | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| raw_quota, allocated_cores = cgroup_quota() | |
| torch.set_num_threads(allocated_cores) | |
| torch.set_num_interop_threads(1) | |
| torch.manual_seed(SEED) | |
| profiles: list[dict[str, object]] = [] | |
| for architecture, dataset, size in ( | |
| ("CNN", "CIFAR-10", 32), | |
| ("CNN", "STL-10", 64), | |
| ("ResNet", "CIFAR-10", 32), | |
| ("ResNet", "STL-10", 64), | |
| ): | |
| generator, discriminator = make_models(architecture, size) | |
| generator_optimizer = torch.optim.Adam( | |
| generator.parameters(), lr=LEARNING_RATE, betas=(0.0, 0.9) | |
| ) | |
| discriminator_optimizer = torch.optim.Adam( | |
| discriminator.parameters(), lr=LEARNING_RATE, betas=(0.0, 0.9) | |
| ) | |
| with torch.inference_mode(): | |
| started = time.perf_counter() | |
| for _ in range(3): | |
| discriminator(generator(torch.randn(BATCH_SIZE, LATENT_DIM))) | |
| forward_seconds = (time.perf_counter() - started) / 3.0 | |
| joint_training_step( | |
| generator, discriminator, generator_optimizer, discriminator_optimizer, size | |
| ) | |
| measured_seconds: list[float] = [] | |
| gradient_l1: list[float] = [] | |
| for _ in range(2): | |
| started = time.perf_counter() | |
| gradient_l1.append( | |
| joint_training_step( | |
| generator, | |
| discriminator, | |
| generator_optimizer, | |
| discriminator_optimizer, | |
| size, | |
| ) | |
| ) | |
| measured_seconds.append(time.perf_counter() - started) | |
| seconds_per_step = sum(measured_seconds) / len(measured_seconds) | |
| profiles.append( | |
| { | |
| "architecture": architecture, | |
| "dataset_shape": dataset, | |
| "resolution": size, | |
| "batch_size": BATCH_SIZE, | |
| "base_channels": BASE_CHANNELS, | |
| "forward_only_seconds": forward_seconds, | |
| "joint_wgan_gp_seconds": seconds_per_step, | |
| "measured_step_seconds": measured_seconds, | |
| "parameter_gradient_l1": gradient_l1, | |
| "single_configuration_16000_step_hours": ( | |
| seconds_per_step * HORIZON_FROM_PUBLISHED_PLOTS / 3600.0 | |
| ), | |
| } | |
| ) | |
| del generator, discriminator, generator_optimizer, discriminator_optimizer | |
| gc.collect() | |
| total_hours_one_seed = sum( | |
| profile["single_configuration_16000_step_hours"] * 11 for profile in profiles | |
| ) | |
| payload = { | |
| "claim": 5, | |
| "route": 3, | |
| "route_name": "full-shape CPU feasibility calibration", | |
| "profile_scope": ( | |
| "Synthetic images; full reported resolutions and batch size; CNN and " | |
| "ResNet; WGAN-GP backward; parameter-gradient L1 measurement." | |
| ), | |
| "not_claim_evidence": True, | |
| "assumed_training_design": { | |
| "joint_critic_generator_ratio": "1:1 lower-cost calibration", | |
| "gradient_penalty": GRADIENT_PENALTY, | |
| "latent_dim": LATENT_DIM, | |
| "base_channels": BASE_CHANNELS, | |
| "optimizer_betas": [0.0, 0.9], | |
| "learning_rate": LEARNING_RATE, | |
| "published_plot_horizon": HORIZON_FROM_PUBLISHED_PLOTS, | |
| "configuration_count": CONFIGURATIONS, | |
| }, | |
| "cpu": { | |
| "estimated_required_cores": 8, | |
| "selected_backend": "hf", | |
| "selected_flavor": "cpu-upgrade", | |
| "cgroup_cpu_max": raw_quota, | |
| "actual_quota_cores": allocated_cores, | |
| "torch_threads": torch.get_num_threads(), | |
| }, | |
| "profiles": profiles, | |
| "projected_hours_one_seed_44_configs": total_hours_one_seed, | |
| "projected_hours_three_seeds_44_configs": total_hours_one_seed * 3.0, | |
| "projection_excludes": [ | |
| "dataset input pipeline", | |
| "Inception Score generation and evaluation", | |
| "additional critic updates", | |
| "uncertainty from the undisclosed architecture", | |
| ], | |
| "verdict": "BLOCKED", | |
| } | |
| (output_dir / "claim5_route3_cpu_profile.json").write_text( | |
| json.dumps(payload, indent=2) + "\n" | |
| ) | |
| return payload | |