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]>
175 lines
6.5 KiB
Rust
175 lines
6.5 KiB
Rust
//! 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);
|
||
}
|
||
}
|