Spaces:
Running
Running
Upload 2 files
Browse files- Quasar_axrvi_ranker.py +124 -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 |
-
|
| 1680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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": {
|