open-standards/eoc/eoc-rs/crates/eoc-spec-decode/tests/integration.rs
Transaction Science 3fe109d58f ingest batch 4: 9 crates · table-stakes completers across all three workspaces
OpenPay (3 crates, ~13k LOC, 175 tests)
  op-connect           sub-merchant onboarding + KYB + split-payments + payouts + 1099-K
  op-3ds2              3DS2.3 SCA processor (AReq/ARes/CReq/CRes/RReq/RRes + exemptions)
  op-tax               US/EU/UK/AU/CA/IN tax with Avalara/Stripe/CCH/Vertex/TaxJar adapters

Smart Byte (3 crates, ~7k LOC, 57 tests)
  smart-byte-libp2p    libp2p transport (kad/gossipsub/mdns/autonat/relay/dcutr/identify/ping/req_resp)
  smart-byte-crdt      LWW/GCounter/PnCounter/OrSet/LwwMap/RgaList/2P-Set + HLC + vector clock + Automerge/Yjs interop
  smart-byte-keri-witness  KERI key-rotation cycle + witness layer + watchers + recovery + delegation

EOC (3 crates, ~8k LOC, 108 tests)
  eoc-tools            function-calling across Anthropic/OpenAI/Google/Mistral/Cohere + parallel exec + sandbox
  eoc-multimodal       vision (CLIP/SigLIP/ColPali) + audio (Whisper/MMS) + video sampling + modality router
  eoc-spec-decode      vanilla + SpS-with-temp + lookahead speculative decoding + EAGLE/Medusa stubs

All three workspaces compile cleanly. Forbidden-unsafe, no unwrap outside tests.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 04:00:04 -04:00

175 lines
6.5 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Integration tests for `eoc-spec-decode`.
//!
//! These complement the unit tests in each module by exercising the
//! crate's public surface end-to-end: a real `SpeculativeDecoder`, a
//! real `SpeculativeBackend`, and a property test against the
//! orchestrator's invariants.
use std::sync::Arc;
use eoc_core::{JouleSource, Query, Stage as StageKind};
use eoc_neural::NeuralBackend;
use eoc_spec_decode::{
Generation, GreedySampler, LookaheadDecoding, SpeculativeAlgorithm, SpeculativeBackend,
SpeculativeDecoder, SpsWithTemperature, SyntheticDraft, SyntheticTarget, VanillaSpeculative,
};
use eoc_spec_decode::orchestrator::baseline_generate;
use proptest::prelude::*;
fn build_decoder(
acceptance: f32,
k: usize,
max_new_tokens: usize,
seed: u64,
) -> (SpeculativeDecoder, Arc<SyntheticTarget>) {
let draft = Arc::new(SyntheticDraft::new("d", vec![7; 32], 16, 100));
let target = Arc::new(SyntheticTarget::new("t", acceptance, 50_000, 16, seed));
let dec = SpeculativeDecoder::new(
draft,
target.clone(),
SpeculativeAlgorithm::Vanilla(VanillaSpeculative::new(k)),
max_new_tokens,
Box::new(GreedySampler),
)
.expect("ok");
(dec, target)
}
#[tokio::test]
async fn vanilla_synthetic_full_acceptance_minimum_forward_passes() {
let (dec, target) = build_decoder(1.0, 4, 25, 1);
let g: Generation = dec.generate("hello").await.expect("ok");
assert_eq!(g.total_new_tokens, 25);
// K=4 with full acceptance + 1 bonus token per pass = 5 per
// round. ceil(25 / 5) = 5 forward passes.
assert_eq!(g.target_forward_passes, 5);
assert_eq!(target.forward_pass_count(), 5);
assert!((g.acceptance_rate - 1.0).abs() < 1e-6);
}
#[tokio::test]
async fn vanilla_synthetic_80pct_acceptance_yields_speedup() {
// K = 4, acceptance ~ 0.8, max_new_tokens = 200. With 80%
// per-position acceptance, expected accepted prefix length per
// round ≈ 0.8 + 0.8^2 + 0.8^3 + 0.8^4 ≈ 2.36, plus one bonus =
// ~3.36 new tokens per target forward pass. Speedup vs baseline
// (~1 token per forward pass) is ~3.36×.
let (dec, _) = build_decoder(0.8, 4, 200, 42);
let g = dec.generate("hello").await.expect("ok");
assert_eq!(g.total_new_tokens, 200);
let baseline_target = Arc::new(SyntheticTarget::new("t", 0.0, 50_000, 16, 99));
let (baseline_emitted, baseline_passes, _) =
baseline_generate(baseline_target, "hello", 200, 0)
.await
.expect("ok");
assert_eq!(baseline_emitted, 200);
// Baseline is 1 emitted token per pass (replacement only).
assert_eq!(baseline_passes, 200);
let speedup = baseline_passes as f32 / g.target_forward_passes as f32;
assert!(
speedup >= 2.5,
"expected ≥2.5× speedup, got {speedup}× ({} vs {} passes)",
baseline_passes,
g.target_forward_passes
);
}
#[tokio::test]
async fn joule_attribution_sums_components() {
let (dec, _) = build_decoder(0.6, 4, 40, 7);
let g = dec.generate("hi").await.expect("ok");
assert_eq!(g.total_joules, g.draft_joules + g.target_joules);
// Both components must be non-zero in a non-trivial run.
assert!(g.draft_joules > 0);
assert!(g.target_joules > 0);
}
#[tokio::test]
async fn wrapper_plugs_into_cascade_as_neural_backend() {
let draft = Arc::new(SyntheticDraft::new("d", vec![3; 16], 16, 100));
let target = Arc::new(SyntheticTarget::new("t", 0.7, 50_000, 16, 11));
let wrapper = SpeculativeBackend::build(
draft,
target,
SpeculativeAlgorithm::Vanilla(VanillaSpeculative::new(4)),
16,
)
.expect("ok");
let backend: &dyn NeuralBackend = &wrapper;
let q = Query::new("hi there");
let r = backend.infer(&q).await;
assert_eq!(r.stage, StageKind::Neural);
assert_eq!(r.joule_cost.source, JouleSource::Estimated);
assert!(r.joule_cost.microjoules > 0);
assert_eq!(r.query_id, q.id);
}
#[tokio::test]
async fn sps_with_temperature_runs_end_to_end() {
// We can't drive SpS-with-temperature through the orchestrator's
// synthetic target (the target uses its own acceptance Bernoulli,
// not the SpS test), but we can verify the algorithm's
// `accept` + `sample_adjusted` methods compose cleanly under
// realistic conditions.
let sps = SpsWithTemperature::new(4, 1.0);
let draft = vec![0.0, 5.0, 1.0, 0.0];
let target = vec![3.0, 3.0, 1.0, 0.0];
let mut rng = eoc_spec_decode::sampler::SplitMix64::new(13);
let mut accepts = 0;
let mut rejects = 0;
for _ in 0..500 {
match sps.accept(1, &draft, &target, &mut rng).expect("ok") {
eoc_spec_decode::algorithms::AcceptanceDecision::Accept => accepts += 1,
eoc_spec_decode::algorithms::AcceptanceDecision::Reject => rejects += 1,
}
}
// p_target(1) / p_draft(1) ≈ softmax(target)[1] / softmax(draft)[1]
// is well under 1, so we expect more rejects than accepts but
// some of both.
assert!(accepts > 0);
assert!(rejects > 0);
}
#[test]
fn lookahead_pure_rust_decode_no_draft_needed() {
let mut la = LookaheadDecoding::new(4, 3);
la.ingest(&[10, 20, 30, 40, 50]);
let proposals = la.propose_from_cache(&[10, 20, 30]);
// Last n-1 = (20, 30) -> we observed 40 next.
assert_eq!(proposals, vec![40]);
// After ingesting another continuation for (20, 30), we should
// see both candidates within the window.
la.ingest(&[20, 30, 60]);
let proposals = la.propose_from_cache(&[10, 20, 30]);
assert_eq!(proposals, vec![40, 60]);
}
// ---------------------------------------------------------------------
// Property test: regardless of the per-position accept/reject decisions
// the synthetic target makes, the decoder must emit exactly
// `max_new_tokens` tokens. No off-by-one in either direction.
proptest! {
#[test]
fn decoder_always_emits_exactly_max_new_tokens(
acceptance in 0.0f32..=1.0f32,
k in 1usize..=8,
max_new_tokens in 1usize..=64,
seed in any::<u64>(),
) {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("rt");
let result: eoc_spec_decode::SpecDecodeResult<Generation> = runtime.block_on(async {
let (dec, _) = build_decoder(acceptance, k, max_new_tokens, seed);
dec.generate("p").await
});
let g = result.expect("ok");
prop_assert_eq!(g.total_new_tokens, max_new_tokens);
prop_assert_eq!(g.tokens.len(), max_new_tokens);
}
}