Skip to content

[RFC]: Adaptive early exit on Ouro-1.4B: accuracy–throughput evaluation and exact-policy async exit #56

Open

Description

@MinhaoLi0318

Motivation.

Adaptive early exit is the main reason to run a looped model through vllm-rlt, but its throughput and its accuracy have been measured separately, under different settings. Baseline: main @ 3314c1b3.

Throughput is measured; accuracy at those settings is not.

  • PR [feat]Implement depth-aware KV and asynchronous decode scheduling #30 measures early exit at exit_threshold=0.2 with min_loops=2 and max_loops=4. At the highest concurrency tested, E2E throughput improves by about +11.9% / +15.9% / +18.7% / +14.2% (P2/P1) for 1K / 4K / 16K / 64K contexts, with about 2.4–2.5 mean decode rounds.
  • The same PR notes that the delayed variant (P3, required by async scheduling) runs about 3.3–3.4 mean rounds, so the extra round "still consumes a substantial part of the original early-exit benefit". It also says that "policy-changing comparisons do not establish equal model quality".
  • The only task-accuracy result covers fixed four-loop greedy decoding: GSM8K 62.02% native vs. 61.64% Transformers on all 1,319 test questions (docs/accuracy.md). The harness enforces this: benchmarks/gsm8k_backends.py raises if any exit depth is not 4. docs/cdb_runtime.md states that no task-accuracy result is claimed for ouro_delayed.

So we cannot yet say how much accuracy the measured speedups cost, or which threshold is a reasonable default for users.

Output differences under the same exit policy are still unresolved. In the PR #30 campaign, 196 of 612 trials differed from their own policy reference (182 in tokens, 168 in exit depths). docs/cdb_runtime.md also records a BF16 multi-stream token divergence whose root cause is unconfirmed: it disappeared when diagnostic logit copies were added. The PR itself lists investigating same-policy differences as a step to take before expanding the matrix. Until that is resolved, accuracy differences under async execution cannot be cleanly attributed to the exit policy.

Why this is separable. The released Transformers model already supports exit_threshold. It runs all four loops, keeps full-depth KV for every token, and only selects the exited loop's hidden state (modeling_ouro.py @ 574fa66c). The native engine really skips loops and uses last_exited KV. Comparing the two at the same threshold therefore isolates the effect of the KV approximation from the effect of the early-exit decision itself.

Proposed Change.

The work has three phases. Phases 1–2 measure and diagnose, and leave runtime behavior, exit-policy defaults and public APIs unchanged. Phase 3 is a runtime change that is proposed now but only implemented if the phase-1 data supports it.

Phase 1: accuracy–compute trade-off (GSM8K, BF16, greedy)

Arms. Each step changes exactly one factor:

Arm Backend Exit policy KV Isolates
A0 HF fixed 4 loops full depth reference (existing baseline)
A1 HF exit_threshold=τ full depth early-exit decision alone
B1 native sync ouro, τ last_exited + KV approximation (B1 vs A1)
B2 native sync ouro_delayed, τ last_exited + one-round delay (B2 vs B1)
B3 native async ouro_delayed, τ last_exited + async scheduling (B3 vs B2, expected to be 0)
  • Thresholds: τ ∈ {1.0 (control), 0.9, 0.7, 0.5, 0.2}. 0.2 is the PR [feat]Implement depth-aware KV and asynchronous decode scheduling #30 operating point.
  • min_loops=1 in the parity arms, because HF has no minimum. The project default, min_loops=2, is reported as an extra native row.
  • Dataset and prompts: the existing pinned protocol (lm-eval gsm8k_cot, 3-shot, strict match, 1,024 new tokens, 2,048 context). First screen every arm on the fixed GSM8K-87 case, then run the full 1,319-question test split for the thresholds that matter.
  • Metrics per arm:
    • strict accuracy, plus paired gains and losses against A0 on the same questions;
    • unparseable-answer and length-termination counts;
    • mean exit depth and depth histogram;
    • generated tokens per question;
    • decode loops per question, the sum of per-token depths. This is a hardware-independent compute measure; it also catches the case where early exit makes answers longer and cancels out the per-token savings.
  • Measured throughput: questions/s and output tokens/s at concurrency 1 / 8 / 32 on the same GPU, for B1–B3 against the τ=1.0 control, interleaved in shuffled order with three repetitions (min–max reported). Absolute numbers are not comparable with PR [feat]Implement depth-aware KV and asynchronous decode scheduling #30 (different GPU and backend); only within-run ratios are reported.

Deliverables:

  1. PR: extend benchmarks/gsm8k with exit controls: exit mode, threshold, min/max loops, and async scheduling. These go into the protocol fingerprint so baselines cannot be mixed. The PR also adds per-token exit-depth recording, the HF exit_threshold arm, and depth/compute statistics plus paired statistics in compare. Validated on CPU with the tiny model.
  2. Report (docs/benchmarks/…): the accuracy vs. decode-loops Pareto curve per arm, with pinned SHAs, environment, raw outputs and checksums, following the existing accuracy-doc conventions. It will propose, not decide, a default threshold.

Phase 2: same-policy output differences

  • Reproduce the BF16 multi-stream divergence on the real checkpoint with a fixed prompt set.
  • At the first divergent position, record the reference top-1/top-2 logit margin:
    • near-ties point to reduced-precision or schedule-dependent numerics;
    • large margins point to a state, ordering or stream-dependency problem.
  • Bisect across the P4–P8 features (async single-stream → multi-stream → static buffers → padding → graphs), holding the exit policy fixed.
  • Deliverable: a diagnosis report. If a real ordering bug is found, a fix PR with a regression test, kept separate from any structural change.

Phase 3 (data-gated): exact-policy exit under async scheduling

Problem. Async scheduling currently requires ouro_delayed. By the time round k's gate score reaches the host, round k+1 has already been submitted, so every exiting token runs one extra round. Its output also follows a different policy from ouro. This is the P2→P3 confound in PR #30: an async-vs-sync comparison also changes model semantics.

Candidate design, an opt-in exit mode (working name ouro_exact) that keeps the async pipeline but restores the original policy's outputs:

  1. After each recurrent round, snapshot the per-request hidden state on the device, into one extra [max_num_seqs, hidden] pool indexed by the existing async request slot. No host transfer is involved.
  2. When round k's signal triggers an exit, run coda from the round-k snapshot instead of the round-(k+1) hidden state, and record exit depth k.
  3. Finalize KV from depth k. finalize_token already copies the exit depth's K/V into every deeper plane, so the K/V that round k+1 wrote at this position is overwritten, and later tokens see exactly the last_exited state that sync ouro would have produced.
  4. The extra round is still executed, so compute matches ouro_delayed. The gain is semantic: outputs and depths should match sync ouro, so async can be compared with sync without a policy confound, and it inherits ouro's measured accuracy.

When to implement:

  • Go ahead if phase 1 shows a material B2-vs-B1 difference in accuracy or outputs.
  • If the difference is negligible, report that and drop or deprioritize this phase.

Open points to settle in design review: where the snapshot lives under CUDA Graph capture; the finalize_many async path; padding rows; and preemption snapshots, which would need to include the extra hidden state.

Acceptance:

  • Token and depth agreement with sync ouro on the phase-1 prompt set, under FP32 on the tiny model and the real checkpoint, with BF16 differences reported against the phase-2 findings.
  • GSM8K accuracy matching arm B1 within the agreed tolerance.
  • Throughput and peak memory against ouro_delayed async under matched conditions.
  • CPU and GPU regression tests for exit, EOS, abort and preemption.

This touches exit handling that M3 (#32) is moving from the engine into the scheduler. I would implement it against whatever interface M3 settles on, coordinating with @bjf-frz on ordering. The implementation would not be written in parallel with that migration.

Non-goals. No trained lookahead gate, no paper reproduction, and no change to the existing ouro / ouro_delayed semantics or defaults (phase 3 adds an opt-in mode). No overlap with the speculative-decoding evaluation in #43; the per-token depth and acceptance data may still be useful to the adaptive-speculation work there. A fully device-side exit decision is out of scope and relates to #43's device-side execution loop.

Hardware. Phase 1 uses the Triton backend on a rented single GPU (L40S or H100 class). The exact GPU, driver, package versions and CPU affinity are recorded, and cases that were not run are marked as such. FA4 arms can be added if maintainers want them for comparability with PR #30.

Feedback Period.

One week. I will start the CPU-only harness PR (deliverable 1) in the meantime, since it doesn't depend on the answers below.

Questions for maintainers:

  1. Is τ=0.2 with min_loops=2 the intended operating point, or should the sweep center elsewhere?
  2. Is the full 1,319 split acceptable as the headline result, with GSM8K-87 as the screen?
  3. Is Triton on H100/L40S acceptable for the accuracy arms, or do you need FA4 for comparability?
  4. Should phase 2 be tracked here or in a separate issue?
  5. For phase 3: is an opt-in exit mode the right shape, or would you prefer this as the default async behavior for ouro? And should it wait for the M3 exit-policy migration, or land first behind the current engine interface?

CC List.

@bjf-frz @hsliuustc0106

Any Other Things.

I'm happy to own this end to end. If someone is already looking at same-policy differences or adaptive-exit accuracy, I'll coordinate rather than duplicate.

Activity

  1. bjf-frz commented on Sep 24, 2026

    @bjf-frz
    Collaborator

    ouro_delayed is only a workaround for testing async scheduling, since we do not have weights trained for a true delayed-exit policy. Its accuracy is therefore expected to be unreliable and does not need to be evaluated as a meaningful model-quality baseline.

    The other evaluations are still useful, especially scheduling correctness, sync/async consistency, exit depth, compute, and throughput.

  2. MinhaoLi0318 commented on Sep 25, 2026

    @MinhaoLi0318
    ContributorAuthor

    Thanks @bjf-frz, that makes sense. I'll treat ouro_delayed as a scheduling-test policy, not a quality baseline. Revised phase 1:

    Accuracy (GSM8K): only the fixed-depth HF reference, HF with exit_threshold, and native sync ouro, across thresholds. ouro_delayed gets no accuracy claim.
    ouro_delayed, sync vs. async: token and exit-depth agreement on the same prompts, recording the first divergent position and the logit margin there whenever the two differ. Also exit-depth distribution, decode loops per token and per question, and throughput. This is also where phase 2 starts.

    This changes the case for phase 3. Async scheduling currently accepts only ouro_delayed, random_lookahead or trace, so no async exit policy uses the trained gate with its original semantics. ouro_exact would fill that gap: the trained gate's ouro decision under async scheduling, with outputs expected to match sync ouro. The extra round would still run, so compute stays at the ouro_delayed level. My original trigger for phase 3 (an accuracy gap between ouro_delayed and ouro) no longer applies. Would you be open to this as the phase-3 target on its own merits?

    Two questions before I rent a GPU:

    Is τ=0.2 with min_loops=2 the operating point you care about, or should I sweep {0.9, 0.7, 0.5, 0.2}?
    Is Triton on an L40S/H100 acceptable, or do you want FA4 for comparability with PR #30?

    The harness support for both parts is in #58 (validated on CPU).

  3. bjf-frz commented on Sep 25, 2026

    @bjf-frz
    Collaborator
    1. τ=0.2, min_loops=2 is fine as the primary operating point. Keep the sweep for reference.
    2. GSM8K-87 for screening and the full 1,319 split for final validation sounds good.
    3. No need to stick to FA4. Feel free to use or adapt the attention backend that best fits the available GPU; just keep it consistent within each comparison.
    4. Please track Phase 2 in a separate issue. The BF16/multi-stream divergence is a standalone correctness/debugging problem.
    5. Keep ouro_exact opt-in and data-gated. Only proceed if Phase 1 shows a meaningful B1-vs-B2 difference, and implement it after M3 settles the exit-policy interface.
  4. MinhaoLi0318 commented on Oct 7, 2026

    @MinhaoLi0318
    ContributorAuthor

    @bjf-frz thanks, all five points are adopted. Status:

    • Harness: the exit controls landed in bench: add adaptive-exit options and depth statistics to the GSM8K ha… #58 (including the prefill-depth fix in the HF release path and loop depth read from config.json), so the comparison runs on main.
    • Phase 1 (not run yet, my delay): τ=0.2 / min_loops=2 as the primary point with the sweep for reference, GSM8K-87 screening and the full 1,319 split for the final numbers, one attention backend (Triton) across all arms. I'll run it on one A100 and post the accuracy, exit-depth, compute and throughput tables here by 10-25.
    • Phase 2: I'll open the BF16/multi-stream divergence as a separate issue together with those runs, so it starts with concrete reproductions.
    • ouro_exact: stays opt-in and data-gated on a meaningful B1-vs-B2 difference, after M3 settles the exit-policy interface. No work on it until then.

    Meanwhile my vllm-rlt time goes to the #70 Phase 1 rollout outputs (#83).

    AI assistance: I used Claude Code to check the thread and the harness state and to draft this update; I reviewed it before posting.

  5. MinhaoLi0318 commented on Oct 22, 2026

    @MinhaoLi0318
    ContributorAuthor

    @bjf-frz Before the Phase 1 run (one A100, 10-24), two things fixed in advance:

    1. Commit: main at 09f229a, the bench: add adaptive-exit options and depth statistics to the GSM8K ha… #58 merge. That predates [Architecture] Unify stage execution with loop-level scheduling and mixed batching #118, so the native numbers describe the pre-[Architecture] Unify stage execution with loop-level scheduling and mixed batching #118 engine; I'm not claiming they carry over to current main.
    2. The ouro_exact trigger, fixed before the full run: on the full 1,319 split, B1 (sync ouro, τ=0.2, min_loops=2) vs B2 (sync ouro_delayed, same τ and min_loops) counts as a meaningful difference if the exact two-sided McNemar test on the discordant pairs gives p < 0.05 and |Δ| ≥ 1 pp. GSM8K-87 is a screen only. I'll report the sign of Δ and decode loops per question for both arms.

    The per-token logit margins I mentioned earlier move to the Phase 2 issue, which I'll open with this run's outputs. Tables here by 10-25.

    AI assistance: Claude Code helped check the harness and draft this comment; I reviewed it before posting.

  6. MinhaoLi0318 commented on Oct 25, 2026

    @MinhaoLi0318
    ContributorAuthor

    Phase 1 results: the accuracy, exit-depth, compute and throughput tables promised on 10-07.

    Setup. vllm-rlt 09f229a (the #58 merge, pre-#118 engine), ByteDance/Ouro-1.4B @ 574fa66c, 1× A100-SXM4-80GB, torch 2.13.0+cu130, Triton 3.7.1, BF16, greedy. Native arms use the Triton attention backend, HF arms the release model (eager). Pinned gsm8k_cot protocol (3-shot, strict match); B0 passed the stored GSM8K-87 gate (60/87 vs 59/87). Depth and loops count decode tokens only.

    Full 1,319 split

    A0 / B0: HF / native, fixed 4 loops. B1m2_0.2: native sync ouro, τ=0.2, min_loops=2 (primary point). Dsync_0.2 (B2): native sync ouro_delayed, same τ and min_loops; its accuracy is used only in the rule below, not as a quality baseline.

    arm acc % (correct) Δ pp [95% CI] lost/gained McNemar p mean depth depth 2/3/4 % loops/q (× B0) tokens/q unparseable len-limited
    A0 62.62 (826) – – – – – 438.2† 110.5 297 5
    B0 62.77 (828) +0.15 [−0.9, +1.2] vs A0 26/28 0.892 4.00 0 / 0 / 100 435.8 (1.000) 109.9 289 3
    B1m2_0.2 55.42 (731) −7.35 [−9.5, −5.3] vs B0 152/55 1.05e-11 2.19 81.6 / 17.3 / 1.1 241.5 (0.554) 111.1 334 8
    Dsync_0.2 (B2) 61.18 (807) – – – 3.19 0 / 81.3 / 18.7 346.8 (0.796) 109.8 306 3

    † The HF release computes all 4 loops per token. Unparseable: no strict-match answer (e.g. The answer is $18.).

    At the primary point, early exit runs 44.6% fewer decode loops per question than fixed depth, with answers about as long (111.1 vs 109.9 tokens/q), and costs 7.35 pp. B2's extra round leaves a 20.4% saving.

    ouro_exact rule (pre-registered 10-22): met

    B2 − B1 = +5.76 pp [95% CI +3.7, +7.8], lost/gained 56/132, exact two-sided McNemar p = 2.95e-08 (needed: p < 0.05 and |Δ| ≥ 1 pp). Loops/q 241.5 (B1) vs 346.8 (B2), +0.99 loops/token on shared prefixes; outputs differ on 1,076 of 1,319 questions.

    This is not a quality claim for ouro_delayed. It shows that ouro_delayed, which async scheduling requires today, changes what the trained gate produces. As designed, ouro_exact would restore B1's outputs (55.42% here) while still running the extra round, so its case is sync/async equivalence, not accuracy.

    Throughput (Run 1, matched work)

    Requests cycle through the GSM8K-87 prompts, and each decodes exactly screen B0's natural length for its prompt (ignore_eos), so every policy does the same token work. 16/32/128 requests at C=1/8/32, fresh engine per cell, 3 reps with the cell order shuffled; median output tok/s (× fixed; req/s ratios are identical). Every cell's 3-rep spread is within 5% of its median. Policies with τ<1 use min_loops=2.

    policy depth C=1 C=8 C=32
    fixed (sync) 4 10.6 (1.00×) 60.9 (1.00×) 205.9 (1.00×)
    fixed_async 4 10.5 (0.997×) 60.4 (0.992×) 204.9 (0.995×)
    ouro τ=0.5 3.07–3.09 13.5 (1.277×) 75.9 (1.246×) 251.9 (1.223×)
    ouro τ=0.2 2.23–2.27 18.6 (1.757×) 101.1 (1.659×) 304.4 (1.478×)
    delayed τ=0.2 sync 3.18–3.20 13.1 (1.240×) 74.5 (1.222×) 241.4 (1.172×)
    delayed τ=0.2 async 3.18–3.20 13.0 (1.233×) 73.9 (1.213×) 240.5 (1.168×)
    delayed τ=0.2 async, 1 stream 3.18–3.20 13.2 (1.251×) 74.6 (1.224×) 244.8 (1.189×)

    Gains at C=1 / 8 / 32 (median ratios):

    • async at full depth (fixed_async / fixed): 0.997 / 0.992 / 0.995
    • async at τ=0.2 (delayed async / sync): 0.995 / 0.992 / 0.996
    • multi-stream overlap at τ=0.2 (async / async 1-stream): 0.986 / 0.991 / 0.982

    The speedup comes from depth alone; async and multi-stream overlap add nothing on this setup. ouro τ=0.2 gets 1.757× at C=1 (depth ratio 4/2.27 = 1.76×) and 1.478× at C=32 (depth ratio 1.79×).

    Consistency: 0 divergent requests at every C for delayed τ=0.2 sync vs async (multi- and single-stream), fixed vs fixed_async, and rep 0 vs rep 2 of every policy; in the harness, Dsync and Dasync match on 87/87 at τ=0.5 and 0.2. Divergence appears only where batch composition changes (the BF16 batch-variance baseline): under fixed, C=1 vs C=8 9/48 requests, C=1 vs C=32 18/48, C=8 vs C=32 27/96, repeated prompts within a C=32 cell 36/123.

    GSM8K-87 screen (informational)

    • min_loops matters only at τ=0.2 (min 1 and min 2 give identical tokens and depths on 87/87 at τ ≥ 0.5). There, min_loops=1 loses 16.09 pp vs B0 (p = 0.00936) and answers grow to 129.9 tokens/q (B0: 103.8).
    • HF vs native under the same rule (min_loops=1): within 2.3 pp at τ ≥ 0.5. At τ=0.2, B1 − A1 = −14.94 pp [−26.7, −3.2], p = 0.0241, at the same mean depth (1.77). The difference-in-differences against the fixed floor, −14.94 pp [−27.2, −2.7], does not isolate the last_exited KV effect: it also includes BF16-vs-FP32 exit arithmetic and eager-vs-Triton kernels.
    All 18 arms
    arm acc % (n/87) Δ pp vs ref McNemar p mean depth loops/q tokens/q
    A0 68.97 (60) – – – 412.4† 104.1
    A1_0.9 68.97 (60) +0.00 1 4.00† 412.4† 104.1
    A1_0.7 68.97 (60) +0.00 1 3.49† 412.4† 104.1
    A1_0.5 67.82 (59) −1.15 1 3.04† 394.4† 99.6
    A1_0.2 67.82 (59) −1.15 1 1.77† 397.4† 100.4
    B0 68.97 (60) +0.00 (vs A0) 1 4.00 411.1 103.8
    B1_0.9 = B1m2_0.9 68.97 (60) +0.00 1 4.00 411.1 103.8
    B1_0.7 = B1m2_0.7 66.67 (58) −2.30 0.688 3.49 354.2 102.6
    B1_0.5 = B1m2_0.5 67.82 (59) −1.15 1 3.03 335.0 111.6
    B1_0.2 (min_loops=1) 52.87 (46) −16.09 0.00936 1.77 227.5 129.9
    B1m2_0.2 59.77 (52) −9.20 0.0574 2.18 235.1 109.0
    Dsync_0.5 = Dasync_0.5 72.41 (63) – – 3.97 403.0 102.4
    Dsync_0.2 = Dasync_0.2 68.97 (60) – – 3.18 330.6 104.9

    Δ: A1 vs A0, B1/B1m2 vs B0. "=": identical outputs on 87/87. † HF: selected exit loop; all 4 loops still computed. B2 − B1: τ=0.5 +4.60 pp (p 0.344), τ=0.2 +9.20 pp (p 0.0574).

    Deviations

    • Not run: throughput Run 2 (fixed 256 tokens) and full-set A1_0.2 / B1_0.2. After the screen I applied the run plan's pre-set cut order to stay inside the rental time cap, so the min_loops=1 evidence, including the HF-vs-native gap above, is screen-only.
    • Pairing: the issue body said "against A0"; Δ here is against the same backend's fixed arm, with B0 − A0 reported as the HF-vs-native floor. B1m2_0.2 vs A0 is −7.20 pp.

    Not claimed: carry-over to current main (these are pre-#118 numbers), and logit margins, which go to the Phase 2 issue I'll open next with this run's per-request outputs and batch logs.

    @bjf-frz The pre-registered rule is met, with B2 above B1. Should ouro_exact go ahead as the opt-in async mode once M3 settles the exit-policy interface, or, since it would return async to B1's outputs at about B2's compute, would you rather deprioritize Phase 3?

    AI assistance: I used Claude Code to drive the pod run, recompute the statistics from the per-question samples and draft this comment; I checked the numbers before posting.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions