open-standards/joule-code/examples/l3-jouleclaw/src/runtime_backend.rs
Transaction Science 58b270aa94 joule-code L3 ← hybrid families: the two arcs meet
The l3-jouleclaw binding gains HybridBackend — the wave
57/58 hybrid surface as a Joule Code L3 stage:

  point it at a snapshot directory; the family is
  auto-detected from config.json's model_type
  (nemotron_h | granitemoehybrid | falcon_h1), the BF16
  checkpoint mmaps, and hybrid_lm::generate drives it —
  batched prefill (bit-identical to sequential by the
  loader's parity guarantees), greedy or seeded sampling,
  EOS stop ids read from the config (int or list).

  Same LlmBackend trait as every other backend; it drops
  into jouleclaw_l3_stage unchanged. Energy stays honest:
  no per-call meter on this path → energy_joules: None,
  the receipt carries the catalog Estimator figure.

Verified on the real Falcon-H1-0.5B checkpoint
(opt-in --ignored, JOULE_HYBRID env to point elsewhere):

  backend:  "The capital of France is"
            → " Paris. It is known for"
  cascade:  L0 cache miss → closes at L3 with a
            content-addressed CodingOp
            (op_id b3:23u9…, estimated_uj 1000000)

Also: repair the example for the jouleclaw-llm-cheap →
jouleclaw-llm crate rename (the example was unbuildable at
HEAD; same API, new name, all refs fixed).

A Mamba2/attention hybrid running on the family's own
from-scratch tensor code now closes the standard's
cascade end to end: tokenize → prefill → decode →
CodingOp → JCR-1-ready receipt, no llama.cpp, no FFI,
no Python.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 17:57:27 -04:00

103 lines
4 KiB
Rust

//! In-process L3 backend: JouleClaw's **own from-scratch GGUF tensor runtime**.
//!
//! Where [`crate::llama_backend::LlamaCompletionBackend`] shells out to
//! `llama.cpp`, this backend runs the model **inside the process** on
//! `jouleclaw-runtime` — the family's own kernel graph engine
//! (`jouleclaw-backend-reference` / `jouleclaw-backend-apple`) over
//! `jouleclaw-loader-gguf`. No external binary, no FFI: tokenize → build the
//! decode-step graph → compile against the registered kernels → autoregressive
//! decode → detokenize, all in Rust the family wrote.
//!
//! It implements the same [`LlmBackend`] trait as every other backend, so it
//! drops into [`crate::jouleclaw_l3_stage`] unchanged. The model loads once at
//! construction (memory-mapped GGUF); each `complete` call is one `generate`.
use jouleclaw_llm::{FinishReason, LlmBackend, LlmError, LlmRequest, LlmResponse};
use jouleclaw_loader_gguf::sample::SamplingConfig;
use jouleclaw_loader_gguf::tokenizer::Vocab;
use jouleclaw_loader_gguf::{read_gguf_file, GgufModel};
use jouleclaw_runtime::{generate, GenerateConfig, KvCacheKind, TokenizerKind};
use std::path::Path;
/// An L3 backend backed by an in-process GGUF model on `jouleclaw-runtime`.
pub struct JouleclawRuntimeBackend {
model: GgufModel,
vocab: Vocab,
name: String,
/// Fallback joule cost reported when no host meter wraps the call.
typical_joules: f64,
}
impl JouleclawRuntimeBackend {
/// Load a GGUF model from disk and prepare its vocabulary. The model is
/// memory-mapped; `complete` runs entirely in-process thereafter.
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, LlmError> {
let path = path.as_ref();
let model = read_gguf_file(path)
.map_err(|e| LlmError::Unavailable(format!("read GGUF {}: {e:?}", path.display())))?;
let vocab = Vocab::from_gguf(&model)
.map_err(|e| LlmError::Unavailable(format!("load vocab: {e:?}")))?;
let name = model
.metadata_string("general.name")
.map(|s| s.to_string())
.unwrap_or_else(|| {
path.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("gguf-model")
.to_string()
});
Ok(Self {
model,
vocab,
name,
typical_joules: 1.0,
})
}
}
impl LlmBackend for JouleclawRuntimeBackend {
fn model_name(&self) -> &str {
&self.name
}
fn complete(&self, request: &LlmRequest) -> Result<LlmResponse, LlmError> {
// temperature 0 → deterministic greedy (the runtime's determinism
// oracle path); >0 → seeded temperature sampling.
let sampling = if request.temperature <= 0.0 {
SamplingConfig::greedy()
} else {
SamplingConfig::temperature(request.temperature, 0)
};
let cfg = GenerateConfig {
max_new_tokens: request.max_tokens as usize,
sampling,
add_bos: true,
tokenizer_kind: TokenizerKind::Auto,
cache_kind: KvCacheKind::InPlace,
max_seq: None,
stop_strings: request.stop.clone(),
};
let result = generate(&self.model, &self.vocab, &request.prompt, &cfg)
.map_err(|e| LlmError::Other(format!("generate: {e}")))?;
Ok(LlmResponse {
text: result.text,
finish_reason: if result.stopped_at_eos {
FinishReason::Stop
} else {
FinishReason::Length
},
input_tokens: result.prompt_token_count as u32,
output_tokens: result.tokens.len() as u32,
// The `generate` path itself carries no energy meter; the run binary
// meters host energy over the whole call and stamps the receipt. So
// honestly: no per-call meter here (`None`), not a fabricated value.
energy_joules: None,
})
}
fn typical_joules_per_call(&self) -> f64 {
self.typical_joules
}
}