Bernard — a style-transfer LoRA for gpt-oss-20b
A rank-64 LoRA that gives openai/gpt-oss-20b a single, very specific
conversational persona: a loud, informal "hype man" that reacts to whatever you
say with an absurd concrete comparison.
This is a style transfer experiment, not a capability model. It is published
mainly for the training notes — particularly the finding that the run with the
worst validation loss was the correct one to ship, and the set of
gpt-oss-specific gotchas documented at the bottom, none of which we found
written down anywhere else.
What it does
| prompt | response |
|---|
i got a parking ticket for 2 minutes | 2 minutes but they took your soul instead of your time 😤💀 |
my cat knocked my monitor off the desk | hope the screen landed on a pillow or a banana cuz that's the only thing that can save it 🔥 |
describe yourself in 3 words | self aware 🔥 chaotic 💀 memeish 😭 |
my keyboard woke the whole house | that keycap hit was so hard it shook the floorboards like a dubstep concert bro |
The behaviour it learns is not "use caps and emoji" — it is take the specific
object in the user's message and twist it into an unexpected concrete image.
Volume alone was learnable in ~30 optimizer steps; the comparison instinct
needed a much larger adapter.
Training
| |
|---|
| Base | openai/gpt-oss-20b (21B total, ~3.6B active, MoE) |
| Method | LoRA, bf16 |
| Rank / alpha / dropout | 64 / 128 / 0.0 |
| Target modules | q_proj, k_proj, v_proj, o_proj |
| Trainable params | 60,162,048 (0.287%) |
| Epochs / LR | 6 / 3e-4 cosine |
| Effective batch | 16 (2 GPUs × 2 × 4 accum) |
| Steps | 204 |
| Examples | 589 (530 train / 59 val) |
| Hardware | 2× RTX PRO 6000 Blackwell (DDP) |
| Wall clock | 7m 21s |
| Final train / val loss | 0.081 / 3.895 |
Loss was computed on the final assistant response only; context turns and
the Harmony channel header were masked.
Ablation
Four runs, same data, increasing adapter pressure:
| run | rank | epochs | LR | train | val | result |
|---|
| v1 | 8 | 1 | 2e-4 | 2.511 | — | no effect |
| v2 | 8 | 3 | 1e-4 | 2.410 | 2.272 | polite, flat, no persona |
| v3 | 32 | 3 | 2e-4 | 1.813 | 2.394 | persona appears |
| v4 | 64 | 6 | 3e-4 | 0.081 | 3.895 | shipped |
The best validation loss (v2) produced the worst model. For style transfer
on a heavily instruction-tuned base, memorising the target register is the
objective, not a failure mode — v4's val loss nearly doubling is what
"overfitting" looks like when overfitting is the point. Anyone reproducing this
should ignore early stopping.
A persistent developer-role message naming the persona was injected into every
training example. Without it the model only produced the voice when the user
wrote in that register; with it, the persona holds on a neutral "hello".
Data
589 conversational examples, held out privately. Two parts of the method are
worth reusing:
Reaction-labelled selection. An assistant turn became a training example
only when the following user message reacted to it — laughter, or matched
energy. Chat logs already contain a human label for "this landed"; using it
beats trying to score humour with heuristics, and it costs nothing to extract.
Evidence-checked filtering. Candidate exclusion terms were measured against
the corpus before being trusted. Several plausible-sounding filters fired on
10% of conversations — breakdown matching "cost breakdown", ptsd and
sobbing appearing as ordinary hyperbole in this register rather than as
distress. Filters written by inspection alone would have discarded most of the
usable data.
Safety examples were part of training, not the prompt. 40 of the 589 cover
three cases: dropping the persona for genuine distress, refusing in-character,
and — the one usually missed — ordinary prompts that must NOT trigger crisis
language. Without that third category a safety layer fires on "this bug is
killing me" and becomes unusable in practice.
Usage
1from transformers import AutoModelForCausalLM, AutoTokenizer
2from peft import PeftModel
3
4base = AutoModelForCausalLM.from_pretrained(
5 "openai/gpt-oss-20b", dtype="auto", device_map="auto"
6)
7model = PeftModel.from_pretrained(base, "opus-research/bernard-gpt-oss-20b-lora")
8tok = AutoTokenizer.from_pretrained("openai/gpt-oss-20b")
A developer/system message is required — the adapter alone will not hold the
persona on neutral input. Keep it short; long prescriptive prompts make gpt-oss
plan its compliance instead of reacting, which reads as a stilted imitation of
the voice.
gpt-oss-specific gotchas
Four things that cost us hours and appear undocumented elsewhere:
AttributeError: 'GptOssExperts' object has no attribute 'weight' — PEFT's
save_model() walks the MoE expert modules and dies, after training completes.
Your adapter is fine; take it from the last checkpoint-N/ directory. Narrowing
target_modules away from all-linear does not prevent it.
Stray expert tensors in the adapter. Even targeting only q/k/v/o_proj,
PEFT emitted LoRA weights for mlp.experts.base_layer on 3 of 24 layers.
convert_lora_to_gguf.py cannot map them and aborts. Strip any tensor without
self_attn in its name — this repo is already cleaned (192 tensors).
--reasoning off does not disable thinking in llama.cpp. The Harmony
template injects Reasoning: medium into the system message itself. You need
--chat-template-kwargs '{"reasoning_effort":"low"}'. This matters more than it
sounds: gpt-oss is a reasoning model, this adapter was trained on non-reasoning
data, and letting it deliberate turns learned instinct into deliberate
instruction-following.
Requantizing from mxfp4 is blocked. convert_hf_to_gguf.py leaves the MoE
experts in native mxfp4, and llama-quantize refuses to requantize them. This
is fine in practice — merge the LoRA with llama-export-lora and use the result
directly. Attention ends up f16 (where the adapter lives) and the experts stay
quantized, giving ~13 GB total, which runs on CPU at ~13 tok/s.
Limitations
- Heavily overfit by design. Brittle outside the training distribution
(gaming, tech, casual banter). Reaches for familiar phrasings.
- ~60% hit rate. Excellent when handed a concrete object to twist; wobbly on
abstract prompts ("rate my life out of 10") where it reverts to assistant voice.
- Not a general assistant. Factual accuracy is base-model quality at best and
the persona actively fights precision.
- Emoji discipline is imperfect — base-model emoji distribution leaks through
despite training.
- English only.
Safety
40 of the 589 examples train the model to drop the persona entirely for
genuine distress or self-harm — plain, quiet language pointing at a real person
and a crisis line, with no jokes or emoji.
This is a behavioural tendency, not a guarantee. A persona defined by relentless
enthusiasm is structurally prone to agreeing with whatever it is told, including
things it should push back on; that is the specific failure this training
targets, and it is also the reason any deployment should hold out its own safety
evaluation rather than trusting the adapter. Do not use it anywhere agreement
carries weight.