hermes-edge / ARCHITECTURE.md
bclermo's picture
Upload folder using huggingface_hub
696c8be verified
|
Raw
History Blame Contribute Delete
46.6 kB
# Hermes Edge v2 β€” Enhanced Architecture
## Executive Summary
```ascii
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Hermes Edge v2 Architecture β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ HF Model │──▢│ CPU-Wise │──▢│ .litertlm │──▢│ iPhone 16 β”‚ β”‚
β”‚ β”‚ Qwen3-0.6Bβ”‚ β”‚ Converter β”‚ β”‚ Bundle β”‚ β”‚ AI Edge β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ Gallery β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ Draft │──▢│ Draft β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ Model β”‚ β”‚ Verifier β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ Agent │◀─▢│ Tool │◀─▢│ Memory β”‚ β”‚ β”‚
β”‚ β”‚ Loop β”‚ β”‚ Registry β”‚ β”‚ Store β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ DeepSeek │──▢│ Thinking │──▢│ Tool-Augmented β”‚ β”‚
β”‚ β”‚ Reasoner β”‚ β”‚ Trace β”‚ β”‚ Generation (TAG) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
---
## A. Model Pipeline β€” HF β†’ .litertlm on CPU (2.7GB RAM, no GPU)
### Challenge
Qwen3-0.6B is 586 MB at INT4. Full FP16 weights are ~1.2 GB. `ai_edge_torch` conversion normally requires ~8GB+ RAM. We need to fit in 2.7GB.
### Strategy: Stage-wise conversion with memory pooling
```ascii
Stage 1: Download & Shrink ─────────────────────────────────────
HF Qwen3-0.6B (FP16 ~1.2GB)
β”‚
β–Ό
apply_weight_only_int4() β†’ in-place STE quant β†’ ~350 MB in RAM
β”‚
β–Ό
Save as checkpoint.pt (state_dict only, no optimizer)
β”‚ (~350 MB on disk)
β–Ό
Stage 2: ai_edge_torch Build & Load ────────────────────────────
build_ai_edge_model(config) β†’ ~200 MB (uninitialized)
β”‚
β–Ό
Load checkpoint via memory-mapped state_dict
Use torch.load(..., mmap=True) β†’ ~200 MB peak
β”‚
β–Ό
Stage 3: Trace & Lower ─────────────────────────────────────────
converter.convert_to_tflite(
prefill_seq_len=[1024, 1], # shorter prefill = less peak
quantize=full_int4_dynamic_recipe(),
)
β”‚ (~500 MB temporary TFLite)
β–Ό
Stage 4: Bundle ────────────────────────────────────────────────
litert_lm.bundler.create_bundle(
tflite_model=...,
tokenizer=...,
output=dist/hermes-mobile-qwen3-0.6b.litertlm,
)
β”‚
β–Ό
Final .litertlm (~586 MB)
```
### New file: `scripts/convert_qwen.py`
Converts Qwen3-0.6B with CPU-optimized settings:
```
python scripts/convert_qwen.py \
--hf-model Qwen/Qwen3-0.6B \
--preset qwen3-0.6b \
--output dist/hermes-mobile-qwen3-0.6b-int4.litertlm \
--low-memory \ # enables mmap + stage-wise GC
--max-prefill 1024 \ # shorter prefill for RAM savings
--dtype fp32 \ # force fp32 accumulation (no GPU)
--gc-collect-between # explicit gc between stages
```
### Memory Budget (2.7 GB total)
| Step | Peak RSS | Cumulative |
|------|----------|------------|
| HF model load (fp16, mmap) | 0 MB (disk-mapped) | 0 MB |
| PTQ calibration (4 batches) | ~200 MB | 200 MB |
| INT4 weight quant in-place | ~200 MB | 400 MB |
| ai_edge_torch model build | ~200 MB | 600 MB |
| Weight load + remap | ~200 MB | 800 MB |
| TFLite lowering | ~1200 MB | 2000 MB |
| TFLite β†’ .litertlm | ~300 MB | 2300 MB |
| Headroom | 400 MB | 2700 MB |
### New config presets in `hermes/config.py`
```python
def qwen3_0_6b_config() -> HermesConfig:
"""Qwen3-0.6B architecture mapped to HermesConfig."""
return HermesConfig(
vocab_size=151936, # Qwen3 vocabulary
hidden_size=2048,
intermediate_size=8192, # SwiGLU: 3 * hidden
num_layers=28,
num_heads=32,
num_kv_heads=4, # GQA 8:1
head_dim=64,
max_seq_len=32768, # Qwen3 supports 32K context
rope_theta=1000000.0, # Qwen3's RoPE base freq
rms_norm_eps=1e-6,
tie_embeddings=False,
pad_token_id=151643,
bos_token_id=151643,
eos_token_id=151645,
tool_call_start_id=151646, # reserved sentinel
tool_call_end_id=151647,
)
```
### Weight remapping (`convert_qwen.py`)
Qwen3 uses `model.layers.{i}.self_attn.{q,k,v,o}_proj` β†’ fuses to `atten_func.qkv_projection` same as existing `remap_state_dict`. New mapping for Qwen3-specific naming:
| Qwen3 HF name | ai_edge_torch name |
|---------------|-------------------|
| `model.embed_tokens.weight` | `tok_embedding.weight` |
| `model.layers.{i}.self_attn.q_proj.weight` | `transformer_blocks.{i}.atten_func.qkv_projection.weight` (concat q,k,v) |
| `model.layers.{i}.self_attn.k_proj.weight` | ↑ same concat |
| `model.layers.{i}.self_attn.v_proj.weight` | ↑ same concat |
| `model.layers.{i}.self_attn.o_proj.weight` | `transformer_blocks.{i}.atten_func.output_projection.weight` |
| `model.layers.{i}.mlp.gate_proj.weight` | `transformer_blocks.{i}.ff.w1.weight` |
| `model.layers.{i}.mlp.up_proj.weight` | `transformer_blocks.{i}.ff.w3.weight` |
| `model.layers.{i}.mlp.down_proj.weight` | `transformer_blocks.{i}.ff.w2.weight` |
| `model.layers.{i}.input_layernorm.weight` | `transformer_blocks.{i}.pre_atten_norm.weight` |
| `model.layers.{i}.post_attention_layernorm.weight` | `transformer_blocks.{i}.post_atten_norm.weight` |
| `model.norm.weight` | `final_norm.weight` |
| `lm_head.weight` | `lm_head.weight` |
---
## B. Inference Engine β€” Streaming, DeepSeek Reasoning, Tool Calling
### Architecture
```ascii
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ InferenceEngine v2 β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ LiteRT-LM β”‚ β”‚ Reasoning β”‚ β”‚ Constrained β”‚ β”‚
β”‚ β”‚ Runtime │────▢│ Pipeline │────▢│ Decoder β”‚ β”‚
β”‚ β”‚ (.litertlm) β”‚ β”‚ (think/tell) β”‚ β”‚ (tool schema) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β–Ό β–Ό β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Token Stream (AsyncIterator) β”‚ β”‚
β”‚ β”‚ [token, token, ..., <think>, ..., </think>, ..., ] β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ StreamProcessor β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Detoken β”‚ β”‚ Reason β”‚ β”‚ Tool Call β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ & Buffer β”‚ β”‚ Extractor β”‚ β”‚ Parser & Router β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
### New file: `hermes/reasoning.py` β€” DeepSeek V4 Flash Reasoning
DeepSeek V4 Flash reasoning uses a **thinking trace** pattern:
```
User: What is 234 * 567?
Assistant: <think>
Let me break this down step by step...
234 * 500 = 117,000
234 * 60 = 14,040
234 * 7 = 1,638
Sum: 117,000 + 14,040 + 1,638 = 132,678
</think>
The answer is 132,678.
```
Key interface:
```python
@dataclass
class ReasoningConfig:
enabled: bool = True
think_tag: str = "<think>"
end_think_tag: str = "</think>"
max_think_tokens: int = 512
separate_in_stream: bool = True # yield think vs answer separately
think_speed_factor: float = 2.0 # show thinking faster
class ReasoningPipeline:
"""
Wraps token generation with DeepSeek-style think/tell separation.
The model is prompted with a system message that asks it to reason
inside <think> tags before answering. The pipeline:
1. Detects entry into <think> mode
2. Collects thinking trace tokens
3. Detects exit into </think> β†’ answer mode
4. Yields (type, text) tuples: ("think", "...") or ("answer", "...")
"""
def __init__(self, config: ReasoningConfig):
...
def process_stream(
self, token_stream: Iterator[str]
) -> Iterator[Tuple[str, str]]:
"""
Yields ("think", str) while inside <think>...</think>
Yields ("answer", str) when outside.
"""
...
def inject_reasoning_prompt(
self, messages: List[Message]
) -> List[Message]:
"""Adds system-level reasoning instruction."""
...
```
### Inference integration (`hermes/inference.py` β€” rewritten)
The new `InferenceEngine` combines LiteRT-LM runtime with all pipeline stages:
```python
class LiteRTInference:
"""
Runs the .litertlm model via LiteRT-LM Python bindings.
Unlike the old HermesInference (which used PyTorch), this directly
interfaces with the on-device runtime, making it suitable for both
desktop testing (via litert_lm) and mobile deployment (identical API).
"""
def __init__(
self,
model_path: str, # path to .litertlm
runtime: str = "litert", # "litert" | "xnnpack" | "coreml"
max_seq_len: int = 4096,
):
self.model = litert_lm.LiteRTModel(model_path)
self.cache = self.model.create_kv_cache(max_seq_len)
def generate_stream(
self,
prompt_ids: List[int],
max_new_tokens: int = 512,
temperature: float = 0.7,
top_p: float = 0.9,
top_k: int = 40,
repetition_penalty: float = 1.1,
reasoning: bool = True, # DeepSeek reasoning mode
speculative: bool = True, # DSpark draft verification
stream: bool = True,
) -> Iterator[Dict[str, Any]]:
"""
Primary generation entry point.
Yields dicts with keys:
- "type": "think" | "answer" | "tool_call" | "tool_result" | "error"
- "text": str (detokenized chunk)
- "tokens": int (cumulative count)
- "speed": float (tok/s for this chunk)
"""
...
```
### LiteRT-LM Python API binding pattern
The LiteRT-LM runtime exposes this C API via Python ctypes/ffi:
```python
# Pseudocode for how we interact with LiteRT-LM on device
class LiteRTRuntime:
def prefill(self, tokens: List[int]) -> np.ndarray:
"""Run prefill, returns logits for last token. Populates KV cache."""
def decode(self, token: int) -> np.ndarray:
"""Single-token decode with existing KV cache. Returns logits."""
def reset_kv_cache(self):
"""Clear KV cache for new conversation."""
```
---
## C. Agent Framework β€” Hermes-Style Tool Calling
### Architecture
```ascii
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ AgentLoop β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ System β”‚ β”‚ Generate β”‚ β”‚ Parse β”‚ β”‚ Execute β”‚ β”‚
β”‚ β”‚ Prompt │──▢│ Response │──▢│ Tool Calls │──▢│ Tools β”‚ β”‚
β”‚ β”‚ Builder β”‚ β”‚ (with β”‚ β”‚ (supports β”‚ β”‚ (sandbox β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ reasoning)β”‚ β”‚ parallel) β”‚ β”‚ + retry)β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β–² β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ └────────────────────│ Memory β”‚β—€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ Store β”‚ β”‚
β”‚ β”‚ (persist) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Tool β”‚ β”‚ Tool β”‚ β”‚ Tool β”‚ β”‚
β”‚ β”‚ Registry │──▢│ Schema │──▢│ Dispatcher β”‚ β”‚
β”‚ β”‚ (global) β”‚ β”‚ Generator β”‚ β”‚ (async, timeout, retry) β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
### New file: `hermes/agent.py`
```python
@dataclass
class ToolDefinition:
"""JSON Schema tool definition matching OpenAI function calling format."""
name: str
description: str
parameters: Dict[str, Any] # JSON Schema object
required: List[str]
handler: Optional[Callable] = None # Python handler (desktop)
skill_url: Optional[str] = None # AI Edge Gallery skill URL (mobile)
class AgentLoop:
"""
Hermes agent with parallel tool calling, retry, and persistent memory.
Flow per round:
1. Build prompt from conversation history + tool schemas
2. Run LiteRTInference.generate_stream() with reasoning=True
3. Parse tool calls from the output (supports multiple parallel calls)
4. For each tool call:
a. Look up handler in registry
b. Execute with timeout & retry
c. Collect result
5. Append tool results to conversation
6. Loop until no more tool calls or max_rounds reached
"""
def __init__(
self,
inference: LiteRTInference,
tokenizer: Any,
tool_registry: ToolRegistry,
memory: MemorySystem,
max_rounds: int = 10,
parallel_tools: bool = True,
):
...
async def run(
self,
user_input: str,
conversation_id: Optional[str] = None,
) -> AsyncIterator[Dict[str, Any]]:
"""
Full agent loop. Yields events:
{"type": "think", "content": "..."}
{"type": "answer", "content": "..."}
{"type": "tool_call", "name": "...", "args": {...}}
{"type": "tool_result", "name": "...", "result": ...}
{"type": "error", "content": "..."}
{"type": "done", "content": "...", "usage": {...}}
"""
...
def _parse_tool_calls(self, text: str) -> List[Dict]:
"""
Extract all tool calls from model output.
Supports both single and parallel formats:
Single: <tool_call>{...}</tool_call>
Parallel: <tool_calls>
<tool_call>{...}</tool_call>
<tool_call>{...}</tool_call>
</tool_calls>
"""
...
def _build_tool_system_prompt(self, tools: List[ToolDefinition]) -> str:
"""Build Hermes-style tool description for the system prompt."""
...
```
### New file: `hermes/tool_registry.py`
```python
class ToolRegistry:
"""
Global tool registry with schema generation.
Tools can be registered either:
- As Python callables (for desktop testing)
- As AI Edge Gallery Skill URLs (for mobile deployment)
"""
def register(self, tool: ToolDefinition): ...
def unregister(self, name: str): ...
def get_schema(self, name: str) -> Dict: ...
def get_all_schemas(self) -> List[Dict]: ...
def dispatch(self, name: str, arguments: Dict) -> Any:
"""Execute tool with timeout and error handling."""
...
```
### New file: `hermes/memory.py`
```python
class MemorySystem:
"""
Persistent agent memory with retrieval.
Stores conversation summaries, facts, and user preferences
that persist across sessions. Uses a lightweight semantic
indexing approach (simple TF-IDF or miniLM embeddings via
the model's own hidden states).
Memory is injected into the system prompt as context.
"""
def store(self, key: str, value: str, metadata: Dict = {}): ...
def recall(self, query: str, top_k: int = 5) -> List[Dict]: ...
def summarize_conversation(self, messages: List[Message]) -> str: ...
def get_context_prompt(self, query: str) -> str:
"""Returns memory context to inject into system prompt."""
...
```
### Tool Calling Format (NousResearch hermes-agent pattern)
```
Hermes Agent tool format:
<tool_calls>
<tool_call>
{"name": "calculator", "arguments": {"expression": "234*567"}}
</tool_call>
<tool_call>
{"name": "web_search", "arguments": {"query": "current weather London"}}
</tool_call>
</tool_calls>
```
The model is trained to emit parallel `<tool_call>` blocks inside a `<tool_calls>` wrapper. Each call is a JSON object with `name` and `arguments`, matching the Hermes function calling standard.
---
## D. DSpark Speculative Decoding Draft Model
### Theory
Speculative decoding accelerates autoregressive generation by:
1. **Draft**: Small model predicts k tokens in one forward pass
2. **Verify**: Large model evaluates all k tokens in parallel
3. **Accept**: Accept tokens where distributions match, resample at first rejection
```
Without Draft: [tok1] β†’ [tok2] β†’ [tok3] β†’ [tok4] β†’ [tok5] (5 steps)
With DSpark: [tok1 tok2 tok3 tok4] (1 verify step)
[─draft─▢][──────verify──────]
Accept 3/4 β†’ draft again from accepted prefix
```
### Architecture
```ascii
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ DSpark Speculative Decoder β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Main Model β”‚ β”‚ Draft Model β”‚ β”‚ Acceptance β”‚ β”‚
β”‚ β”‚ 270M INT4 β”‚ β”‚ 30M INT4 β”‚ β”‚ Criterion β”‚ β”‚
β”‚ β”‚ ~55 tok/s β”‚ β”‚ ~300 tok/s β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β–Ό β–Ό β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Speculative Loop β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 1. Draft model autoregressively produces k=4 tokens β”‚ β”‚
β”‚ β”‚ (using its own small KV cache) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 2. Main model prefill-fills all k draft tokens in one β”‚ β”‚
β”‚ β”‚ forward pass (extending its KV cache) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 3. Compare draft vs main logits at each position: β”‚ β”‚
β”‚ β”‚ - If draft token == argmax(main_logits): ACCEPT β”‚ β”‚
β”‚ β”‚ - If draft token != argmax(main_logits): REJECT β”‚ β”‚
β”‚ β”‚ and resample from main distribution + truncated β”‚ β”‚
β”‚ β”‚ draft distribution β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ 4. Repeat from the last accepted position β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
### New file: `dspark/draft_model_arch.py`
```python
class DraftModelConfig:
"""
Ultra-light draft model for speculative decoding.
Architecture: 4-layer, 4-head transformer with tied embeddings.
~30M params β†’ ~60 MB at INT4 β†’ ~300 tok/s on iPhone 16 ANE.
"""
vocab_size: int = 32000
hidden_size: int = 512
intermediate_size: int = 1024
num_layers: int = 4
num_heads: int = 4
num_kv_heads: int = 2
head_dim: int = 64
max_seq_len: int = 4096
rope_theta: float = 10000.0
```
### New file: `dspark/draft_verify.py`
```python
class DraftVerifyEngine:
"""
Core speculative decoding loop.
Manages two KV caches (draft and main), runs the draft-verify cycle,
and handles acceptance/rejection logic.
"""
def __init__(
self,
main_model: LiteRTRuntime,
draft_model: LiteRTRuntime,
draft_k: int = 5, # tokens to speculate
temperature: float = 0.7,
top_k: int = 40,
top_p: float = 0.9,
):
self.main = main_model
self.draft = draft_model
self.draft_k = draft_k
self.temperature = temperature
self.top_k = top_k
self.top_p = top_p
self.draft_cache = KVCache(...)
self.main_cache = KVCache(...)
@torch.no_grad()
def generate(
self,
prompt_ids: List[int],
max_new_tokens: int,
) -> Iterator[int]:
"""
Generate tokens with speculative decoding.
Yields accepted token IDs one at a time.
Internal flow:
1. Prefill both models with prompt
2. Loop:
a. Draft k tokens autoregressively (draft model)
b. Main model forward on all k tokens (single pass)
c. Compare & accept/reject each position
d. Yield accepted tokens
e. Reset draft cache to last accepted position
"""
...
def _verify(
self,
draft_tokens: List[int],
main_logits: np.ndarray, # [k, vocab_size]
draft_logits: np.ndarray, # [k, vocab_size]
) -> Tuple[List[int], Optional[int]]:
"""
Verify each draft token against main model logits.
Returns: (accepted_tokens, rejected_position_or_None)
Uses the standard rejection sampling criterion from
Leviathan et al. "Fast Inference from Transformers via
Speculative Decoding" (2022).
"""
...
```
### New file: `dspark/acceptance.py`
```python
def rejection_sample(
main_logits: np.ndarray, # [vocab_size]
draft_logits: np.ndarray, # [vocab_size]
draft_token: int,
temperature: float = 1.0,
rng: Optional[np.random.Generator] = None,
) -> Tuple[bool, int]:
"""
Standard speculative decoding acceptance criterion.
Accept draft_token with probability min(1, p_main / p_draft).
On rejection, resample from max(0, p_main - p_draft) distribution.
"""
...
def greedy_accept(
main_logits: np.ndarray,
draft_token: int,
) -> Tuple[bool, int]:
"""
Greedy acceptance: accept iff draft_token == argmax(main_logits).
On rejection, return argmax(main_logits) as replacement.
Faster than rejection sampling, slightly lower acceptance rate.
This is the recommended mode for INT4 mobile deployment.
"""
...
```
### Bundling Draft Model
The draft model is bundled **inside** the same `.litertlm` file as a second signature:
```python
# In scripts/convert_to_litertlm.py (extended)
def bundle_with_draft(
main_tflite: str,
draft_tflite: str,
tokenizer_path: str,
output_path: str,
config: HermesConfig,
) -> str:
"""
Bundle main model + draft model + tokenizer into single .litertlm.
The .litertlm container supports multiple TFLite graphs as
named signatures:
- "prefill": main model prefill
- "decode": main model decode
- "draft_prefill": draft model prefill
- "draft_decode": draft model decode
"""
from litert_lm import bundler
bundler.create_bundle(
tflite_models={
"prefill": main_tflite.replace(".tflite", "_prefill.tflite"),
"decode": main_tflite.replace(".tflite", "_decode.tflite"),
"draft_prefill": draft_tflite.replace(".tflite", "_prefill.tflite"),
"draft_decode": draft_tflite.replace(".tflite", "_decode.tflite"),
},
tokenizer=tokenizer_path,
output=output_path,
metadata={"speculative_decoding": True, "draft_k": 5},
)
```
### Training the Draft Model: `scripts/train_draft.py`
```
python scripts/train_draft.py \
--teacher dist/hermes-mobile-270m-int4.litertlm \
--student-config draft-30m \
--data data/agentic_sft.jsonl \
--output checkpoints/draft-30m.pt \
--temperature 2.0 \
--lr 1e-3 \
--epochs 5
```
The draft model is trained via **distribution distillation**: minimize KL(teacher || draft) over the teacher's full vocabulary distribution. This teaches the draft model to match the teacher's token preferences, maximizing acceptance rate.
**Outputs**: `checkpoints/draft-30m.pt` β†’ converted to `dist/draft-30m-int4.tflite` via same `convert_to_litertlm.py` pipeline.
---
## E. Deployment β€” iPhone 16 via AI Edge Gallery
### Package Structure
```ascii
hermes-mobile-v2.litertlm (single file, ~650 MB)
β”œβ”€β”€ Signature: "prefill" β†’ Main model prefill (TFLite)
β”œβ”€β”€ Signature: "decode" β†’ Main model decode (TFLite)
β”œβ”€β”€ Signature: "draft_prefill" β†’ Draft model prefill (TFLite)
β”œβ”€β”€ Signature: "draft_decode" β†’ Draft model decode (TFLite)
β”œβ”€β”€ Tokenizer: SentencePiece .model
β”œβ”€β”€ Metadata:
β”‚ β”œβ”€β”€ model_name: "Hermes Edge v2"
β”‚ β”œβ”€β”€ quantization: "int4_per_channel"
β”‚ β”œβ”€β”€ context_length: 4096
β”‚ β”œβ”€β”€ speculative_decoding: true
β”‚ β”œβ”€β”€ draft_k: 5
β”‚ β”œβ”€β”€ agentic: true
β”‚ β”œβ”€β”€ tools: ["calculator", "web_search", "memory", "timer"]
β”‚ β”œβ”€β”€ reasoning: true
β”‚ └── runtime_info:
β”‚ β”œβ”€β”€ min_ios: "18.0"
β”‚ β”œβ”€β”€ min_device: "iPhone 16"
β”‚ └── delegate: "coreml"
```
### Gallery Import
Users import via URL or file:
```
https://huggingface.co/bclermo/hermes-edge/resolve/main/hermes-mobile-v2.litertlm
```
### iOS Runtime Layer (Swift pseudocode for Xcode project)
```swift
// HermesEdgeAgent.swift β€” The on-device agent runtime
import LiteRTLM
class HermesEdgeAgent {
let model: LiteRTLModel
let tokenizer: SentencePieceTokenizer
let draftModel: LiteRTLModel? // optional, for speculative decoding
init(bundlePath: String) throws {
self.model = try LiteRTLModel(path: bundlePath, signature: "decode")
self.tokenizer = try SentencePieceTokenizer(path: bundlePath)
if model.hasSignature("draft_decode") {
self.draftModel = try LiteRTLModel(path: bundlePath, signature: "draft_decode")
}
}
func generate(
messages: [Message],
tools: [ToolDefinition]?,
onToken: (TokenEvent) -> Void,
completion: (Result<String, Error>) -> Void
) {
let prompt = buildHermesPrompt(messages, tools: tools)
// Prefill
let tokens = tokenizer.encode(prompt)
model.runSignature("prefill", input: tokens)
// Generate loop with optional speculative decoding
if let draft = draftModel {
speculativeGenerate(draft: draft, onToken: onToken, completion: completion)
} else {
standardGenerate(onToken: onToken, completion: completion)
}
}
private func speculativeGenerate(
draft: LiteRTLModel,
onToken: (TokenEvent) -> Void,
completion: (Result<String, Error>) -> Void
) {
let draftK = 5
var acceptedTokens: [Int] = []
while acceptedTokens.count < maxTokens {
// Draft: run draft model autoregressively
var draftTokens: [Int] = []
for _ in 0..<draftK {
let draftLogits = draft.runSignature("draft_decode", input: lastToken)
draftTokens.append(sample(draftLogits))
}
// Verify: run main model on all draft tokens in one prefill
let mainLogits = model.runSignature("prefill", input: draftTokens)
// mainLogits shape: [draftK, vocabSize]
// Accept/reject each token
for i in 0..<draftK {
if greedy_accept(mainLogits[i], draftTokens[i]) {
acceptedTokens.append(draftTokens[i])
onToken(.token(tokenizer.decode([draftTokens[i]])))
} else {
acceptedTokens.append(argmax(mainLogits[i]))
onToken(.token(tokenizer.decode([argmax(mainLogits[i])])))
break // stop at first rejection
}
}
}
completion(.success(tokenizer.decode(acceptedTokens)))
}
}
```
### Performance Targets (iPhone 16, A18 Pro ANE)
| Mode | Tokens/sec | Speedup vs Baseline |
|------|-----------|-------------------|
| Baseline (no draft) | ~55 tok/s | 1.0Γ— |
| DSpark k=3 | ~110 tok/s | 2.0Γ— |
| DSpark k=5 | ~140 tok/s | 2.5Γ— |
| DSpark k=7 | ~150 tok/s | 2.7Γ— |
| DSpark + CoreML optimizations | ~165 tok/s | 3.0Γ— |
### AI Edge Gallery Agent Skills
Each tool maps to an AI Edge Gallery Agent Skill (JavaScript, sandboxed):
| Tool | Skill File | Runtime |
|------|-----------|---------|
| Calculator | `skills/hermes_calculator/SKILL.md` | In-app JS sandbox |
| Web Search | `skills/hermes_web_search/SKILL.md` | URL session (offline cache) |
| Memory | `skills/hermes_memory/SKILL.md` | App storage (KV store) |
| Timer | `skills/hermes_timer/SKILL.md` | iOS timer API via bridge |
---
## New File Structure (Additions in bold)
```
hermes-edge/
β”œβ”€β”€ hermes/
β”‚ β”œβ”€β”€ __init__.py [ADD] exports ReasoningConfig, AgentLoop, ToolRegistry
β”‚ β”œβ”€β”€ config.py [EDIT] add qwen3_0_6b_config()
β”‚ β”œβ”€β”€ model.py [EDIT] add DraftModelForCausalLM for training
β”‚ β”œβ”€β”€ inference.py [REWRITE] LiteRTInference with streaming & speculative
β”‚ β”œβ”€β”€ kv_cache.py [EXISTING]
β”‚ β”œβ”€β”€ quantization.py [EXISTING]
β”‚ β”œβ”€β”€ chat_template.py [EDIT] add parallel tool call format, DeepSeek reason tags
β”‚ β”œβ”€β”€ reasoning.py [NEW] DeepSeek V4 Flash reasoning pipeline
β”‚ β”œβ”€β”€ agent.py [NEW] Hermes agent loop with tool orchestration
β”‚ β”œβ”€β”€ tool_registry.py [NEW] Tool registration & dispatch
β”‚ └── memory.py [NEW] Persistent agent memory store
β”œβ”€β”€ dspark/
β”‚ β”œβ”€β”€ __init__.py [NEW]
β”‚ β”œβ”€β”€ draft_model_arch.py [NEW] Draft transformer architecture
β”‚ β”œβ”€β”€ draft_verify.py [NEW] Draft-verify loop
β”‚ └── acceptance.py [NEW] Acceptance criteria (greedy, rejection)
β”œβ”€β”€ agent/
β”‚ β”œβ”€β”€ __init__.py [NEW]
β”‚ β”œβ”€β”€ tool_defs.py [NEW] Tool definition schemas & validation
β”‚ β”œβ”€β”€ dispatcher.py [NEW] Async tool dispatcher with timeout/retry
β”‚ β”œβ”€β”€ context.py [NEW] Conversation context manager
β”‚ └── memory_store.py [NEW] On-device KV memory store backend
β”œβ”€β”€ scripts/
β”‚ β”œβ”€β”€ convert_to_litertlm.py [EDIT] add draft model bundling
β”‚ β”œβ”€β”€ convert_qwen.py [NEW] Qwen3-specific CPU-only conversion
β”‚ β”œβ”€β”€ train_draft.py [NEW] Train draft model via distillation
β”‚ β”œβ”€β”€ train.py [EXISTING]
β”‚ β”œβ”€β”€ distill_from_gemma.py [EXISTING]
β”‚ β”œβ”€β”€ benchmark.py [EDIT] add speculative decode benchmark mode
β”‚ β”œβ”€β”€ eval.py [EXISTING]
β”‚ └── train_tokenizer.py [EXISTING]
β”œβ”€β”€ deployment/
β”‚ β”œβ”€β”€ gallery_manifest.json [NEW] AI Edge Gallery metadata
β”‚ └── hermes_ios/ [NEW] Optional Swift Xcode project
β”œβ”€β”€ data/
β”‚ β”œβ”€β”€ eval.jsonl [EXISTING]
β”‚ └── tool_eval.jsonl [EXISTING]
β”œβ”€β”€ tests/
β”‚ β”œβ”€β”€ test_model.py [EDIT] add draft model tests
β”‚ β”œβ”€β”€ test_inference.py [EDIT] add reasoning & speculative tests
β”‚ β”œβ”€β”€ test_kv_cache.py [EXISTING]
β”‚ β”œβ”€β”€ test_quantization.py [EXISTING]
β”‚ β”œβ”€β”€ test_reasoning.py [NEW] Reasoning pipeline tests
β”‚ β”œβ”€β”€ test_agent.py [NEW] Agent loop tests
β”‚ └── test_dspark.py [NEW] Speculative decoding tests
└── requirements.txt [EDIT] add psutil, transformers (optional)
```
---
## Key Interfaces Summary
| Interface | File | Purpose |
|-----------|------|---------|
| `LiteRTInference.generate_stream()` | `hermes/inference.py` | Main streaming generation (new) |
| `ReasoningPipeline.process_stream()` | `hermes/reasoning.py` | DeepSeek think/tell separation |
| `AgentLoop.run()` | `hermes/agent.py` | Full agent orchestration loop |
| `ToolRegistry.dispatch()` | `hermes/tool_registry.py` | Tool lookup & execution |
| `MemorySystem.recall()` | `hermes/memory.py` | Semantic memory retrieval |
| `DraftVerifyEngine.generate()` | `dspark/draft_verify.py` | Speculative decoding loop |
| `greedy_accept()` | `dspark/acceptance.py` | Token acceptance criterion |
---
## Data Flow: Complete Request β†’ Response
```
User: "What's 234*567? Also, set a timer for 5 minutes."
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. AgentLoop.run() β”‚
β”‚ β”œβ”€β”€ Build system prompt with: β”‚
β”‚ β”‚ - Tool schemas (calculator, timer) β”‚
β”‚ β”‚ - Memory context (if any) β”‚
β”‚ β”‚ - Reasoning instruction β”‚
β”‚ β”œβ”€β”€ Prefill prompt (main model) β”‚
β”‚ └── Enter generate loop β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 2. LiteRTInference.generate_stream(speculative=True, reasoning=True) β”‚
β”‚ β”œβ”€β”€ Draft model predicts k=5 tokens: "Let", " me", " think", "...", "" β”‚
β”‚ β”œβ”€β”€ Main model verifies: accept "Let", " me", " think", "..." β”‚
β”‚ β”‚ reject "" β†’ replace with "<" β”‚
β”‚ β”œβ”€β”€ Continue: draft "think", ">", "234", " *", " 567" β†’ verify β†’ accept β”‚
β”‚ └── After ~20 tokens: reach "</think>" β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 3. ReasoningPipeline.process_stream() β”‚
β”‚ β”œβ”€β”€ Detect <think> tag β†’ emit type="think" chunks β”‚
β”‚ β”œβ”€β”€ Detect </think> tag β†’ switch to type="answer" chunks β”‚
β”‚ └── Yield: ("think", "Let me break this down..."), ("answer", "I'll...") β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 4. Model emits: β”‚
β”‚ <tool_calls> β”‚
β”‚ <tool_call>{"name":"calculator","arguments":{"expression":"234*567"}}</> β”‚
β”‚ <tool_call>{"name":"timer","arguments":{"duration":300,"unit":"seconds"}}</>β”‚
β”‚ </tool_calls> β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 5. AgentLoop._parse_tool_calls() β”‚
β”‚ β”œβ”€β”€ Extract 2 tool calls from <tool_calls> block β”‚
β”‚ β”œβ”€β”€ Parallel dispatch via ToolRegistry β”‚
β”‚ β”‚ β”œβ”€β”€ calculator β†’ 132,678 β”‚
β”‚ β”‚ └── timer β†’ {"status": "created", "id": "t1"} β”‚
β”‚ └── Append results as tool messages β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ 6. Second round: model generates final answer β”‚
β”‚ "234 * 567 = 132,678. I've also set a 5-minute timer." β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
```
---
## Build Steps (Ordered)
### Phase 1: Environment
```bash
# 1. Install system deps
sudo apt-get install cmake python3-dev build-essential
# 2. Create venv
python3 -m venv venv && source venv/bin/activate
# 3. Install Hermes Edge + LiteRT stack
pip install -e .
pip install ai-edge-torch litert-lm sentencepiece torch numpy psutil
# 4. Install optional (for Qwen3 conversion)
pip install transformers accelerate safetensors
```
### Phase 2: Convert Qwen3-0.6B to .litertlm
```bash
# 5. Convert Qwen3-0.6B (CPU, <2.7GB RAM)
python scripts/convert_qwen.py \
--hf-model Qwen/Qwen3-0.6B \
--preset qwen3-0.6b \
--output dist/hermes-mobile-qwen3-0.6b-int4.litertlm \
--low-memory --max-prefill 1024 --gc-collect-between
```
### Phase 3: Train Draft Model
```bash
# 6. Train 30M draft model
python scripts/train_draft.py \
--teacher dist/hermes-mobile-qwen3-0.6b-int4.litertlm \
--student-config draft-30m \
--data data/agentic_sft.jsonl \
--output checkpoints/draft-30m.pt \
--temperature 2.0 --lr 1e-3 --epochs 5
# 7. Convert draft to TFLite
python scripts/convert_to_litertlm.py \
--checkpoint checkpoints/draft-30m.pt \
--tokenizer tokenizer/hermes.model \
--preset draft-30m \
--output dist/draft-30m-int4.tflite \
--backend apple --multi-sig
```
### Phase 4: Final Bundle
```bash
# 8. Bundle main + draft + tokenizer into single .litertlm
python scripts/convert_to_litertlm.py \
--checkpoint dist/hermes-mobile-qwen3-0.6b-int4.litertlm \
--draft-checkpoint dist/draft-30m-int4.tflite \
--tokenizer tokenizer/hermes.model \
--preset qwen3-0.6b \
--output dist/hermes-mobile-v2.litertlm \
--backend apple --multi-sig --bundle-draft
```
### Phase 5: Verify
```bash
# 9. Run tests
pytest tests/ -v
# 10. Benchmark (desktop - CPU)
python scripts/benchmark.py \
--preset qwen3-0.6b \
--seq-lens 64 128 256 512 \
--speculative \
--runs 3
# 11. Run agent eval
python scripts/eval.py \
--model dist/hermes-mobile-v2.litertlm \
--data data/tool_eval.jsonl \
--reasoning \
--speculative
```
### Phase 6: Deploy
```bash
# 12. Upload to HuggingFace
huggingface-cli upload bclermo/hermes-edge \
dist/hermes-mobile-v2.litertlm \
--repo-type model
# 13. Import URL in AI Edge Gallery:
# https://huggingface.co/bclermo/hermes-edge/resolve/main/hermes-mobile-v2.litertlm
```
---
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `ai-edge-torch` | β‰₯0.3.0 | PyTorch β†’ TFLite conversion |
| `litert-lm` | β‰₯0.1.0 | .litertlm bundler + runtime |
| `torch` | β‰₯2.4.0 | Reference model training |
| `sentencepiece` | β‰₯0.2.0 | Tokenizer |
| `numpy` | β‰₯1.26.0 | Array ops, sampling |
| `transformers` | (optional) | HF model loading for Qwen3 |
| `accelerate` | (optional) | CPU memory-efficient loading |
| `safetensors` | (optional) | Safe weight loading |
| `psutil` | β‰₯5.9.0 | Memory profiling |
| `tqdm` | β‰₯4.66.0 | Progress bars |
---
## Performance Model (Estimated)
### Without DSpark (Baseline)
| Stage | Time | Tok/s |
|-------|------|-------|
| Prefill (512 tok prompt) | ~2.5 s | 205 tok/s |
| Decode (100 tokens) | ~1.8 s | 55 tok/s |
| Total | ~4.3 s | β€” |
### With DSpark (k=5, 60% acceptance)
| Stage | Time | Tok/s |
|-------|------|-------|
| Prefill (512 tok prompt) | ~2.5 s | 205 tok/s |
| Draft decode (100 tokens ~ 20 drafts) | ~0.3 s | β€” |
| Main verify (20 verifications) | ~0.4 s | β€” |
| Total | ~3.2 s | β€” |
| **Effective decode** | β€” | **~140 tok/s** |
| **Speedup** | β€” | **2.5Γ—** |