Skip to content

feat: add per-topology-class residual correction (behind default-off flag) - #3

Open
madhugoutham wants to merge 2 commits into
mainfrom
feat/topology-residual-correction
Open

feat: add per-topology-class residual correction (behind default-off flag)#3
madhugoutham wants to merge 2 commits into
mainfrom
feat/topology-residual-correction

Conversation

@madhugoutham

Copy link
Copy Markdown
Owner

Summary

Adds TopologyCorrectionTable (common/topology_correction.py): a per-class TTFT correction computed out-of-fold on a frozen base model, applied after prediction rather than fed into the model as a raw training feature.

Why not a raw feature

Measured on real P/D traffic (predictor issue llm-d#30): feeding topology_distance directly into XGBoost let it steal decision-splitting capacity from higher-value continuous features, improving one class (zone: -42.8ms MAE) while regressing another (region: +20.3ms MAE, CI excludes zero - a real regression, not noise). Residual correction cannot cause that kind of cross-class regression, since no class's correction is estimated from or affects another class's data.

What changed

  • common/topology_correction.py - new, self-contained TopologyCorrectionTable class (fit/correction_for/serialize)
  • TrainingEntry / PredictionRequest (both servers) - new optional topology_distance field, default None
  • LatencyPredictor.train() - fits the table after each retrain, scoped to XGBoost only (the only model type this was validated against), gated behind settings.ENABLE_TOPOLOGY_CORRECTION (default false)
  • Persisted via the existing _get_model_paths()/joblib pattern; explicitly excluded from the /model/export seed bundle (a fresh replica should re-derive its own cluster's corrections, not inherit a source cluster's)
  • prediction_server.py - syncs and applies the table the same way as every other model artifact

Backward compatibility

The flag defaults to false. correction_for() returns exactly 0.0 for any absent/unrecognized class regardless of the flag, so every existing caller that doesn't send topology_distance is completely unaffected. Verified: existing test suite (tests/, 16 cases) passes unchanged; both servers import and construct cleanly with the feature present but unconfigured.

Validation this session

  • 8 new unit tests (tests/test_topology_correction.py): no-op guarantees (None, unknown class, empty table), correct bias-recovery direction on synthetic data with a known injected gap, min-sample-per-class skipping, joblib serialization round-trip, input validation
  • Confirmed via companion PR (test: add real P/D reference trace and topology feature A/B result #2, real Kermit P/D data + the team's own offline_feature_ab.py self-test): this approach is neutral (no regression) on real production-shaped data

Explicitly deferred - do not enable yet

This is not validated for any real deployment. Measured neutral (no improvement, no harm) on Kermit's single-region, uniform-InfiniBand fabric - the real topology gap there (~37ms) is too small relative to this model's own ~750-800ms p90 margin to move the needle either way. Do not flip LATENCY_ENABLE_TOPOLOGY_CORRECTION on in any real deployment without first validating on a cluster with a larger real topology gap (heterogeneous fabric / cross-region).

Not covered in this PR (follow-up)

  • predict_batch() does not yet apply the correction (only predict() does) - same wiring needed, kept out to keep this diff reviewable
  • The existing tests/test_dual_server_client.py integration suite (live server processes) was not re-run locally in this session (requires infra beyond what's available here) - please confirm it stays green in CI

Scope note

Sandboxed on my fork. Not proposing this upstream, and not closing predictor issue llm-d#30 - the issue is a design investigation, and this PR's own honest conclusion is "safe but not yet proven beneficial," not "solved."

…flag)

Adds TopologyCorrectionTable (common/topology_correction.py): a per-class
TTFT correction computed out-of-fold on a frozen base model, rather than
feeding topology_distance directly into the model as a raw feature.

Why not a raw feature: measured on real P/D traffic (predictor issue llm-d#30),
feeding a low-cardinality topology label into XGBoost let it steal
decision-splitting capacity from higher-value continuous features, improving
one topology class (zone: -42.8ms MAE) while regressing another (region:
+20.3ms MAE, CI excludes zero). The residual-correction approach cannot
cause that kind of cross-class regression, because no class's correction is
estimated from or affects another class's data - confirmed by two
independent test datasets in this session (real Kermit data: neutral, no
regression; synthetic data: no regression on either gap size tested).

Wiring:
- TrainingEntry / PredictionRequest (both servers): new optional
  topology_distance field, default None
- LatencyPredictor.train(): fits the correction table after each retrain,
  scoped to XGBoost only (the only model type this was validated against),
  gated behind settings.ENABLE_TOPOLOGY_CORRECTION (default false)
- Persisted via the existing _get_model_paths()/joblib pattern; excluded
  from the /model/export seed bundle (a fresh replica should re-derive its
  own cluster's corrections, not inherit a source cluster's)
- prediction_server.py: syncs and applies the table the same way as every
  other model artifact; correction_for() is an unconditional 0.0 no-op
  when topology_distance is absent or the table is empty

Backward compatibility: the flag defaults to false, and correction_for()
returns exactly 0.0 for any absent/unrecognized class regardless of the
flag, so every existing caller that doesn't send topology_distance is
completely unaffected.

Explicitly deferred: enabling this in any real deployment. Measured neutral
(no improvement, no harm) on Kermit's single-region uniform-InfiniBand
fabric - the real gap there (~37ms) is too small relative to this model's
own ~750-800ms p90 margin to show a measurable difference either way. Do
not enable without validating on a cluster with a larger real topology gap
first.

Tests: tests/test_topology_correction.py (8 cases - no-op guarantees, bias
recovery direction, min-sample skipping, serialization round-trip).

Signed-off-by: Madhu Goutham Reddy Ambati <mambati@redhat.com>
Real regression caught via live integration testing (not the unit test
suite, which doesn't exercise the raw DataFrame path): training_server.py's
train() runs `pd.DataFrame(clean_ttft).dropna()` on the FULL raw training
row - every column, not just the ones actually used as features - before
any feature selection happens. pandas treats None as NaN once it's in a
DataFrame, and .dropna() defaults to dropping a row if ANY column is NaN.

Defaulting topology_distance to None therefore silently discarded every
row from every caller that doesn't send it - which, for a brand new
optional field, was every existing caller, 100% of the time. Confirmed via
a live training/prediction server pair: 3,000 real training samples
submitted, zero survived to actually train on.

Fix: default to "" instead, matching the existing pod_type field's already-
safe convention exactly (empty string survives .dropna(); None does not).
Also updated TopologyCorrectionTable.fit()/correction_for() and the
retrain trigger condition to treat "" as "no topology known", consistent
with None, so an empty-string absence never gets fit as a spurious real
class or looked up as one.

Verified against a real, live training+prediction server pair (matching
the actual Dockerfile invocation, not just unit tests): the existing
tests/test_dual_server_client.py::test_dual_server_quantile_regression_learns_distribution_stress
integration test, which exercises this exact path with real HTTP traffic,
now passes. It did not before this fix, even with training/training_server.py's
unrelated pre-existing _drop_timestamp bug fixed locally to isolate this
specific issue.

Added tests/test_topology_correction.py::TestTopologyDistanceWireDefaultIsDropnaSafe
as a permanent regression guard, plus two more cases confirming "" is never
fit or looked up as a real topology class.

Signed-off-by: Madhu Goutham Reddy Ambati <mambati@redhat.com>
@madhugoutham

Copy link
Copy Markdown
Owner Author

Update: found and fixed a real bug in this PR itself via live integration testing (not caught by the unit test suite alone).

The bug: topology_distance defaulted to None. training_server.py's train() runs .dropna() on the full raw training row before feature selection - None becomes NaN in the DataFrame, and .dropna() drops any row with any NaN. Since no existing caller sends this new field, it silently discarded 100% of training data for every caller, always.

Fix: default to "" instead, matching pod_type's existing safe convention exactly. Updated TopologyCorrectionTable and the retrain trigger condition to treat "" the same as absent. Added a permanent regression test (TestTopologyDistanceWireDefaultIsDropnaSafe) that fails if this default is ever changed back to None.

How I caught it: ran the actual tests/test_dual_server_client.py integration suite against real, live training+prediction servers (matching the Dockerfile's own invocation) instead of relying on unit tests alone. That test failed before this fix and passes after it.

Also found, while doing this: a separate, pre-existing, unrelated bug (self._drop_timestamp called on a module-level function, not a class method - training_server.py lines 739/884/993/1071) that breaks train() entirely regardless of this PR. Not fixed here (out of scope, affects main independent of any topology work) - worth its own issue/PR.

Also found a second, separate, unrelated pre-existing calibration issue in test_tif_features_quantile_learns_distribution_stress (high-TIF-bucket TPOT coverage), confirmed via isolated testing to have zero connection to topology work either.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

This PR is marked as stale after 21d of inactivity. After an additional 14d of inactivity (7d to become rotten, then 7d more), it will be closed. To prevent this PR from being closed, add a comment or remove the lifecycle/stale label.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant