# 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, ..., , ..., , ..., ] │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ 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: 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 The answer is 132,678. ``` Key interface: ```python @dataclass class ReasoningConfig: enabled: bool = True think_tag: str = "" end_think_tag: str = "" 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 tags before answering. The pipeline: 1. Detects entry into mode 2. Collects thinking trace tokens 3. Detects exit into → 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 ... 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: {...} Parallel: {...} {...} """ ... 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: {"name": "calculator", "arguments": {"expression": "234*567"}} {"name": "web_search", "arguments": {"query": "current weather London"}} ``` The model is trained to emit parallel `` blocks inside a `` 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) -> 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) -> Void ) { let draftK = 5 var acceptedTokens: [Int] = [] while acceptedTokens.count < maxTokens { // Draft: run draft model autoregressively var draftTokens: [Int] = [] for _ in 0..", "234", " *", " 567" → verify → accept │ │ └── After ~20 tokens: reach "" │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 3. ReasoningPipeline.process_stream() │ │ ├── Detect tag → emit type="think" chunks │ │ ├── Detect tag → switch to type="answer" chunks │ │ └── Yield: ("think", "Let me break this down..."), ("answer", "I'll...") │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 4. Model emits: │ │ │ │ {"name":"calculator","arguments":{"expression":"234*567"}} │ │ {"name":"timer","arguments":{"duration":300,"unit":"seconds"}}│ │ │ ├─────────────────────────────────────────────────────────────────────────────┤ │ 5. AgentLoop._parse_tool_calls() │ │ ├── Extract 2 tool calls from 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×** |