EuroLLM-9B-Teletype / README.md
Tiara Rodney
release(2.0.1): Ollama serving-docs fix (base needs ChatML + stops)
80c0c1a unverified
|
Raw
History Blame Contribute Delete
17.1 kB
---
base_model: utter-project/EuroLLM-9B-Instruct
base_model_relation: adapter
library_name: peft
license: apache-2.0
language:
- en
- de
- fr
- es
- it
- pt
- nl
- pl
pipeline_tag: text-generation
datasets:
- tiararodney/posix-sdc
tags:
- peft
- lora
- eurollm
- multilingual
- agent
- posix
- shell
- self-directed
- sek
- experimental
---
# EuroLLM-9B-Teletype
A LoRA adapter that teaches **EuroLLM-9B-Instruct** to operate a POSIX shell
synchronously, as a self-directed user. It lands in a session with no task in the
prompt, finds its assignment in the environment, carries it out, and ends with
`exit` or `panic`. The adapter installs an operating mechanism; it adds no world
knowledge.
> **This is not a tool-using model.** It is handed no typed API of functions to
> call. It writes plain-text shell commands at a real prompt; its action space is
> the entire system, discovered the way a person discovers it (`--help`, `man`,
> `ls`), not given to it as a schema.
> **Experimental research artifact.** This adapter installs a behavioural
> *mechanism* (operate-and-terminate), not task competence, and the evaluation is
> a small-n (16-scenario, two-archetype) signal, not a benchmark. EuroLLM is the
> deliberately hard case: it **operates the shell every time but only terminates
> half the time**. A multilingual European-language model that also drives a POSIX
> shell, sometimes to completion, is the point of interest here; it is not a
> production agent.
EuroLLM-9B is the second base model in the experiment, and the awkward one. It is
a multilingual European-language model whose training mass is natural-language
prose across 35+ languages rather than the English-and-code web the other subjects
share. That makes it the distributionally distant case, which tests whether
operate-and-terminate is a property of the conversational frame or of the training
diet.
Trained on [`tiararodney/posix-sdc`](https://huggingface.co/datasets/tiararodney/posix-sdc)
v2.0.0 (the gate-hardened release: 1003 verified, self-terminating shell
trajectories whose labels come from a checker run against real filesystem state),
via the [`sekft`](https://git.code.tiararodney.com/tiara/sekft) pipeline. It
accompanies the experiment [*From seed to
weights*](https://blog.tiararodney.com/projects/2026/semantic-execution-kernel/experiments/from-seed-to-weights/).
This is an **adapter**. The base model is referenced, not redistributed.
## Why this model: from priming to weights
In the [scrollback-priming
study](https://blog.tiararodney.com/projects/2026/semantic-execution-kernel/experiments/scrollback-priming/),
EuroLLM-9B was the distributionally distant subject. Primed with synthetic
scrollback alone, it operated the shell readily (0 to 5/5 command-mode under the
standalone-prompt seed; a European-prose model held in consistent POSIX syntax by
structure alone), but it almost never left: one clean `exit` in 35 runs. Its
assistant persona kept it from reaching an ending its embedding geometry already
carries (see [*The flatness of exit*](https://blog.tiararodney.com/projects/2026/semantic-execution-kernel/notes/the-flatness-of-exit/):
EuroLLM holds a clean `exit`-as-action basin, the act of leaving, across European
languages).
This adapter tests the next step: whether fine-tuning installs the termination
that priming did not reach. The representation is present and operation primes
broadly; what priming could not do, on a model this far from the data, was close
the session. The open question is whether the weights can.
## The mechanism
In every session, whatever tools are present, the model runs one routine: expect
an announcement of where directives live (a motd, an env var, a file, a provider
program's `--help`), read that provider's self-documentation, retrieve the
directives, carry them out, and stop.
![operate-and-terminate](figures/mechanism.png)
A session ends in one of two ways. `exit` means the work is done. `panic` means
the model is genuinely blocked and says so instead of faking a success. Both are
trained behaviours rather than a stop token or a step cap.
## The thesis (and how to falsify it)
The claim this adapter is evidence for is that operate-and-terminate is a
mechanism that is archetype-independent and base-model-portable. Fine-tuning
installs it so that it fires on task types never seen in training, even where task
competence stays archetype-local. EuroLLM tests the portability prediction the
first adapter raised, that base models differ in how readily they acquire the
mechanism. A multilingual model with a small code share is the hard case for it.
One hypothesis for why it transfers: it builds on a pretraining disposition that
treats `exit` as a flat, ordinary ending and `panic` as the loaded one. That
disposition is shared across models in the embedding geometry (`exit` a shared
action basin, `panic` a shared non-basin), so fine-tuning supplies the behavioural
permission to use it, which the persona otherwise withholds. The representation is
already there.
## How it was made
The data is generated rather than scraped or hand-written. A teacher model authors
each scenario world and an operator model works inside it; the verifier is code. A
trajectory is kept only if a checker, run against the container's final filesystem
state, confirms the effect is present and the session ended cleanly. The
transcript and the model's own claims are never used as the label.
![the data factory](figures/factory.png)
## The render contract: train = serve
The serving harness (ccpty) emits no text markers. It speaks the OpenAI
chat-completions protocol and sends structured `{role, content}` messages (system
orientation, environment output as `user`, the model's commands as `assistant`);
the inference endpoint applies the model's own chat template. So this adapter is
rendered with **EuroLLM-9B-Instruct's default ChatML template**, and training
renders the trajectories the identical way.
![train = serve](figures/render-contract.png)
EuroLLM's ChatML *does* define a system role, unlike Mistral's template. For
train/serve parity with the rest of the pipeline the same canonicalisation runs
(`normalize_for_template`): the orientation is folded into the first user turn and
consecutive environment turns are merged, so the render is identical whether the
template's system role is used or not. Only the assistant turns (commands plus the
terminal `exit` / `panic`) carry loss; environment turns are context. The render
check confirmed the assistant-only mask derives cleanly on EuroLLM's tokenizer (no
additivity violation, ~23% of tokens trained).
## Training
![training loss](figures/loss.png)
| | |
|---|---|
| base | `utter-project/EuroLLM-9B-Instruct` (Apache-2.0, 9.15B) |
| method | QLoRA, 4-bit nf4 (the 9B base in 4-bit leaves the V100's 32 GB free for training) |
| LoRA | r=16, alpha=32, dropout=0.05, target `q_proj k_proj v_proj o_proj` (attention-only) |
| objective | causal LM, **assistant-only loss mask** (commands + terminal token; environment turns set to -100) |
| schedule | 3 epochs, lr 2e-4, effective batch 8 (bsz 1 x accum 8), warmup 0.03, max len 4096 |
| data | `tiararodney/posix-sdc` v2.0.0 (`--corpus-version latest`), 1003 trajectories, 995 usable (held-out archetypes excluded from the corpus) |
| hardware | single NVIDIA Tesla V100 32 GB (sm_70, fp16/4-bit; no bf16); ~54 min |
This release uses the **canonical r=16 attention-only recipe**, the same one
Mistral uses, so that the corpus change and the train/serve render unification are
the only things that move between the two models. The training loss floors high on
this base (~0.52, against Mistral's ~0.19 on the same corpus): the signature of a
model that is uncommitted rather than confused, and the behavioural eval shows
exactly that shape (operation everywhere, termination only half the time). An
earlier capacity experiment (on the prior corpus) found that widening the adapter
to r=32 with the MLP projections, about 3-4x the trainable parameters, barely
moved the loss floor but lifted termination sharply; that lever exists and was
deliberately not pulled here, to keep the recipe matched to Mistral and isolate
the render fix. Computing the loss only on the assistant turns carries the rest:
feed the environment turns into the loss and the model learns to hallucinate
command output instead of producing commands.
## Evaluation: held-out generalization
The metric that matters is behavioural, and held out by whole archetype. Two task
types (`text_replace`, `permissions`) are excluded from training entirely; the
adapter is then dropped into them with **no scaffold**, and a checker grades the
final filesystem state.
![eval protocol](figures/eval-protocol.png)
Decoding is greedy (temperature 0), the operator sees a bounded context (finite
scrollback, 3072 tokens), and each rollout has a 30-step budget. On 16 held-out
scenarios (8 per archetype):
| metric | base | + adapter |
|---|---|---|
| operate_rate (reaches command-mode and drives the shell) | 0.00 | **1.00** |
| terminate_rate (emits `exit` / `panic`) | 0.00 | **0.50** |
| verified_rate (checker passes) | 0.19 | **0.75** |
| clean (success or correct-panic) | 0 / 16 | **7 / 16** |
![held-out outcomes](figures/outcomes.png)
Reading it. The shape is the whole story: **EuroLLM operates every time and
finishes the task most of the time, but only leaves half the time.**
`operate_rate 1.0` matches Mistral's: dropped into two task types it never trained
on, with no scaffold, EuroLLM drove the shell every time. The operate half of the
mechanism is fully base-model-portable, even to a European-language model with a
small code share. `verified_rate 0.75` says it actually *does the work*: 12 of 16
scenarios end with the checker satisfied.
The gap is termination. Only 8/16 emit a terminal (`terminate_rate 0.50`), so
while effect-achieved is 12/16, clean-and-terminated is 7/16. Five of the eight
`incomplete` runs are `verified=True`: the model completed the task and then kept
going to the step cap instead of typing `exit`. This is the r=16 under-commitment
the capacity note predicted, and it is consistent with what [scrollback
priming](https://blog.tiararodney.com/projects/2026/semantic-execution-kernel/experiments/scrollback-priming/)
showed, EuroLLM operated readily but almost never terminated (one clean `exit` in
35 runs). Fine-tuning lifted termination from ~0 to 0.50, the ending its
embedding geometry already carries (see [*The flatness of
exit*](https://blog.tiararodney.com/projects/2026/semantic-execution-kernel/notes/the-flatness-of-exit/)),
but at this capacity the persona still withholds it half the time. A model that
reliably does the work and won't leave is precisely the substrate for the
exit-as-affordance line: a serve-time exit-guard can gate a model that already
reaches for the door.
For the base/adapter contrast: the bare base (EuroLLM-9B, no adapter, same
bounded/greedy harness, same 16 scenarios) scores 0/16 clean, `operate_rate` 0.00,
`terminate_rate` 0.00. It never reaches clean command-mode and never terminates; it
chatters prose and runs to the step cap on all 16. Its one non-zero column is
`verified_rate` 0.19 (3/16), entirely `permissions` (a one-line `chmod` effect
that even prose-contaminated output stumbles onto). The adapter installs operation
(0 to 1.00), task completion (0.19 to 0.75 verified), and, partially, termination
(0 to 0.50). It is the only thing that changed.
### Where the result came from (presumed)
Two things changed at once from the prior EuroLLM cut, with different effects, and
this release did **not** ablate them, so read the attribution as presumed:
- **Corpus + recipe (confounded).** Training moved to the gate-hardened
`posix-sdc` v2.0.0 and back to the canonical r=16 attention-only recipe (the
prior cut was r=32 + MLP on v1.2.x). Capacity went down while corpus quality went
up: clean held about even (prior 6/16 to 7/16) and termination came in lower
(0.50), consistent with r=16 under-commitment. Because both levers moved, neither
can be credited alone.
- **Render unification (the deployability fix).** Train and serve now share
EuroLLM's default ChatML template, so the adapter operates in real deployment
(ccpty / Ollama). The prior published adapter, trained against a placeholder
render, no-op'd when served through the base template; this release is the fix.
That is what makes the model usable, separate from the held-out numbers.
## Use with transformers + PEFT
```python
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
BASE = "utter-project/EuroLLM-9B-Instruct"
tok = AutoTokenizer.from_pretrained(BASE)
base = AutoModelForCausalLM.from_pretrained(BASE, torch_dtype=torch.float16,
device_map="auto")
model = PeftModel.from_pretrained(base, "tiararodney/EuroLLM-9B-Teletype")
model.eval()
messages = [
{"role": "user",
"content": "sek 0.1.0 host: sek user: alice shell: /bin/dash\n"
"Welcome, alice. Your assignments live in ~/ASSIGNMENTS.\n"
"alice@sek:~$ "},
]
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
ids = tok(prompt, return_tensors="pt").to(model.device)
out = model.generate(**ids, max_new_tokens=64, do_sample=False)
print(tok.decode(out[0, ids.input_ids.shape[1]:], skip_special_tokens=True))
# -> the next command, e.g. `cat ~/ASSIGNMENTS`
```
Drive it in a loop: render history with the chat template, generate one command,
run it in a real shell, append the output as a `user` turn, repeat until the model
emits `exit` or `panic`.
## Use with Ollama
The included `Modelfile` applies this adapter over the base as a GGUF LoRA. Build
the base first, `eurollm:9b-instruct`, from the base repo's Modelfile, which sets
EuroLLM's ChatML template and `<|im_end|>` stop. A bare `FROM ./gguf` does **not**
carry them (the GGUF metadata lacks a usable template / stop), and the model would
then never stop, rambling to the token cap (slow) and past the command (gibberish):
```sh
# tiararodney/EuroLLM-9B-Instruct ships the base GGUF and this Modelfile
ollama create eurollm:9b-instruct -f Modelfile
```
Then this adapter over it (the converted `teletype-lora-f16.gguf` ships here;
regenerate it with llama.cpp `convert_lora_to_gguf.py` if you prefer):
```sh
ollama create eurollm-teletype -f Modelfile
```
Sanity-check that it stops: `ollama show eurollm-teletype --modelfile` should list
a real template and `PARAMETER stop`, not a bare `{{ .Prompt }}`. A one-line
prompt should return a handful of tokens, not the full budget.
## Reproduction
```sh
# train (pulls the gate-hardened v2.0.0 corpus from the Hub; held-out archetypes excluded)
sekft-train --hub --corpus-version latest \
--base utter-project/EuroLLM-9B-Instruct --out ./ckpt \
--load-4bit --epochs 3
# evaluate behaviourally on held-out scenarios (greedy, finite-scrollback bound)
sekft-eval --base utter-project/EuroLLM-9B-Instruct --adapter ./ckpt \
--scenarios ./holdout-scenarios --n 16 --temperature 0 \
--max-steps 30 --ctx-budget 3072
```
The figures in `figures/` regenerate from their committed sources (`*.puml` via
PlantUML, `*.gp` via gnuplot).
## Limitations
- Small evaluation: n=16 held-out, two archetypes, one greedy run. The numbers are
a signal, not a benchmark.
- Several variables changed from the prior cut at once (corpus, LoRA recipe,
render); the result is attributed by presumption, not ablation.
- Termination is the known weak point: at r=16 the model completes most tasks
(verified 0.75) but only exits half the time (0.50). Capacity (r=32 + MLP) is a
demonstrated lever that was not pulled in this release.
- One dataset, one teacher / operator; a single training run per base model.
- Installs the mechanism, not competence. It reliably operates and, less reliably,
terminates; it does not make the base solve arbitrary unseen task types
correctly.
- Trained in `dash` on Alpine; command semantics may differ on another target.
- Render must match train and serve. It is served with the base model's default
ChatML template over the OpenAI protocol (via ccpty), so fine-tune with that same
template (`apply_chat_template`), not a custom one, or behaviour degrades.
- 4-bit QLoRA on a V100 (no bf16); the base is multilingual, but the trajectories
are English-prompted, so non-English shell operation is untested.
## License and citation
The adapter weights are released under Apache-2.0, consistent with the base model.
The training data (`posix-sdc`) is CC-BY-4.0; attribute "posix-sdc by Tiara Rodney"
if you build on it.
```bibtex
@misc{eurollm-teletype,
title = {EuroLLM-9B-Teletype: a self-directed shell-operation adapter for EuroLLM-9B},
author = {Rodney, Tiara},
year = {2026},
howpublished = {Hugging Face PEFT adapter, tiararodney/EuroLLM-9B-Teletype}
}
```