eoc-route-learned: recommend the stage cheapest in expectation, not the likeliest #439
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/eoc-route-cheapest"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The crate says a learned router "predicts the cheapest stage that is likely to satisfy the query". Three of its four routers recommended the stage likeliest to succeed instead. MF and both bandits took an argmax over success, and the bandits also ignored the joule cost passed to
observe. Neural succeeds on everything and costs the most, so these routers learned to send every query to Neural. The LinUCB synthetic test asserted exactly that as its goal ("converges to best arm": Neural). This came from the 2026-10-19 sweep (item 17, "the bandit reward ignores joules"). Checked on main, the problem is wider than that item said.evaluatehad a second defect. It charged every row's own recorded cost whatever the router chose, and priced the "full cascade" at 4× that. So every router reported a lift of 3× its spend, whatever it did.The rule
router::expected_joulesprices a skip the wayLearnedCascadedispatches it:cheapest_in_expectationpicks the stage with the lowest expected joules. A tie goes to the cheaper stage. A probability that is NaN or outside [0, 1] can only make a stage cost more, never less.Changes
observeand pick by the rule. LinUCB uses optimistic chances (the UCB) and Thompson uses sampled ones, so an untried cheap stage still gets tried. Confidence is now the mean belief, not the bound or sample used to explore; otherwise the threshold policy's skip would be a coin toss. States saved before this change import with the default costs.observe's contract: each call reports one stage that ran and that stage's own joules. When a dispatch runs several stages, report each one; a miss is evidence too.train_logreglabels each query with its cheapest successful stage. Rows with the same embedding count as one query. The classifier's online update no longer reinforces a correct stage costlier than the one it predicts, since every query gets a correct Neural answer.evaluatereplays each recommendation over per-stage outcomes:full_cascade_microjoulesandunscored.joules_per_correctisNonewhen nothing was correct. Zero would read as the best possible figure.What the tests show
On synthetic data whose coverage is soft and of the family the model fits, priced at each query's true chances:
With the default costs, Graph is 1% of a Neural call. The plain walk is then within 0.04% of the best, so routing has little to win. The router is held to within 1% of the best.
When Kv and Graph cost a real share of a Neural call, the walk is 4.5% above the best. At this training budget MF lands at about the walk's cost and does not capture that gap. Its learned chances are off by 5–9% per stage, and a wrong skip costs a whole Neural call. Training longer made it beat the walk by 2% while its chances got worse, so this is a limit of the estimator, not of the rule. The test therefore requires only that the router not lose to the walk, and the test doc records the measurement.
The bandits, with each stage that ran reporting its outcome:
Verification (build host)
cargo test -p eoc-route-learned: 48 pass.cargo clippy -p eoc-route-learned --all-targets --no-deps -D warningsis clean.observe, confidence from the bound or sample, the classifier gate (removed, or made strict), the label choice, eachevaluatepricing rule, the serde defaults, the EMA weight, and MF's confidence. Two survived the first run, the strict gate and MF's confidence, and each now has a test.cargo test --workspace --no-fail-fastfor eoc-rs: 763 pass, 0 fail.eoc-cascade'shit_calibration_process_energyis flaky on the shared host, independent of this change. Run alone it failed 1 of 3 runs on main, and 3 of 3 on this branch, which does not touch eoc-cascade. It passed inside the full workspace run.🤖 Generated with Claude Code