KarlQuant commited on
Commit
08c57fe
Β·
verified Β·
1 Parent(s): 9485efe

Upload 2 files

Browse files
Files changed (2) hide show
  1. Quasar_axrvi_ranker.py +124 -2
  2. websocket_hub.py +34 -31
Quasar_axrvi_ranker.py CHANGED
@@ -1344,6 +1344,21 @@ class AdaptiveNormalizer:
1344
 
1345
  def update_and_normalize(self, x: np.ndarray) -> np.ndarray:
1346
  x = np.asarray(x, dtype=np.float32)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1347
  if self.n == 0:
1348
  self.mean = x.copy()
1349
  self.var = np.ones_like(x)
@@ -1354,6 +1369,17 @@ class AdaptiveNormalizer:
1354
  # Use old_mean so the variance delta is computed before the mean shifts
1355
  self.var = (1 - self.momentum) * self.var + self.momentum * (x - old_mean) ** 2
1356
  self.n += 1
 
 
 
 
 
 
 
 
 
 
 
1357
  return np.clip((x - self.mean) / (np.sqrt(self.var) + self.eps), -5.0, 5.0)
1358
 
1359
  def state_dict(self) -> dict:
@@ -1376,6 +1402,24 @@ class AdaptiveNormalizer:
1376
  self.var = np.array(state["var"], dtype=np.float32)
1377
  self.n = state["n"]
1378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1379
 
1380
  # ══════════════════════════════════════════════════════════════════════════════════════
1381
  # SECTION 6 β€” UNIFIED FEATURE ENGINE (26-dim)
@@ -1676,8 +1720,26 @@ class UnifiedFeatureEngine:
1676
  rets = np.array(list(self._returns)[-20:])
1677
  vols = np.abs(rets)
1678
  if len(rets) > 1:
1679
- corr = np.corrcoef(rets[:-1], vols[1:])[0, 1]
1680
- raw[22] = float(np.clip(corr, -1.0, 1.0))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1681
 
1682
  # [23] Martingale deviation (variance-ratio test)
1683
  if (self.stoch_config.use_martingale_deviation
@@ -5881,6 +5943,39 @@ class HybridTrainer:
5881
  + self.lambda_align * l_align)
5882
 
5883
  # ── Backward pass (AMP-aware) ─────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5884
  self.optimizer.zero_grad()
5885
  self.scaler.scale(loss).backward()
5886
  # Unscale before clipping so grad norms are in the original fp32 scale
@@ -10198,6 +10293,33 @@ class RankerCheckpointManager:
10198
  if incompatible.unexpected_keys:
10199
  logger.warning(f"[Restore] axrvi_net unexpected keys: {incompatible.unexpected_keys}")
10200
  logger.info(" βœ… axrvi_net restored")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10201
  else:
10202
  logger.info(" ⏭️ axrvi_net skipped (asset count mismatch β€” fresh weights kept)")
10203
 
 
1344
 
1345
  def update_and_normalize(self, x: np.ndarray) -> np.ndarray:
1346
  x = np.asarray(x, dtype=np.float32)
1347
+
1348
+ # FIX: this is an EMA (mean = (1-m)*mean + m*x). A single NaN/Inf
1349
+ # in x poisons `mean`/`var` PERMANENTLY from that point on β€” every
1350
+ # future update is (1-m)*NaN + m*(clean value) = NaN forever,
1351
+ # even once the input goes back to being perfectly clean. That
1352
+ # state is also what gets checkpointed, so one bad tick anywhere
1353
+ # upstream (e.g. a degenerate correlation calc dividing by a
1354
+ # near-zero stddev) permanently corrupts this feature dimension
1355
+ # across every future restart. Replace non-finite inputs with the
1356
+ # current running mean (best available "neutral" estimate, and
1357
+ # exactly zero-effect on the mean update) so a transient bad
1358
+ # value never enters the EMA at all.
1359
+ if not np.all(np.isfinite(x)):
1360
+ x = np.where(np.isfinite(x), x, self.mean if self.n > 0 else 0.0)
1361
+
1362
  if self.n == 0:
1363
  self.mean = x.copy()
1364
  self.var = np.ones_like(x)
 
1369
  # Use old_mean so the variance delta is computed before the mean shifts
1370
  self.var = (1 - self.momentum) * self.var + self.momentum * (x - old_mean) ** 2
1371
  self.n += 1
1372
+
1373
+ # Self-heal: if mean/var are *already* NaN (e.g. restored from a
1374
+ # checkpoint saved during an earlier NaN episode, before this fix
1375
+ # existed), don't let that dead state haunt every future call β€”
1376
+ # reset the poisoned dims back to a neutral prior so training can
1377
+ # actually recover instead of being stuck at NaN forever.
1378
+ bad = ~np.isfinite(self.mean) | ~np.isfinite(self.var)
1379
+ if np.any(bad):
1380
+ self.mean = np.where(bad, 0.0, self.mean)
1381
+ self.var = np.where(bad, 1.0, self.var)
1382
+
1383
  return np.clip((x - self.mean) / (np.sqrt(self.var) + self.eps), -5.0, 5.0)
1384
 
1385
  def state_dict(self) -> dict:
 
1402
  self.var = np.array(state["var"], dtype=np.float32)
1403
  self.n = state["n"]
1404
 
1405
+ # FIX: existing checkpoints (including the one currently deployed)
1406
+ # were saved *while* a NaN was already loose in the EMA state β€”
1407
+ # see update_and_normalize() for how a single bad tick poisons
1408
+ # mean/var permanently. Restoring that NaN state verbatim would
1409
+ # keep the corruption alive forever across every future restart.
1410
+ # Reset only the poisoned dims to a neutral prior; everything
1411
+ # else the checkpoint learned is kept intact.
1412
+ bad = ~np.isfinite(self.mean) | ~np.isfinite(self.var)
1413
+ if np.any(bad):
1414
+ n_bad = int(bad.sum())
1415
+ self.mean = np.where(bad, 0.0, self.mean)
1416
+ self.var = np.where(bad, 1.0, self.var)
1417
+ logger.warning(
1418
+ f"[AdaptiveNormalizer] Healed {n_bad} NaN/Inf dimension(s) "
1419
+ f"in restored normalizer state (checkpoint was saved during "
1420
+ f"a prior NaN training episode) β€” reset to neutral prior."
1421
+ )
1422
+
1423
 
1424
  # ══════════════════════════════════════════════════════════════════════════════════════
1425
  # SECTION 6 β€” UNIFIED FEATURE ENGINE (26-dim)
 
1720
  rets = np.array(list(self._returns)[-20:])
1721
  vols = np.abs(rets)
1722
  if len(rets) > 1:
1723
+ # FIX: np.corrcoef divides internally by each series' std
1724
+ # dev. Early after a restart (or any low-tick-history
1725
+ # window) rets[:-1] or vols[1:] can be near-constant
1726
+ # (stddevβ‰ˆ0) β€” corrcoef then returns NaN silently, and
1727
+ # np.clip does NOT sanitize NaN, it passes it straight
1728
+ # through. That single NaN feature poisons the whole
1729
+ # forward pass (every downstream tensor becomes NaN),
1730
+ # which is exactly what was showing up as
1731
+ # "gate=nan | align_loss=nan" on the very first forward
1732
+ # after every restart β€” the checkpoint kept re-saving
1733
+ # the corruption. Skip the correlation (leave the
1734
+ # feature at its neutral default) when either series
1735
+ # has ~zero variance instead of computing a meaningless
1736
+ # ratio.
1737
+ r_std = np.std(rets[:-1])
1738
+ v_std = np.std(vols[1:])
1739
+ if r_std > 1e-12 and v_std > 1e-12:
1740
+ corr = np.corrcoef(rets[:-1], vols[1:])[0, 1]
1741
+ if np.isfinite(corr):
1742
+ raw[22] = float(np.clip(corr, -1.0, 1.0))
1743
 
1744
  # [23] Martingale deviation (variance-ratio test)
1745
  if (self.stoch_config.use_martingale_deviation
 
5943
  + self.lambda_align * l_align)
5944
 
5945
  # ── Backward pass (AMP-aware) ─────────────────────────────────────────
5946
+ # FIX: if loss is NaN/Inf, backward() propagates NaN gradients to
5947
+ # EVERY parameter that participated in the forward pass, and the
5948
+ # very next optimizer.step() then sets all of those parameters to
5949
+ # NaN in one shot β€” permanently, since NaN arithmetic never
5950
+ # recovers on its own. That NaN state then gets checkpointed and
5951
+ # reloaded on every future restart, which is exactly the
5952
+ # "gate=nan | align_loss=nan" seen from fwd#1 immediately after
5953
+ # every restore. Skip the step entirely when this happens so a
5954
+ # single bad batch (e.g. degenerate features at startup) can't
5955
+ # take down the whole model β€” the checkpoint stays as it was and
5956
+ # training just tries again on the next cycle.
5957
+ if not torch.isfinite(loss):
5958
+ logger.error(
5959
+ f"[HybridTrainer] ⚠️ Skipping optimizer step at train_step="
5960
+ f"{self.train_step} β€” loss is {loss.item()} (non-finite). "
5961
+ f"This would otherwise permanently poison model weights "
5962
+ f"with NaN. Model/optimizer state left untouched; "
5963
+ f"training will retry on the next batch."
5964
+ )
5965
+ self.optimizer.zero_grad()
5966
+ return {
5967
+ "total": float("nan"), "rl": float("nan"), "ce": float("nan"),
5968
+ "rank": float("nan"), "risk": float("nan"), "ql": float("nan"),
5969
+ "rl_norm": float("nan"), "ce_norm": float("nan"),
5970
+ "rank_norm": float("nan"), "risk_norm": float("nan"),
5971
+ "ql_norm": float("nan"), "div": float("nan"), "moe": float("nan"),
5972
+ "gate": float("nan"), "crps": float("nan"), "rent": float("nan"),
5973
+ "align": float("nan"), "grad_norm": 0.0,
5974
+ "reward_mean": 0.0, "reward_std": 0.0, "reward_absmax": 0.0,
5975
+ "step": self.train_step,
5976
+ "skipped_nan_step": True,
5977
+ }
5978
+
5979
  self.optimizer.zero_grad()
5980
  self.scaler.scale(loss).backward()
5981
  # Unscale before clipping so grad norms are in the original fp32 scale
 
10293
  if incompatible.unexpected_keys:
10294
  logger.warning(f"[Restore] axrvi_net unexpected keys: {incompatible.unexpected_keys}")
10295
  logger.info(" βœ… axrvi_net restored")
10296
+
10297
+ # FIX: if a prior session's loss went NaN and its
10298
+ # optimizer.step() ran anyway, every parameter that
10299
+ # received a gradient that step becomes NaN permanently
10300
+ # (NaN arithmetic never self-corrects) β€” and that NaN
10301
+ # state is exactly what gets checkpointed and re-restored
10302
+ # here on every future run. Detect it and reinitialise
10303
+ # only the poisoned tensors (leaving every healthy,
10304
+ # actually-trained parameter untouched) so a single past
10305
+ # bad step doesn't permanently brick the model.
10306
+ poisoned = []
10307
+ for name, param in bridge.axrvi_net.named_parameters():
10308
+ if not torch.isfinite(param.data).all():
10309
+ with torch.no_grad():
10310
+ if param.dim() >= 2:
10311
+ torch.nn.init.xavier_uniform_(param.data, gain=0.5)
10312
+ else:
10313
+ torch.nn.init.zeros_(param.data)
10314
+ poisoned.append(name)
10315
+ if poisoned:
10316
+ logger.warning(
10317
+ f"[Restore] ⚠️ Healed {len(poisoned)} NaN/Inf parameter "
10318
+ f"tensor(s) from a previously-corrupted checkpoint "
10319
+ f"(reinitialised fresh β€” these had already permanently "
10320
+ f"lost their trained values to a prior NaN training "
10321
+ f"step): {poisoned[:8]}{' …' if len(poisoned) > 8 else ''}"
10322
+ )
10323
  else:
10324
  logger.info(" ⏭️ axrvi_net skipped (asset count mismatch β€” fresh weights kept)")
10325
 
websocket_hub.py CHANGED
@@ -53,7 +53,39 @@ from typing import Dict, List, Optional, Set
53
  import uvicorn
54
  from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
55
  from fastapi.middleware.cors import CORSMiddleware
56
- from fastapi.responses import FileResponse, JSONResponse
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  # ─── Logging ────────────────────────────────────────────────────────────────────────
59
  logging.basicConfig(
@@ -1982,40 +2014,11 @@ async def api_flips_asset(asset: str):
1982
  return JSONResponse({"ok": True, "flip": flip, "hub_timestamp": time.time()})
1983
 
1984
 
1985
- def _json_safe(obj):
1986
- """
1987
- Recursively replace NaN / +Inf / -Inf floats with None so the
1988
- standard-library json encoder (used by Starlette's JSONResponse,
1989
- which calls json.dumps with allow_nan defaulting to True but still
1990
- raises on Inf/NaN found *within nested dict/list structures* under
1991
- strict encoders) never raises "Out of range float values are not
1992
- JSON compliant". These values leak in from live model outputs
1993
- (e.g. a confidence/profit computed from a divide-by-zero or an
1994
- exploded loss during a bad training step) and previously took the
1995
- whole /api/state endpoint down with a 500 until the offending
1996
- snapshot aged out.
1997
- """
1998
- if isinstance(obj, float):
1999
- if math.isnan(obj) or math.isinf(obj):
2000
- return None
2001
- return obj
2002
- if isinstance(obj, dict):
2003
- return {k: _json_safe(v) for k, v in obj.items()}
2004
- if isinstance(obj, (list, tuple)):
2005
- return [_json_safe(v) for v in obj]
2006
- return obj
2007
-
2008
-
2009
- def SafeJSONResponse(content: dict, **kwargs) -> JSONResponse:
2010
- """Drop-in replacement for JSONResponse that sanitizes NaN/Inf first."""
2011
- return JSONResponse(_json_safe(content), **kwargs)
2012
-
2013
-
2014
  @app.get("/api/state")
2015
  async def api_state():
2016
  """Full dashboard state polled by hub_dashboard.html every 2 s."""
2017
  rankings = _compute_rankings()
2018
- return SafeJSONResponse({
2019
  "rankings": rankings,
2020
  "metric_history": manager.get_metric_history(),
2021
  "health": {
 
53
  import uvicorn
54
  from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
55
  from fastapi.middleware.cors import CORSMiddleware
56
+ from fastapi.responses import FileResponse, JSONResponse as _RawJSONResponse
57
+
58
+
59
+ def _json_safe(obj):
60
+ """
61
+ Recursively replace NaN / +Inf / -Inf floats with None. These values
62
+ leak in from live model outputs (e.g. a confidence/profit computed
63
+ during a NaN training step, which is exactly what's happening
64
+ upstream in the ranker right now) and crash json.dumps with
65
+ "Out of range float values are not JSON compliant" β€” which previously
66
+ took down whichever endpoint happened to serialize them (api/state,
67
+ api/ranker/logs/recent, etc. β€” this hit each one separately since
68
+ every route calls JSONResponse independently).
69
+ """
70
+ if isinstance(obj, float):
71
+ if math.isnan(obj) or math.isinf(obj):
72
+ return None
73
+ return obj
74
+ if isinstance(obj, dict):
75
+ return {k: _json_safe(v) for k, v in obj.items()}
76
+ if isinstance(obj, (list, tuple)):
77
+ return [_json_safe(v) for v in obj]
78
+ return obj
79
+
80
+
81
+ def JSONResponse(content=None, *args, **kwargs):
82
+ """
83
+ Drop-in replacement for starlette's JSONResponse that sanitizes
84
+ NaN/Infinity before serialization. Shadowing the name here (rather
85
+ than patching each call site) means every existing `JSONResponse(...)`
86
+ call below β€” and any added later β€” is covered automatically.
87
+ """
88
+ return _RawJSONResponse(_json_safe(content), *args, **kwargs)
89
 
90
  # ─── Logging ────────────────────────────────────────────────────────────────────────
91
  logging.basicConfig(
 
2014
  return JSONResponse({"ok": True, "flip": flip, "hub_timestamp": time.time()})
2015
 
2016
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2017
  @app.get("/api/state")
2018
  async def api_state():
2019
  """Full dashboard state polled by hub_dashboard.html every 2 s."""
2020
  rankings = _compute_rankings()
2021
+ return JSONResponse({
2022
  "rankings": rankings,
2023
  "metric_history": manager.get_metric_history(),
2024
  "health": {