--- license: apache-2.0 base_model: Qwen/Qwen3-0.6B library_name: transformers pipeline_tag: text-generation language: - en tags: - tool-calling - function-calling - qwen3 - lora - sft - trl - zynthetix --- # AL1-model-B — Qwen3-0.6B fine-tuned for native tool-calling A compact (0.6B-parameter) language model fine-tuned to **call tools reliably** while staying a capable general-chat assistant. It takes the Apache-2.0 [`Qwen/Qwen3-0.6B`](https://huggingface.co/Qwen/Qwen3-0.6B) base and applies a single **LoRA supervised fine-tune (SFT)** targeting native, Hermes-style `` emission over a small set of file/shell tools — then merges the adapter back into the base for a self-contained model. Part of the **Zynthetix AL-1** two-model lab (Model A = from-scratch NumPy LM; **Model B = improve an open-source small model**, this repo). Code: https://github.com/GruheshKurra/AL1-model-B --- ## Results — before → after Tool-calling is scored by **exact match** against a held-out 12-case set: `name_acc` = correct tool name chosen, `full_acc` = correct name **and** arguments. Both evaluated with greedy decoding, fp32, on the same device (no blending with chat scores). | Metric | Base `Qwen3-0.6B` | **AL1-model-B (SFT)** | Δ | |---|---|---|---| | Tool `name_acc` | 0.500 | **1.000** | +0.500 | | Tool `full_acc` | 0.500 | **0.917** (11/12) | +0.417 | | Chat coherence (10 probes) | strong | strong, terser | no regression | **Why the base scored only 0.5:** it *refused* to call `run_bash` and `edit_file` (0/2 each) and mis-handled one `list_dir` — its arguments were fine *when* it chose to call. The SFT data therefore over-weighted `edit_file` + `run_bash` + explicit anti-refusal examples, which closed the entire gap (`name_acc` → 1.0 = a valid tool is now always selected). > The merged fp16 model reproduces the adapter score exactly (`full_acc` 0.917) — merging lost nothing. --- ## The tools Five file/shell tools, described to the model via the OpenAI-style `tools=` schema and the Qwen3 native chat template (Hermes `` tags): | Tool | Purpose | Required args | |---|---|---| | `read_file` | Read a file's contents | `path` | | `write_file` | Create/overwrite a file | `path`, `content` | | `edit_file` | Replace an exact substring | `path`, `old_string`, `new_string` | | `list_dir` | List a directory | `path` | | `run_bash` | Run a shell command | `command` | The model **emits** tool name + arguments; wiring those to a real executor (or an MCP client) is a serving-layer concern, deliberately out of scope for the model itself. --- ## Usage ```python from transformers import AutoModelForCausalLM, AutoTokenizer import torch MODEL = "karthik-2905/AL1-model-B" tok = AutoTokenizer.from_pretrained(MODEL) model = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.float16).to("mps") # or "cuda"/"cpu" TOOLS = [{"type": "function", "function": { "name": "read_file", "description": "Read and return the contents of a file.", "parameters": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}}] # + the other 4 msgs = [{"role": "user", "content": "Show me what's in config.py"}] text = tok.apply_chat_template(msgs, tools=TOOLS, tokenize=False, add_generation_prompt=True, enable_thinking=False) ids = tok(text, return_tensors="pt").to(model.device) out = model.generate(**ids, max_new_tokens=128, do_sample=False) print(tok.decode(out[0][ids.input_ids.shape[1]:], skip_special_tokens=True)) # -> {"name": "read_file", "arguments": {"path": "config.py"}} ``` --- ## How it was built | Stage | Choice | |---|---| | Base | `Qwen/Qwen3-0.6B` (Apache-2.0) — GQA + RoPE + SwiGLU + RMSNorm | | PEFT | LoRA `r=16`, `α=32`, dropout 0.05, on all 7 attention+MLP projections | | Trainer | TRL `SFTTrainer`, `assistant_only_loss=True`, `max_length=512`, packing off | | Schedule | lr `3e-4` (cosine, 3% warmup), 3 epochs, effective batch 32, bf16 | | Hardware | RunPod **RTX A6000** (train), Apple Silicon / MPS (merge + eval) | | Merge | `merge_and_unload()` → standalone fp16 | **LR sweep** (10% data, 1 epoch) picked `3e-4` (eval_loss: `1e-4`=1.099, `2e-4`=0.999, `3e-4`=0.975). The full 3-epoch run overfits by *eval_loss* after ~epoch 1 (0.91 → 0.99), but generation quality and tool exact-match were best at 3 epochs — the **task metric beat the proxy loss**, so 3 epochs shipped. ### Data - **1,423 train / 74 val** examples, chat-formatted to the Qwen3 template, ~**2:1 tool:chat** ratio. - Tool examples teach the Hermes `` format across all 5 tools (over-weighting the two the base refused); chat examples guard against chat regression. - Train/val split is dedup'd and **leakage-checked against the eval set**. ### Evaluation - **Tool track (trust anchor):** exact-match over a 12-case set, scored by `mlb/eval_tools.py`. - **Chat track (regression guard):** 10 probes, currently eyeballed for coherence (LLM-judge = future work). --- ## Limitations - **Small model (0.6B):** capable within its size, not a substitute for a large instruct model. - **Chat quality is eyeballed,** not yet LLM-judge scored — no formal regression number. - **No general-capability guard** (MMLU/GSM8K) was run; broad-knowledge regression is unmeasured. - **4-bit quantization untested** — a 0.6B model can degrade more than the usual −1–2% at 4-bit. --- ## License **Apache-2.0**, inherited from the `Qwen/Qwen3-0.6B` base.