Skip to content

Adaptive link: live counterfactual MCS probes for measured candidate PER #274

Description

@josephnef

Motivation

The adaptive controller currently observes SNR/PER at the selected operating point and uses link_model to infer whether other MCS rows would work. That is fast, but the counterfactual can be wrong on real hardware: MCS-specific receiver behavior, multipath, interference, PA distortion, temperature, and calibration error are not fully represented by one SNR measurement.

Rate controllers such as Fluke address this by inserting low-duty probes at other MCSes. The receiver therefore learns the actual PER of candidate rates instead of extrapolating all of them from the active rate.

Devourer already has the necessary building blocks:

  • tests/link_probe.{sh,py} performs an offline MCS sweep and reports per-MCS delivery/SNR.
  • rc_proto.probe_bw() defines a deterministic, zero-wire-overhead bandwidth-probe schedule from the 12-bit application sequence number.
  • AdaptiveVtx.probe_bw_for_seq() exposes the per-packet override to the injector.
  • score.RungWindow attributes both received probes and gap-inferred missing probes to a bandwidth rung.
  • Controller.report_rung_delivery() consumes measured candidate delivery and temporarily blocks bad bandwidth rows.

We should reuse this shape for continuous MCS probing, while keeping Devourer's objective: minimum energy per delivered bit subject to the per-layer delivery SLA, not simply the highest MCS.

Proposed first version

Add deterministic, low-duty probes for the MCS immediately above and below the selected MCS. Keep bandwidth, TXAGC, guard interval, and FEC unchanged during an MCS probe so the observation isolates the rate dimension.

Example while the commanded row is MCS4:

normal frames: MCS4
up probe:      MCS5 at commanded BW/TXAGC/FEC
down probe:    MCS3 at commanded BW/TXAGC/FEC

Both endpoints derive the probe assignment from (video_seq, profile/epoch). The first version should not add a per-frame control field. A schedule epoch or commanded profile must be part of the derivation (or reset the estimator on a profile change), otherwise delayed/reordered frames around a rate transition can be attributed to the wrong candidate.

Do not combine MCS and bandwidth exploration in the same probe frame. Allocate disjoint schedule slots so every result has one changed variable.

Implementation sketch

1. Shared probe schedule (tools/precoder/rc_proto.py)

  • Generalize the probe API or add probe_mcs(seq, selected_mcs, mcs_set, epoch=...) -> int | None.
  • Only return adjacent valid MCSes initially: selected-1 and selected+1.
  • Give bandwidth and MCS probes disjoint slots.
  • Make the duty configurable, with a conservative default (target 2-5% total MCS probes).
  • Preserve deterministic behavior and the 12-bit sequence wrap.
  • Document schedule/config equality as a protocol invariant between VTX and VRX.

2. Per-packet VTX override (tools/precoder/adaptive_link.py and injector plumbing)

  • Add an AdaptiveVtx.probe_mcs_for_seq() companion to probe_bw_for_seq().
  • Apply the returned rate through the existing per-packet radiotap MCS/rate path.
  • Keep the commanded BW, TXAGC and FEC for the probe.
  • Disable speculative upward probes in failsafe/listen/rendezvous states.
  • Expose counters for attempted probes by candidate MCS.

3. Receiver estimator (tools/precoder/score.py)

Add McsProbeWindow, analogous to RungWindow:

stats() -> {mcs: ProbeStat(delivery, successes, attempts, confidence, age_ms)}
  • Attribute received scheduled sequences as successes.
  • Attribute wrap-safe sequence gaps as failures only when the missing sequence was a scheduled MCS probe.
  • Cap monster-gap walks as RungWindow does; a link outage is not useful per-MCS evidence.
  • Reset or separate windows on profile/schedule-epoch changes.
  • Expire stale observations; evidence from an earlier channel state must not authorize a promotion.
  • Track raw probe delivery separately from post-FEC/application delivery. The controller's SLA authority remains post-FEC delivery.

Use a small-binomial confidence bound (Wilson or Beta posterior), not a bare mean. At low target PER, absence of a few failures is weak evidence.

4. Controller integration (tools/precoder/controller.py)

Add report_mcs_delivery(stats, now_ms) and use probe evidence as a correction/gate on the model, not as a complete replacement:

  • SNR/link model remains the prior and fast emergency signal.
  • A faster row may be entered only when its probe lower confidence bound clears the layer target plus hysteresis and the row improves energy/bit by improve_frac.
  • A candidate whose upper confidence bound is below target is temporarily blocked.
  • Failure of the active row still triggers the existing fast downgrade; do not wait to collect probes.
  • Downward-probe success can distinguish "drop MCS" from "all rates are failing".
  • Price probe airtime and failed-probe bytes in the energy accounting, or at minimum report probe overhead separately in v1 so an apparent energy win cannot hide exploration cost.

Avoid permanently replacing LinkRow.snr_req with a short-window empirical threshold. A useful follow-up is a bounded online per-MCS calibration offset learned from probes.

5. Layer-aware safety

An unsuccessful upward probe must not damage indispensable video:

  • Never place speculative upward probes on RCF/DISC, base-layer critical data, IDR/key frames, or irreplaceable FEC metadata.
  • Prefer disposable enhancement-layer payloads for upward probes.
  • Downward probes are safe for all data but still consume extra airtime.
  • If no enhancement traffic exists, use explicit probe/padding frames or duplicated parity rather than relabeling a critical base frame.
  • Maintain separate "intentional probe loss" metrics so it does not falsely penalize the user-facing base-layer SLA or alink-compatible score.
  • Enforce a hard exploration airtime/energy budget and suspend upward probing during thermal back-off, congestion, feedback loss, or rapid delivery collapse.

Suggested configuration

Names are illustrative:

mcs_probe_enabled: bool = False       # opt-in until on-air validated
mcs_probe_period: int = 64
mcs_probe_min_samples: int = 12
mcs_probe_window_samples: int = 64
mcs_probe_max_age_ms: int = 3000
mcs_probe_block_hold_ms: int = 5000
mcs_probe_confidence: float = 0.90

The sample thresholds must be tuned to the traffic rate and target PER. A 0/12 failure record must not be interpreted as proof of 99% delivery.

Validation

Unit tests

  • Shared VTX/VRX schedule, disjoint from bandwidth-probe slots.
  • Boundary behavior at lowest/highest MCS and sequence wrap.
  • Correct success/failure attribution, including gaps, reordering policy, epoch changes, and stale-data expiry.
  • No upward probes in failsafe or on protected frame classes.
  • Confidence-bound promotion/block decisions.
  • A profile change cannot reuse evidence collected for a different (MCS, BW, TXAGC, FEC) context.

Simulation

Deliberately bias individual MCS thresholds in the simulated channel so the static model is wrong. Compare:

  1. model-only controller;
  2. model + live adjacent-MCS probes.

Run static, fly-out-and-back, time-correlated fading, and burst-interference cases. Report:

  • energy per delivered source bit, including probes;
  • base/critical and enhancement delivery separately;
  • time to discover/promote a better MCS;
  • time to reject a bad MCS;
  • probe airtime/loss budget;
  • operating-point churn.

On-air A/B

Add an opt-in harness using the existing adaptive-link and B210 interferer setup. Run paired seeds/interference schedules with probing off/on. Require that probing:

  • does not reduce base/critical delivery below its SLA;
  • discovers an intentionally model-mismatched MCS boundary;
  • improves or preserves total energy/delivered-bit after counting probes;
  • stays within the configured exploration budget;
  • does not increase profile flapping materially.

Acceptance criteria

  • VTX and VRX share a deterministic, versioned MCS-probe schedule with no attribution ambiguity across profile changes.
  • Live per-packet rate override sends adjacent-MCS probes while holding other levers fixed.
  • VRX produces per-MCS attempted/received/lost counts plus confidence and age.
  • Controller uses probe evidence to gate promotions and block empirically bad candidates while retaining fast SNR/model fallback.
  • Upward probes cannot consume protected base/IDR/control frames.
  • Probe cost is included in, or explicitly adjacent to, the energy metric.
  • Unit and simulation tests cover miscalibrated per-MCS models and sequence/epoch edge cases.
  • On-air A/B results are documented before enabling the feature by default.

Non-goals for the first version

  • Full multi-dimensional bandit exploration across MCS x BW x TXAGC x FEC.
  • Per-packet ACK/ARQ.
  • Probing every MCS continuously.
  • Replacing the link model or emergency downgrade path.
  • Enabling exploration by default before on-air validation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions