Skip to content

Commit 01a4e4d

Browse files
committed
Average planned verification depth across request rounds
Count allocated draft prefixes from validated completions and divide by progressing decode or verification request rounds. Include rejected drafts and zero-draft rounds while excluding padding, prefill, failed lanes, and terminal rounds without progress. Shared accounting covers combined, split, packed, and mixed speculation, including copy drafts. Expose the counters through JSON and Prometheus. The verification_drafts JSON field now measures per-request allocation instead of graph maximum width; verification_rounds supplies its denominator. Prepared packages and execution policy are unchanged. Validation: 872 Helios library tests passed with 22 ignored, including packed allocations [5, 1, 0] reporting depth 2.0 with zero acceptance, ordinary decode, and combined MTP/copy accounting. Flare release build and Heliopause installation passed. Full-model GPU validation was not run; the running service has not been restarted. Design-Signed-Off-By: Codex (GPT-6); design-review=6aeb
1 parent 152704b commit 01a4e4d

6 files changed

Lines changed: 141 additions & 15 deletions

File tree

‎docs/api.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,8 @@ instantaneous gauges.
220220
| `decode_weight_bytes` | Bytes of weights one decode step reads for one request, as the package's recipe declares for its checkpoint; zero when undeclared. Times verification steps per second it is the weight bandwidth the model consumes, shown as WEIGHTS on the dashboards | `flare_decode_weight_bytes` |
221221
| `draft_steps` | Draft positions run by the learned drafter; zero without one | none |
222222
| `verify_steps` | Completed speculative or verification graphs | sum of `flare_speculative_rounds_total` |
223-
| `verification_drafts` | Draft positions verified: verification width minus one per graph | sum of `flare_speculative_rounds_total` × (`width` − 1) |
223+
| `verification_drafts` | Draft positions allocated for progressing request rounds, including rejected drafts and excluding padding | `flare_verification_draft_tokens_total` |
224+
| `verification_rounds` | Progressing decode or verification request rounds, including zero-draft rounds | `flare_verification_request_rounds_total` |
224225
| `accepted_drafts` | Accepted draft tokens, excluding the bonus token | `flare_accepted_draft_tokens_total` |
225226
| `draft_rounds` | Request rounds that offered at least one draft | `flare_draft_request_rounds_total` |
226227
| `active` | Model slots with a live request | `flare_active_requests` |
@@ -243,6 +244,13 @@ instantaneous gauges.
243244
| `host_bytes_in` | Bytes copied from the host store into GPU pages and rows | `flare_host_cache_bytes_in_total` |
244245
| `learned_drafter` | Boolean: the model has a learned drafter | none |
245246

247+
Dashboard DEPTH is the change in `verification_drafts` divided by the change in
248+
`verification_rounds` over the sampling window. It averages each request's planned
249+
draft-prefix length, including zero-draft rounds, rather than graph width or batch
250+
capacity. For packed speculation, completion records report the prefixes allocated
251+
on the GPU before target verification. Prefill, resume sampling, inactive padding,
252+
failed lanes, and terminal rounds without progress do not enter this average.
253+
246254
## Conversation and tool semantics
247255

248256
The neutral request representation in `helios/src/server/inference.rs` separates

‎helios/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ forwards inside completed speculative graphs; verification steps count completed
6363
speculative graphs. Copy-only proposals contribute no drafter steps. Mean draft
6464
acceptance length is accepted draft tokens per progressing request round with
6565
at least one offered draft, excluding the bonus token and width-one rounds.
66+
DEPTH is allocated draft positions per progressing decode or verification request
67+
round, including zero-draft rounds. Packed rounds contribute their allocated
68+
per-request prefixes; the graph's maximum width and padded batch size do not
69+
determine this average. Rejected drafts count toward depth.
6670
The display also shows active/queued requests, completed/failed requests, reused
6771
tokens, and cached/free KV pages. Token counters are available through `/metrics`.
6872

‎helios/src/server/dashboard.rs‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ pub struct Rates {
334334
pub verify: f64,
335335
/// Accepted drafts per draft-offering request round; `None` without such rounds.
336336
pub acceptance: Option<f64>,
337-
/// Verified draft positions per verification graph; `None` without verification.
337+
/// Allocated draft positions per progressing decode or verification request round.
338338
pub depth: Option<f64>,
339339
/// Weight bytes read per second: verification graphs per second times the bytes one decode
340340
/// step reads, as the package declares; zero when it declares none.
@@ -377,7 +377,7 @@ impl History {
377377
}
378378
};
379379
let rounds = snapshot.draft_rounds.saturating_sub(before.draft_rounds);
380-
let verifications = snapshot.verify_steps.saturating_sub(before.verify_steps);
380+
let verifications = snapshot.verification_rounds.saturating_sub(before.verification_rounds);
381381
self.rates = Rates {
382382
prefill: per_second(snapshot.prefill_tokens, before.prefill_tokens),
383383
generated: per_second(snapshot.generated_tokens, before.generated_tokens),
@@ -761,7 +761,8 @@ mod tests {
761761
generated_tokens: 8,
762762
draft_steps: 7,
763763
verify_steps: 1,
764-
verification_drafts: 5,
764+
verification_drafts: 7,
765+
verification_rounds: 3,
765766
accepted_drafts: 6,
766767
draft_rounds: 2,
767768
..Default::default()
@@ -772,15 +773,16 @@ mod tests {
772773
generated_tokens: 10,
773774
draft_steps: 8,
774775
verify_steps: 2,
775-
verification_drafts: 8,
776+
verification_drafts: 10,
777+
verification_rounds: 5,
776778
accepted_drafts: 7,
777779
draft_rounds: 3,
778780
..Default::default()
779781
};
780782
history.sample(start + Duration::from_secs(2), last);
781783
assert_eq!(history.rates.prefill, 150.0);
782784
assert_eq!(history.rates.generated, 5.0);
783-
assert_eq!(history.rates.depth, Some(4.0));
785+
assert_eq!(history.rates.depth, Some(2.0));
784786
assert_eq!(history.rates.verify, 1.0);
785787
assert_eq!(history.rates.acceptance, Some(7.0 / 3.0));
786788
for tick in 3..=130 {
@@ -795,6 +797,7 @@ mod tests {
795797
start + Duration::from_secs(131),
796798
Snapshot {
797799
verify_steps: last.verify_steps + 1,
800+
verification_rounds: last.verification_rounds + 2,
798801
..last
799802
},
800803
);
@@ -822,6 +825,7 @@ mod tests {
822825
draft_steps: tick * 12,
823826
verify_steps: tick * 3,
824827
verification_drafts: tick * 12,
828+
verification_rounds: tick * 6,
825829
accepted_drafts: tick * 7,
826830
draft_rounds: tick * 3,
827831
..cache

‎helios/src/server/metrics.rs‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,10 @@ pub struct Snapshot {
7676
pub draft_steps: u64,
7777
/// Completed speculative or verification graphs, including terminal no-op rounds.
7878
pub verify_steps: u64,
79-
/// Draft positions those graphs verified: verification width minus one per graph.
79+
/// Draft positions allocated for progressing request rounds, including rejected drafts.
8080
pub verification_drafts: u64,
81+
/// Progressing decode or verification request rounds, including zero-draft rounds.
82+
pub verification_rounds: u64,
8183
/// Accepted draft tokens from progressing request rounds, excluding the bonus token.
8284
pub accepted_drafts: u64,
8385
/// Progressing request rounds that offered at least one draft.
@@ -203,6 +205,8 @@ pub(super) struct Metrics {
203205
generated_tokens: AtomicU64,
204206
accepted_drafts: AtomicU64,
205207
draft_rounds: AtomicU64,
208+
verification_drafts: AtomicU64,
209+
verification_rounds: AtomicU64,
206210
pub admitted: AtomicU64,
207211
pub http_requests: [AtomicU64; 5],
208212
pub completed: AtomicU64,
@@ -268,12 +272,15 @@ impl Metrics {
268272
}
269273
}
270274

271-
pub fn progress(&self, prefill: u64, generated: usize, accepted: u64, rounds: u64) {
275+
pub fn progress(&self, prefill: u64, generated: usize, accepted: u64, rounds: u64,
276+
verification_drafts: u64, verification_rounds: u64) {
272277
for (counter, value) in [
273278
(&self.prefill_tokens, prefill),
274279
(&self.generated_tokens, generated as u64),
275280
(&self.accepted_drafts, accepted),
276281
(&self.draft_rounds, rounds),
282+
(&self.verification_drafts, verification_drafts),
283+
(&self.verification_rounds, verification_rounds),
277284
] {
278285
if value != 0 {
279286
counter.fetch_add(value, Ordering::Relaxed);
@@ -288,6 +295,8 @@ impl Metrics {
288295
generated_tokens: self.generated_tokens.load(Ordering::Relaxed),
289296
accepted_drafts: self.accepted_drafts.load(Ordering::Relaxed),
290297
draft_rounds: self.draft_rounds.load(Ordering::Relaxed),
298+
verification_drafts: self.verification_drafts.load(Ordering::Relaxed),
299+
verification_rounds: self.verification_rounds.load(Ordering::Relaxed),
291300
active: self.active_requests.load(Ordering::Relaxed),
292301
pending: self.pending.load(Ordering::Relaxed),
293302
completed: self.completed.load(Ordering::Relaxed),
@@ -310,10 +319,9 @@ impl Metrics {
310319
..Snapshot::default()
311320
};
312321
for widths in &self.speculative {
313-
for (drafts, counts) in widths.iter().enumerate() {
322+
for counts in widths {
314323
let rounds = counts.rounds.load(Ordering::Relaxed);
315324
snapshot.verify_steps += rounds;
316-
snapshot.verification_drafts += rounds * drafts as u64;
317325
}
318326
}
319327
snapshot
@@ -625,6 +633,16 @@ impl Metrics {
625633
"Progressing live request rounds offering at least one draft; denominator for mean acceptance length.",
626634
&self.draft_rounds,
627635
),
636+
(
637+
"verification_draft_tokens_total",
638+
"Draft positions allocated for progressing live request rounds, including rejected drafts and excluding padding.",
639+
&self.verification_drafts,
640+
),
641+
(
642+
"verification_request_rounds_total",
643+
"Progressing live decode or verification request rounds, including zero-draft rounds; denominator for mean planned verification depth.",
644+
&self.verification_rounds,
645+
),
628646
(
629647
"requests_admitted_total",
630648
"Requests admitted into a physical model slot.",
@@ -998,7 +1016,6 @@ mod tests {
9981016
for learned in [false, true] {
9991017
let snapshot = metrics.snapshot(learned);
10001018
assert_eq!(snapshot.verify_steps, 1);
1001-
assert_eq!(snapshot.verification_drafts, 7);
10021019
}
10031020
let text = metrics.render();
10041021
for line in [
@@ -1024,7 +1041,6 @@ mod tests {
10241041
let snapshot = metrics.snapshot(true);
10251042
assert_eq!(snapshot.draft_steps, 7);
10261043
assert_eq!(snapshot.verify_steps, 2);
1027-
assert_eq!(snapshot.verification_drafts, 2);
10281044
let text = metrics.render();
10291045
assert!(text.contains("flare_proposal_rounds_total{batch=\"4\",depth=\"7\"} 1"));
10301046
assert!(text.contains("flare_proposal_gpu_nanoseconds_total{batch=\"4\",depth=\"7\"} 700"));
@@ -1034,7 +1050,7 @@ mod tests {
10341050
#[test]
10351051
fn snapshot_json_round_trips_beside_the_prometheus_counters() {
10361052
let metrics = Metrics::new(100, 128, 0);
1037-
metrics.progress(40, 3, 2, 1);
1053+
metrics.progress(40, 3, 2, 1, 5, 2);
10381054
metrics.scheduler(SchedulerStats {
10391055
active_requests: 2,
10401056
free_pages: 10,
@@ -1044,6 +1060,8 @@ mod tests {
10441060
let snapshot = metrics.snapshot(true);
10451061
let json = snapshot.json(true);
10461062
assert_eq!(json["generated_tokens"], 3);
1063+
assert_eq!(json["verification_drafts"], 5);
1064+
assert_eq!(json["verification_rounds"], 2);
10471065
assert_eq!(json["free_pages"], 10);
10481066
assert_eq!(json["learned_drafter"], true);
10491067
assert_eq!(serde_json::from_value::<Snapshot>(json).unwrap(), snapshot);
@@ -1057,6 +1075,8 @@ mod tests {
10571075
let text = metrics.render();
10581076
for line in [
10591077
"flare_generated_tokens_total 3",
1078+
"flare_verification_draft_tokens_total 5",
1079+
"flare_verification_request_rounds_total 2",
10601080
"flare_prefill_tokens_total 40",
10611081
"flare_active_requests 2",
10621082
"flare_free_kv_pages 10",

‎helios/src/server/runtime.rs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1319,10 +1319,13 @@ impl<'a> Worker<'a> {
13191319
let mut prefill = 0;
13201320
let mut accepted = 0;
13211321
let mut draft_rounds = 0;
1322+
let mut verification_drafts = 0;
1323+
let mut verification_rounds = 0;
13221324
if matches!(
13231325
kind,
13241326
GraphKind::Prefill
13251327
| GraphKind::ScoredPrefill
1328+
| GraphKind::Decode
13261329
| GraphKind::Speculative
13271330
| GraphKind::Verification
13281331
| GraphKind::PackedSpeculative
@@ -1343,9 +1346,11 @@ impl<'a> Worker<'a> {
13431346
.saturating_sub(live.consumed_tokens)
13441347
.min(lane.tokens as u64);
13451348
} else if record.generated_tokens > live.generated_tokens as i64 {
1349+
verification_rounds += 1;
13461350
if let Some(vector) = record.vector {
13471351
let offered =
13481352
(vector.offered_drafts as usize).min(lane.tokens.saturating_sub(1));
1353+
verification_drafts += offered as u64;
13491354
if offered > 0 {
13501355
accepted += (vector.accepted_drafts as usize).min(offered) as u64;
13511356
draft_rounds += 1;
@@ -1377,7 +1382,8 @@ impl<'a> Worker<'a> {
13771382
completion.speculative_survival.as_ref().map(|rows| rows.as_slice()))?
13781383
};
13791384
self.metrics
1380-
.progress(prefill, generated.len(), accepted, draft_rounds);
1385+
.progress(prefill, generated.len(), accepted, draft_rounds,
1386+
verification_drafts, verification_rounds);
13811387
self.metrics.replay_completed(completion.timing);
13821388
if prefill > 0 && self.store.is_some() {
13831389
self.observe_prefill_rate(completion.fence, prefill);

‎helios/src/server/runtime/phase_tests.rs‎

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,9 @@ fn worker_observes_prefill_decode_and_prerequisite_timings() {
430430
frame.timing.wall_ns = 350;
431431
worker.complete(kind, frame).unwrap();
432432
assert_eq!(worker.scheduler.estimated_duration_ns(&step), Some(619));
433+
let metrics = fixture.metrics.snapshot(false);
434+
assert_eq!(metrics.verification_drafts, 0);
435+
assert_eq!(metrics.verification_rounds, u64::from(kind == GraphKind::Decode));
433436
}
434437
}
435438

@@ -903,6 +906,8 @@ fn speculative_dashboard_counts_live_rounds_and_batched_forward_passes() {
903906
.is_err()
904907
);
905908
assert_eq!(fixture.metrics.snapshot(!copy).generated_tokens, 1);
909+
assert_eq!(fixture.metrics.snapshot(!copy).verification_rounds, 0);
910+
assert_eq!(fixture.metrics.snapshot(!copy).verification_drafts, 0);
906911
worker.complete(GraphKind::Speculative, frame).unwrap();
907912
let profile = worker
908913
.scheduler
@@ -916,11 +921,90 @@ fn speculative_dashboard_counts_live_rounds_and_batched_forward_passes() {
916921
let metrics = fixture.metrics.snapshot(!copy);
917922
assert_eq!((metrics.prefill_tokens, metrics.generated_tokens), (13, 4));
918923
assert_eq!((metrics.accepted_drafts, metrics.draft_rounds), (2, 1));
924+
assert_eq!((metrics.verification_drafts, metrics.verification_rounds), (3, 1));
919925
assert_eq!(metrics.verify_steps, 1);
920926
assert_eq!(metrics.draft_steps, if copy { 0 } else { 3 });
921927
}
922928
}
923929

930+
#[test]
931+
fn packed_dashboard_depth_uses_allocated_prefixes_and_live_request_count() {
932+
use crate::model::contract::{GraphOperation, ValueSource};
933+
let mut fixture = Fixture::new();
934+
fixture.config.speculative_widths = (1..=8).collect();
935+
let mut manifest = crate::model::testing::manifest(&fixture.config);
936+
let name = "Speculative-4-8";
937+
let graph = manifest.graphs.iter_mut().find(|g| g.name == name).unwrap();
938+
graph.kind = GraphKind::PackedSpeculative;
939+
graph.draft_tokens = 7;
940+
graph.packed_token_capacity = 9;
941+
let contract = fixture.config.contract.graphs.iter_mut().find(|g| g.graph == name).unwrap();
942+
contract.kind = GraphOperation::PackedSpeculative;
943+
for input in &mut contract.inputs {
944+
if input.source == ValueSource::QueryLength {
945+
input.source = ValueSource::SpeculativePrefixLimit;
946+
}
947+
}
948+
let mut scheduler = RequestScheduler::new(fixture.config.identity, fixture.config.limits,
949+
crate::model::scheduler_capabilities(&fixture.config)).unwrap();
950+
let graphs: Vec<_> = manifest.graphs.iter().filter(|g|
951+
g.kind != GraphKind::Speculative || g.token_capacity == 1).cloned().map(|mut g| {
952+
g.estimated_duration_ns = match g.kind {
953+
GraphKind::Speculative | GraphKind::Decode => 100_000,
954+
GraphKind::PackedSpeculative => 10_000,
955+
_ => 100,
956+
};
957+
g
958+
}).collect();
959+
scheduler.measured_graph_durations(&graphs).unwrap();
960+
scheduler.enable_speculation(&(1..=8).collect::<Vec<_>>()).unwrap();
961+
scheduler.split_speculation(8).unwrap();
962+
let (sender, receiver) = mpsc::channel();
963+
let mut generations = Vec::new();
964+
for token in 0..3 {
965+
let (incoming, generation) = fixture.incoming(vec![token; 13], 16);
966+
sender.send(vec![incoming]).unwrap();
967+
generations.push(generation);
968+
}
969+
let mut worker = Worker::new(&fixture.config, &fixture.frontend, &fixture.stop,
970+
&fixture.metrics, scheduler, receiver, 4);
971+
worker.inputs = Some(crate::model::Inputs::new(&fixture.config, &manifest).unwrap());
972+
assert_eq!(worker.admit().unwrap(), ControlFlow::Continue(true));
973+
let mut sequence = 0;
974+
let step = loop {
975+
sequence += 1;
976+
assert!(sequence < 20);
977+
let (plan, _) = worker.plan(0).unwrap();
978+
let step = plan.unwrap();
979+
worker.reserve_and_pack(&step, Fence(sequence)).unwrap();
980+
if step.kind == GraphKind::PackedSpeculative { break step; }
981+
let frame = completion(&worker, &step, sequence, step.kind.is_prefill().then_some(0));
982+
worker.complete(step.kind, frame).unwrap();
983+
};
984+
assert_eq!((step.batch_capacity, step.token_capacity, step.lanes.len()), (4, 8, 3));
985+
let mut frame = completion(&worker, &step, sequence, Some(1));
986+
frame.proposal_confidence = Some([[0.5; 7]; crate::execution::REQUEST_LANES]);
987+
for (lane, offered) in step.lanes.iter().zip([5, 1, 0]) {
988+
let record = &mut frame.records[lane.request.slot as usize];
989+
record.consumed_tokens = lane.start_position as i64 + 1;
990+
record.vector = Some(crate::execution::TokenVector {
991+
emitted_count: 1, offered_drafts: offered, accepted_drafts: 0,
992+
verification_width: 8, draft_cache_consumed: lane.start_position as i64,
993+
tokens: [1, -1, -1, -1, -1, -1, -1, -1], ..Default::default()
994+
});
995+
}
996+
worker.complete(GraphKind::PackedSpeculative, frame).unwrap();
997+
let metrics = fixture.metrics.snapshot(true);
998+
assert_eq!((metrics.verification_drafts, metrics.verification_rounds), (6, 3));
999+
assert_eq!((metrics.accepted_drafts, metrics.draft_rounds), (0, 2));
1000+
assert_eq!((metrics.draft_steps, metrics.verify_steps), (7, 1));
1001+
let start = std::time::Instant::now();
1002+
let mut history = super::super::dashboard::History::new(start, Default::default());
1003+
history.sample(start + std::time::Duration::from_secs(1), metrics);
1004+
assert_eq!(history.rates().depth, Some(2.0));
1005+
assert_eq!(history.rates().acceptance, Some(0.0));
1006+
}
1007+
9241008
#[test]
9251009
fn shared_decoded_history_reaches_copy_and_mtp_uploads_after_response_completion() {
9261010
use crate::model::contract::{GraphOperation, InputIndexing, ValueSource};
@@ -2203,7 +2287,7 @@ async fn metrics_route_serves_prometheus_or_json_without_the_api_key() {
22032287
use std::io::{Read, Write};
22042288
let (sender, _receiver) = mpsc::sync_channel(4);
22052289
let state = http_state(sender, Some("serving-key".into()));
2206-
state.model.metrics().progress(10, 3, 0, 0);
2290+
state.model.metrics().progress(10, 3, 0, 0, 0, 0);
22072291
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
22082292
let address = listener.local_addr().unwrap();
22092293
let router = super::super::router(state, &[], 65536);

0 commit comments

Comments
 (0)