Coupled Workload-Order Gate / coupled_gate.py

Failed on benchmark

Raw ⬇ ZIP
  1import json, math, random
  2from pathlib import Path
  3import numpy as np
  4import torch
  5import torch.nn as nn
  6
  7SEED = 1599
  8random.seed(SEED); np.random.seed(SEED); torch.manual_seed(SEED)
  9torch.set_num_threads(4)
 10DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
 11try:
 12    if DEVICE == "cuda":
 13        torch.zeros(1, device="cuda").sum().item()
 14except Exception:
 15    DEVICE = "cpu"
 16
 17
 18def step(w, dt, s, admission):
 19    return torch.relu(w - dt) + admission * s
 20
 21
 22def deterministic_check():
 23    # Baseline admits a small first job and rejects a large second job.
 24    # Controlled does the opposite: it is initially lower, then exceeds baseline.
 25    dt, s1, s2 = 0.1, 2.0, 10.0
 26    wb = step(torch.tensor(0.), 0., s1, torch.tensor(1.))
 27    wc = step(torch.tensor(0.), 0., s1, torch.tensor(0.))
 28    wb = step(wb, dt, s2, torch.tensor(0.))
 29    wc = step(wc, dt, s2, torch.tensor(1.))
 30    return {"baseline_after_second": float(wb), "controlled_after_second": float(wc),
 31            "violation": float(torch.relu(wc-wb)), "order_loss": float(torch.relu(wc-wb)**2)}
 32
 33
 34def math_sweeps():
 35    # With first-job residual b and controlled residual zero, the next-job
 36    # violation is max(S2-b,0), hence onset at S2=b and quadratic loss above it.
 37    b, s2 = 1.9, np.linspace(0, 5, 101)
 38    observed = np.maximum(s2-b, 0.)
 39    predicted = np.maximum(s2-b, 0.)
 40    onset = float(s2[np.argmax(observed > 1e-9)])
 41    # Fit slope of loss vs (S2-b)^2 only in the active region.
 42    active = observed > 0
 43    x = (s2[active]-b)**2; y = observed[active]**2
 44    slope = float(np.dot(x,y)/np.dot(x,x))
 45    # Scaling sweep directly changes the excess gap.
 46    scales = np.array([0.5, 1., 1.5, 2.])
 47    gaps = np.maximum(scales*4-b, 0.)
 48    ratios = gaps**2 / (gaps[1]**2)
 49    return {"threshold_prediction": b, "threshold_observed_grid": onset,
 50            "quadratic_prediction_slope": 1.0, "quadratic_observed_slope": slope,
 51            "scale_factors": scales.tolist(), "loss_ratio_observed": ratios.tolist(),
 52            "loss_ratio_predicted": ((gaps/gaps[1])**2).tolist()}
 53
 54
 55def traces(n, device):
 56    # Paired arrivals and service costs. Baseline is a state-dependent reference
 57    # gate; the learned gate sees the same workload, size, and position.
 58    dt = torch.rand(n, 6, device=device) * .35 + .05
 59    s = torch.exp(torch.randn(n, 6, device=device)*.55 + .35).clamp(.2, 8.)
 60    return dt, s
 61
 62class Gate(nn.Module):
 63    def __init__(self):
 64        super().__init__()
 65        self.net = nn.Sequential(nn.Linear(4, 24), nn.Tanh(), nn.Linear(24, 1))
 66    def forward(self, x):
 67        return torch.sigmoid(self.net(x)).squeeze(-1)
 68
 69def baseline_decision(w, s):
 70    # Reference production policy: avoid admitting a job if current workload
 71    # is high or if the job is large. This creates the feedback counterexample.
 72    return ((w < 1.5) & (s < 3.0)).float()
 73
 74def rollout(model, dt, s, lam):
 75    n, k = s.shape
 76    wb = torch.zeros(n, device=dt.device); wc = torch.zeros(n, device=dt.device)
 77    violations=[]; probs=[]; accepted=[]; baseacc=[]
 78    for j in range(k):
 79        xb = torch.stack([wc, s[:,j], dt[:,j], torch.full_like(wc, j/(k-1))], 1)
 80        p = model(xb)
 81        ib = baseline_decision(wb, s[:,j])
 82        # Soft admission is the differentiable training surrogate.
 83        wb = step(wb, dt[:,j], s[:,j], ib)
 84        wc = step(wc, dt[:,j], s[:,j], p)
 85        v = torch.relu(wc-wb)
 86        violations.append(v); probs.append(p); accepted.append(p); baseacc.append(ib)
 87    v = torch.stack(violations,1); p = torch.stack(probs,1)
 88    # Throughput reward, mild workload regularization, plus proposed order term.
 89    objective = -p.mean() + .01*wc.mean() + lam*(v**2).mean()
 90    return objective, v.detach(), p.detach(), torch.stack(baseacc,1).detach(), wc.detach(), wb.detach()
 91
 92def train(lam, device):
 93    model=Gate().to(device); opt=torch.optim.Adam(model.parameters(),lr=.025)
 94    for epoch in range(350):
 95        dt,s=traces(96,device)
 96        loss,*_=rollout(model,dt,s,lam)
 97        opt.zero_grad(); loss.backward(); opt.step()
 98    with torch.no_grad():
 99        dt,s=traces(2048,device)
100        _,v,p,ba,wc,wb=rollout(model,dt,s,lam)
101        return {"violation_frequency":float((v>1e-5).any(1).float().mean()),
102                "mean_violation":float(v.mean()), "p99_violation":float(torch.quantile(v.flatten(),.99)),
103                "mean_admission_probability":float(p.mean()), "mean_final_workload":float(wc.mean()),
104                "baseline_mean_final_workload":float(wb.mean()),
105                "baseline_admission_rate":float(ba.mean())}
106
107def main():
108    checks=math_sweeps(); exact=deterministic_check()
109    results={}
110    for lam in (0.0, 0.5, 2.0):
111        results[str(lam)]=train(lam,DEVICE)
112    out={"device":DEVICE,"exact_counterexample":exact,"math_sweeps":checks,"training":results}
113    Path("results.json").write_text(json.dumps(out,indent=2))
114    print(json.dumps(out,indent=2))
115
116if __name__=="__main__": main()