open-standards/eoc/eoc-rs/crates/eoc-spec-decode/src/draft.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

78 lines
2.8 KiB
Rust

//! Draft model trait.
//!
//! A *draft* is a cheap autoregressive model that proposes the next `K`
//! tokens given a prefix. Quality is allowed to be mediocre — the
//! target is the source of truth and corrects the draft on rejection —
//! but throughput must be much higher than the target's, otherwise the
//! technique loses energy instead of saving it.
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::error::SpecDecodeResult;
/// Vocabulary identifier for one token. We use `u32` to match the
/// sampler trait in `eoc-local` and the conventions of `tokenizers`,
/// llama.cpp, and ONNX Runtime.
pub type TokenId = u32;
/// A draft proposal — the tokens the draft model thinks come next and
/// the per-position logits it used to pick them. The orchestrator hands
/// both to the target so the target can apply Leviathan's acceptance
/// criterion (greedy or probabilistic) without re-running the draft.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DraftSequence {
/// Proposed tokens in order. Length is at most `k`; a draft is
/// allowed to give up early (e.g. it hit an EOS token).
pub tokens: Vec<TokenId>,
/// Per-position logit vectors as produced by the draft. Same length
/// as `tokens`. Each inner vector has length `vocab_size`.
pub logits: Vec<Vec<f32>>,
/// Energy attributable to producing this proposal, in micro-joules.
/// Backends with a hardware counter report a measured reading;
/// vendor-API backends report an estimate.
pub draft_joules: u64,
}
impl DraftSequence {
/// Build a sequence and assert the vector lengths match.
pub fn new(tokens: Vec<TokenId>, logits: Vec<Vec<f32>>, draft_joules: u64) -> Self {
debug_assert_eq!(
tokens.len(),
logits.len(),
"DraftSequence: tokens and logits must have matching length"
);
Self {
tokens,
logits,
draft_joules,
}
}
/// Number of proposed tokens.
pub fn len(&self) -> usize {
self.tokens.len()
}
/// True if no tokens were proposed.
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
}
/// A pluggable draft model.
///
/// Implementations are `Send + Sync` so the orchestrator can call into
/// them from a Tokio task. Backends that wrap a non-`Send` runtime
/// should put a mutex / oneshot around it; this crate doesn't impose a
/// concurrency story on the draft.
#[async_trait]
pub trait DraftModel: Send + Sync {
/// Propose `k` tokens following `prefix`. Implementations are free
/// to return fewer (e.g. they hit a stop token), but must never
/// return more.
async fn propose(&self, prefix: &str, k: usize) -> SpecDecodeResult<DraftSequence>;
/// Stable identifier for logging / receipts (e.g. `"llama-3-1b"`).
fn name(&self) -> &str;
}