Skip to content

Commit 3e9be44

Browse files
docs/index.html: fold BLER mechanism into P6 card; status doc: RLLA root cause
docs/index.html: mirrors README.md (c012148) -- extends P6's card with the same BLER-vs-target finding (RLLA 0.13x-0.17x, LTSLA on-target at 3 km/h but 1.57x-2.07x at 30/120 km/h) instead of leaving the bare gain figures unexplained, using the file's own metric-span + status-doc-link convention. P6_reaudit_status.md SS9: identifies the specific mechanism behind RLLA's BLER undershoot (SS8), not just that it's miscalibrated. EpsilonGreedyRLLA's optimistic init (0.9) is set exactly equal to its own feasibility threshold (1-target_bler=0.90), so a single NACK on any untried arm drops it just below the cutoff, permanently blocking greedy re-selection except via rare, slow epsilon-driven recovery. A throwaway single-seed trace (not committed, no changes to rl_core.py/runner.py) confirms this empirically: mean selected MCS converges to ~5.6-5.9 regardless of speed (3 km/h and 30 km/h land on nearly the same steady state), ruling out a Doppler-tracking explanation and pointing at the bandit's own estimation dynamics instead. Design/calibration issue, not a coding bug or deliberate reward shaping; no fix attempted, per scope.
1 parent c012148 commit 3e9be44

2 files changed

Lines changed: 198 additions & 1 deletion

File tree

docs/index.html

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,9 @@ <h3>Reinforcement-Learning Link Adaptation</h3>
362362
<span class="metric">LTSLA −1.60% mean / +10.06% best-config vs OLLA (n=5 seeds, N_TTIS=30000)</span>
363363
<span class="metric">Not clearly distinguishable from zero overall — favorable only at 3 km/h</span>
364364
<span class="metric">Thompson sampling · ε-greedy · Bayesian SINR posterior</span>
365-
<span class="metric">RLLA −37.40% mean; see docs/redesign/P6_reaudit_status.md</span>
365+
<span class="metric">RLLA −37.40% mean — BLER 0.13×–0.17× of the 10% target at every config (OLLA validated at 0.99×–1.01×): massive conservatism, likely an EpsilonGreedyRLLA MCS-selection calibration issue, not a weaker-algorithm story</span>
366+
<span class="metric">LTSLA BLER on-target at 3 km/h (genuine efficiency win) but 1.57×–2.07× of target at 30/120 km/h, worsening monotonically with speed — an under-regulated reliability problem there, not just a suboptimal-but-safe MCS choice</span>
367+
<span class="metric">Open question, not concluded: the live-Doppler forgetting factor may itself be under-compensating as speed increases — full BLER table & methodology: <a href="https://github.com/karrthikmohank/5g-nr-mac-simulator/blob/main/docs/redesign/P6_reaudit_status.md" target="_blank">status doc §8 ↗</a> (covers only P6's own 6-config × 3-arm grid — no other project's committed data has an equivalent check)</span>
366368
</div>
367369
</div>
368370

docs/redesign/P6_reaudit_status.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,3 +776,198 @@ It does **not** resolve the equivalent gap for:
776776
| Test suite result after the change | 235 passed, 0 failed (`pytest tests/`) |
777777
| HEAD at start of this section | `81c76bb` (this document's own §7 commit) |
778778
| This section | Drafted 2026-07-18, same session as the diagnostic run |
779+
780+
## 9. RLLA mechanism — why is realized BLER 0.13×–0.17× of target, specifically?
781+
782+
Continues §8 (same commit lineage: `81c76bb`, `5e40240`, `c012148`). §8 established
783+
*that* RLLA's BLER is far under target; this section identifies *why*, from
784+
`EpsilonGreedyRLLA`'s code (`projects/p6_rl_link_adapt/rl_core.py`, lines
785+
178–264, read in full this session) plus a throwaway empirical trace. No
786+
committed file was modified to reach this conclusion (confirmed: `git diff
787+
--stat projects/p6_rl_link_adapt/rl_core.py projects/p6_rl_link_adapt/runner.py`
788+
returns empty at the time of writing).
789+
790+
### 9.1 Part 1 — code reading
791+
792+
**1. Action space / MCS range — ruled out as a cause.** `_MCS_INDICES =
793+
sorted(NRConstants.MCS_TABLE.keys())` (`rl_core.py` line 61) — confirmed via
794+
`nr_simulator/constants.py` line 111 (`N_MCS = len(MCS_TABLE) # 29 (0..28)`)
795+
this is the full 0–28 range. `EpsilonGreedyRLLA.select_mcs` draws
796+
exploration arms from `_MCS_INDICES` directly (line 232: `self._rng.choice
797+
(_MCS_INDICES)`) and scans the same set for the greedy choice (line 240:
798+
`for mcs in _MCS_INDICES`) — identical to the shared `_MCS_INDICES` global
799+
that `ThompsonSamplingLA`/`_best_mcs_for_sinr` (LTSLA) also uses (`rl_core.py`
800+
line 71). `BanditLAController.select_mcs`'s `np.clip(mcs, 0,
801+
NRConstants.N_MCS - 1)` (line 451) is a no-op defensive clip, not a range
802+
restriction. **No off-by-one or truncated range** — RLLA can structurally
803+
reach every MCS OLLA/LTSLA can.
804+
805+
**2. Initial value estimates — optimistic, but exactly at the feasibility
806+
cutoff.** Line 219: `self._p_success: Dict[int, float] = {m: 0.9 for m in
807+
_MCS_INDICES}` — every arm (MCS 0 through 28 alike) starts at the *same*
808+
optimistic estimate, 0.9, with the comment (line 218) confirming intent:
809+
"Optimistic init: assume each arm succeeds, encourages exploration." The
810+
critical detail: `target_bler` defaults to `NRConstants.TARGET_BLER` = 0.10
811+
(`constants.py` line 128), so the feasibility test in `select_mcs` (line
812+
242: `if p >= (1.0 - self.target_bler)`) requires `p >= 0.90`**exactly
813+
equal to the optimistic initial value**, not comfortably above it.
814+
815+
**3. Exploration schedule.** `epsilon=0.10`, `epsilon_decay=0.9995`,
816+
`epsilon_min=0.01` (lines 205–207), decayed once per `update()` call (line
817+
259: `self.epsilon = max(self.epsilon_min, self.epsilon * self.epsilon_decay)`).
818+
Solving `0.10 × 0.9995^N = 0.01` gives `N ≈ 4603` updates — at roughly one
819+
scheduled update per TTI per UE, epsilon reaches its floor well inside the
820+
first ~15% of a 30000-TTI run, then stays at the 1% floor (not zero) for
821+
the remainder. Exploration never fully stops, but becomes rare early on.
822+
823+
**4. Reward/update rule — plain unweighted running average, no explicit
824+
asymmetric penalty.** `update()` (lines 253–259): `target = 1.0 if is_ack
825+
else 0.0; self._p_success[mcs] += self.lr * (target - self._p_success[mcs])`
826+
(`lr=0.05`, line 208). This is a plain exponential running average of the
827+
raw ACK/NACK bit — **not** throughput-weighted, and there is no explicit
828+
NACK-penalty coefficient anywhere in this update. Any apparent asymmetry
829+
between "cost of a NACK" and "benefit of a higher MCS" is not written into
830+
the reward function directly — it would have to emerge from the interaction
831+
of items 2+3 with the hard threshold in `select_mcs`, not from a deliberate
832+
reward-shaping choice. This distinguishes the finding from "RLLA's authors
833+
chose a conservative reward on purpose" — nothing in the code sets out to
834+
do that.
835+
836+
**Most likely explanation from code alone, before the empirical check:**
837+
items 2+3 together create a specific, quantifiable failure mode. Starting
838+
at `p=0.9` and drawing one NACK gives `p = 0.9 + 0.05×(0 − 0.9) = 0.855`
839+
**below the 0.90 cutoff after exactly one failure**, regardless of an
840+
arm's true long-run success rate. Once below 0.90, the greedy branch (lines
841+
236–247) can never re-select that arm (it only iterates `p >= 0.90`
842+
arms) — the *only* path back is an epsilon-triggered uniform draw over all
843+
29 arms (line 232, not weighted toward near-threshold arms), each such
844+
event nudging `p` by only `lr × (target − p)`, i.e. slow, and needing
845+
several consecutive lucky successes to climb back above 0.90 from 0.855.
846+
This reads as a **calibration issue** in how three otherwise-reasonable
847+
choices interact (optimistic init exactly at the threshold, a hard
848+
pass/fail cutoff, slow+rare recovery) rather than a coding bug (no
849+
incorrect logic) or a deliberate reward-shaping choice (no throughput/
850+
penalty weighting exists to shape). Confirming this requires seeing actual
851+
selection behavior over time — code reading alone can't distinguish "gets
852+
stuck at an early unlucky estimate and never recovers" from "explores fine
853+
but something else makes low MCS genuinely optimal" — hence Part 2.
854+
855+
### 9.2 Part 2 — empirical trace
856+
857+
**Method.** A throwaway script (not committed; scratchpad only) instantiated
858+
`BanditLAController(method="epsilon_greedy", ...)` and drove it through the
859+
same `_run_rl`-style loop (`CellLayout``ProportionalFairScheduler`
860+
`ctrl.select_mcs`/`ctrl.update`), recording every returned `mcs` in call
861+
order (aggregated across all UEs — an initial attempt to track a single
862+
UE's trajectory found that UE 0 was scheduled rarely under this seed/PF
863+
combination, so tracking switched to all scheduled events instead, which
864+
also better matches "does the bandit population as a whole ever try/
865+
abandon higher MCS"). Bucketed into 1000-event windows. Two configs, one
866+
seed (42): 3 km/h/CDL-A (favorable, §7/§8) and 30 km/h/CDL-A (unfavorable).
867+
No `rl_core.py`/`runner.py` code was touched — only the existing public
868+
`select_mcs`/`update` API was called from the external script.
869+
870+
**Result — both configs, essentially identical pattern:**
871+
872+
| Config | Mean MCS, first 1000 events | Mean MCS, last 1000 events | Realized BLER (this single-seed run) |
873+
|---|---|---|---|
874+
| 3 km/h, CDL-A | 10.74 | 5.63 | 0.0178 |
875+
| 30 km/h, CDL-A | 9.89 | 5.73 | 0.0194 |
876+
877+
(Single-seed values here differ slightly from §8's 5-seed means — 0.0143
878+
at 3 km/h-A, 0.0159 at 30 km/h-A — as expected for one seed vs. a 5-seed
879+
average; same order of magnitude, same conclusion.)
880+
881+
- Both configs start with a mean selected MCS around 10–11 (consistent with
882+
the optimistic-init greedy branch initially favoring high-spec-eff,
883+
as-yet-untried arms) and decline over the run to a **steady state around
884+
5.6–5.9 — nearly identical between 3 km/h and 30 km/h**, not a
885+
monotonic straight-line drop: the 3 km/h trace shows a partial rebound
886+
around events 6000–10000 (mean rising back to ~8.5) before settling down
887+
again by the end. This is consistent with a stochastic
888+
equilibrium-seeking process (arms near the 0.90 cutoff randomly crossing
889+
above and below it), not a one-shot collapse.
890+
- **MCS 28 (the ceiling) is drawn in nearly every 1000-event bucket, all
891+
the way to the final bucket of the run, in both configs** — the bandit
892+
never permanently loses access to the top of the range; the epsilon
893+
floor (1%, uniform over 29 arms, ≈0.03% per event) keeps sampling it
894+
throughout, exactly as item 3 predicts. This is why "highest MCS ever
895+
tried" and "max in final 1000 events" are both 28 in every trace — a
896+
low-rate noise artifact of `min`/`max`, not evidence the ceiling is
897+
reachable *and stable*: the **mean** is the signal that matters, and it
898+
settles low regardless.
899+
- **The key discriminating result: 3 km/h and 30 km/h converge to nearly
900+
the same final mean MCS (5.63 vs. 5.73)**, despite very different fading
901+
dynamics between the two configs. If the mechanism were Doppler/speed-
902+
tracking-related (the way LTSLA's §8 finding is), the two configs would
903+
be expected to diverge, not converge to nearly the same value. They
904+
don't — this is direct evidence against a speed-dependent explanation
905+
and points at something intrinsic to the bandit's own estimation
906+
dynamics, common to both configs.
907+
908+
### 9.3 Part 3 — conclusion
909+
910+
**The evidence (code + empirical trace) converges on a single, specific
911+
mechanism, not an unresolved choice between candidates:**
912+
913+
`EpsilonGreedyRLLA`'s optimistic initial value (0.9) is set **exactly
914+
equal to** its own feasibility threshold (`1 − target_bler` = 0.90, since
915+
`target_bler` defaults to the same 10% used everywhere else in this
916+
project). Because of this exact equality, a single NACK on any untried arm
917+
— including an arm whose *true* long-run success rate is close to the
918+
target's own 90% — immediately drops that arm's estimate to 0.855, below
919+
the cutoff, disqualifying it from ever being greedily re-selected until a
920+
rare (≈0.03%-per-event, uniform-over-29-arms) exploration event re-tries
921+
it, which then needs *sustained* luck (several consecutive successes
922+
against the slow `lr=0.05` running average) to climb back above 0.90.
923+
Arms whose true success rate sits *near* 90% are the least likely to
924+
survive this process, because they fail roughly 1 time in 10 by
925+
definition — almost exactly as often as it takes to trigger the
926+
disqualification — while arms with a true success rate *safely above* 90%
927+
(i.e., meaningfully more conservative than the 10% BLER target) are the
928+
only ones stable enough to survive an initial unlucky run and remain the
929+
long-run greedy choice. This produces a systematic downward bias toward
930+
BLER well under target, **independent of channel speed** — confirmed
931+
empirically by the near-identical steady-state mean MCS at 3 km/h and
932+
30 km/h (§9.2), which specifically rules out a Doppler/speed-tracking
933+
explanation (the mechanism behind LTSLA's §8 finding) as the cause here.
934+
935+
This is a **design/calibration issue**, not a coding bug (no incorrect
936+
logic, no off-by-one, confirmed in §9.1 item 1) and not a deliberate
937+
reward-shaping choice (the update rule is an unweighted ACK/NACK running
938+
average with no asymmetric penalty term, confirmed in §9.1 item 4) —
939+
specifically, the calibration issue is the coincidence of the optimistic
940+
initial value and the feasibility threshold being set to the *same*
941+
number, combined with a hard binary cutoff and a slow, rare recovery path.
942+
943+
**What this does not establish**: this conclusion rests on code analysis
944+
plus a single-seed, two-config empirical trace (per this task's own scope
945+
— not a fresh 5-seed/6-config campaign). The qualitative mechanism (the
946+
arithmetic of `p0=0.9`, `threshold=0.90`, `lr=0.05` interacting with a hard
947+
cutoff) does not depend on seed or config, so there is no specific reason
948+
to expect other seeds/configs to tell a different story — but this was not
949+
verified beyond the two configs and one seed traced here. No fix was
950+
attempted or evaluated (out of scope per the task's own STOP condition);
951+
this section identifies the mechanism, it does not remediate it.
952+
953+
### 9.4 Recommendation for a follow-up task (not done here)
954+
955+
`README.md` and `docs/index.html` (commits `c012148` and the prior,
956+
still-uncommitted `docs/index.html` edit) currently describe this as
957+
"likely a design/calibration issue in `EpsilonGreedyRLLA`'s MCS selection"
958+
— deliberately unspecific at the time. Given §9.3's conclusion is now
959+
specific and code-cited, a follow-up documentation task could tighten that
960+
wording to name the actual mechanism (optimistic-init-equals-threshold
961+
plus hard-cutoff-with-slow-recovery). Not done as part of this task, which
962+
was scoped to identification only, and per the task's own instruction not
963+
to edit `README.md`/`docs/index.html` here.
964+
965+
### 9.5 Provenance for this section
966+
967+
| Artifact | Location |
968+
|---|---|
969+
| Code read this session | `projects/p6_rl_link_adapt/rl_core.py` lines 61, 71, 178–264, 451; `nr_simulator/constants.py` lines 111, 128 |
970+
| Empirical trace script | Throwaway, scratchpad-only, not committed — calls only `BanditLAController`'s existing public API |
971+
| Confirmation no algorithm file was left modified | `git diff --stat projects/p6_rl_link_adapt/rl_core.py projects/p6_rl_link_adapt/runner.py` → empty, this session |
972+
| HEAD at start of this section | `81c76bb` / `5e40240` / `c012148` (no new commits from this section's own work — documentation-only append) |
973+
| This section | Drafted 2026-07-18, same session as the code review and trace |

0 commit comments

Comments
 (0)