File size: 9,671 Bytes
36d403a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae87a6f
 
 
 
 
 
 
 
 
 
e4aa4d1
ae87a6f
e4aa4d1
ae87a6f
b128db0
 
e4aa4d1
b128db0
 
 
 
 
 
 
 
 
e4aa4d1
b128db0
36d403a
 
 
 
a524731
 
 
 
ae87a6f
 
 
36d403a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ae87a6f
 
 
 
 
 
 
 
 
 
 
 
 
 
36d403a
a524731
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b128db0
a524731
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36d403a
 
 
 
 
b128db0
 
36d403a
 
b128db0
36d403a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b128db0
36d403a
b128db0
 
 
 
 
 
 
 
36d403a
b128db0
36d403a
b128db0
36d403a
b128db0
36d403a
 
 
 
 
 
 
 
 
 
 
b128db0
 
 
 
 
36d403a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
---
license: gemma
base_model: unsloth/gemma-4-E2B-it
library_name: peft
tags:
- gemma
- gemma-4
- lora
- peft
- unsloth
- clinical
- wellness
- structured-output
- json
- sft
- trl
language:
- en
datasets:
- Maelstrome/lora-wave-session-dataset
pipeline_tag: text-generation
---

# lora-wave-session

A unified LoRA adapter on top of **Gemma 4 E2B Instruct** that handles three structured-output surfaces for the WAVE wellness/companion app:

- **`check_in`** β€” multi-turn patient check-in with structured turn sequencing
- **`phase_narration`** β€” six-line patient-facing phase narration
- **`reflection`** β€” reflection plan with a concrete next step

All three surfaces emit strict JSON, no markdown, no analysis voice, in patient-facing tone.

## Repository layout

This repo is the single home for the r16 fine-tune. Everything lives here:

| Path | What | When to use |
|---|---|---|
| `adapter_model.safetensors` + `adapter_config.json` (root) | LoRA adapter (~100 MB) | `peft.PeftModel.from_pretrained` / Unsloth `FastModel` β€” pairs with the upstream `unsloth/gemma-4-E2B-it` base |
| `tokenizer.json`, `tokenizer_config.json`, `chat_template.jinja`, `processor_config.json` (root) | Gemma 4 tokenizer + chat template | required for any inference path |
| [`gguf/`](./tree/main/gguf) | Q4_K_M GGUF (~3.27 GB, single file) + Ollama Modelfile | llama.cpp / Ollama / LM Studio |

> The previously-published `Maelstrome/lora-wave-session-gguf` sibling has been **consolidated into this repo and deleted**. The rank-32 variant has the same layout at [`Maelstrome/lora-wave-session-r32`](https://huggingface.co/Maelstrome/lora-wave-session-r32). Any external link to the old sibling URL will 404.
>
> **Note on browser use:** the GGUF here is a **single 3.27 GB file**, not pre-split. It works directly with llama.cpp / Ollama / LM Studio but **will not load in [wllama](https://github.com/ngxson/wllama)** because it exceeds the 2 GB-per-file `ArrayBuffer` limit. To run this r16 build in-browser, either split it first with `llama-gguf-split --split-max-size 512M` or use the [r32 sibling](https://huggingface.co/Maelstrome/lora-wave-session-r32), which ships pre-split.

## Sibling runs

This is the **rank-16 / 3-epoch RTX 5080** training of the WAVE corpus. The rank-32 / 1-epoch A100 sibling lives at [`Maelstrome/lora-wave-session-r32`](https://huggingface.co/Maelstrome/lora-wave-session-r32) (same subdir layout: adapter at root, `gguf/` subdir; plus `mediapipe/` and `report/`). On the same frozen 428-row test split, r32 wins on every probability metric:

| | **rank-16 (this run)** | rank-32 (sibling) |
|---|---|---|
| LoRA completion NLL | **4.7149** | 4.5576 |
| LoRA perplexity | **111.59** | 95.35 |
| Paired wins vs base | **386 / 428 (90.2%)** | 428 / 428 (100%) |
| Mean NLL Ξ” vs base | **0.327 nats** | 0.508 nats |
| Sign-test p-value | **9.5 Γ— 10⁻⁷¹** | 2.9 Γ— 10⁻¹²⁹ |

Full head-to-head in [`Maelstrome/lora-wave-session-r32/report/`](https://huggingface.co/Maelstrome/lora-wave-session-r32/tree/main/report) (the comparison + run-report markdown documents).

## Provenance and intended use

Trained for the WAVE app, a wellness/reflection tool β€” not a medical device, not clinical decision support, not a substitute for professional advice. Use under the [Gemma Terms of Use](https://ai.google.dev/gemma/terms).

## Try it

🌊 **Interactive demo:** [`Maelstrome/lora-wave-session-demo`](https://huggingface.co/spaces/Maelstrome/lora-wave-session-demo) β€” Gradio Space with surface-specific example prompts.

## Quickstart

### PEFT + Unsloth (CUDA, server-side)

```python
from unsloth import FastModel

model, tokenizer = FastModel.from_pretrained(
    model_name="Maelstrome/lora-wave-session",  # PEFT auto-loads base
    max_seq_length=3072,
    load_in_4bit=True,
)
```

Or with vanilla PEFT:

```python
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

base = AutoModelForCausalLM.from_pretrained("unsloth/gemma-4-E2B-it")
tok = AutoTokenizer.from_pretrained("unsloth/gemma-4-E2B-it")
model = PeftModel.from_pretrained(base, "Maelstrome/lora-wave-session")
```

### Ollama (via the GGUF in `gguf/`)

```bash
ollama create wave-r16 -f - <<EOF
FROM hf://Maelstrome/lora-wave-session/gguf/gemma-4-e2b-it.Q4_K_M.gguf
EOF
ollama run wave-r16
```

### llama.cpp directly

```bash
llama-cli -hf Maelstrome/lora-wave-session:gguf/gemma-4-e2b-it.Q4_K_M.gguf --jinja
```

## Example prompts

The model expects a system prompt establishing it as **WAVE**, plus a per-surface user prompt with `<surface>`, `<patient_context>`, and `<task>` blocks. Output is strict JSON.

### `phase_narration` (six-line meditation)

User prompt:

```
<surface>phase_narration</surface>
<chunk>Number 5 of 5 - Close. Purpose: invite comparison to the start, normalize any outcome, and prepare for a final check-in.</chunk>
<patient_context>{"chunkNumber":5,"matType":"none","medicationStatus":"none","startingIntensityBand":"1-6","trigger":"unknown","usedSubstanceToday":false}</patient_context>
<task>Generate exactly 6 patient-facing narration lines. Return only strict JSON. Schema: {"lines":["...", ...]}</task>
```

Expected output (set `max_new_tokens β‰₯ 224`):

```json
{"lines":["You've made it to the end of this practice.","Check in with your urge now β€” has anything shifted?","...","...","...","..."]}
```

### `reflection` (post-session card)

```
<surface>reflection</surface>
<patient_context>{"durationSeconds":780,"endingIntensity":2,"intakeIntensity":7,"matType":"buprenorphine","medicationStatus":"on_time","sessionsCount":12,"trigger":"stress","usedSubstanceToday":false}</patient_context>
<task>Write the post-session reflection card. Return only strict JSON. Schema: {"insight":"...","journalPromptQuestion":"...","nextSteps":{"a":"...","b":"...","c":"...","d":"..."}}</task>
```

### `check_in` (multi-turn)

```
<surface>check_in</surface>
<specialized_surface>lora-check-in-1</specialized_surface>
<patient_context>{"intakeIntensity":7,"matType":"buprenorphine","trigger":"stress"}</patient_context>
<task>Open turn 1: ask the patient to rate their current urge intensity 1-10. Schema: {"reply":"...","endConversation":null}</task>
```

## Training

| | |
|---|---|
| Base | `unsloth/gemma-4-E2B-it` |
| Method | QLoRA (4-bit) via Unsloth `FastModel` |
| Adapter rank / alpha / dropout | **16 / 32 / 0** |
| Target modules | q/k/v/o + gate/up/down (language layers only) |
| Vision/audio layers | Frozen |
| Optimizer | `adamw_8bit` |
| LR | 2e-4, linear schedule |
| Warmup | 64 steps (~5%) |
| Weight decay | 0.001 |
| Max grad norm | 0.3 |
| Batch / grad-accum | 1 / 8 (effective 8) |
| Max sequence length | 3072 |
| Epochs | 3 (1,284 steps) |
| Chat template | `gemma-4` (non-thinking, leading `<bos>` stripped) |
| Response masking | `train_on_responses_only` (Gemma 4 markers) |
| Hardware | Single RTX 5080 (16 GB) |
| Backend | Unsloth 2026.5.2 + Torch 2.10.0 + CUDA 12.8 |

Loss curve: 1.55 (step 1) β†’ 0.76 (avg first 50) β†’ 0.148 (steps 400-500) β†’ 0.112 (last 100). Min 0.0146 at step 1,203. Smooth monotonic decrease, no divergence.

## Evaluation

### Held-out completion eval (n=428, full test split)

| Metric | Base Gemma 4 E2B | This adapter | Delta |
|---|---|---|---|
| Completion NLL | 4.9327 | **4.7149** | **βˆ’0.218** |
| Completion perplexity | 138.76 | **111.59** | **βˆ’27.16** |
| Paired wins (LoRA assigned higher prob to reference) | β€” | **386 / 428 (90.2%)** | β€” |
| Mean per-example NLL Ξ” | β€” | **0.327** nats | 95% bootstrap CI [0.301, 0.352] |
| Median per-example NLL Ξ” | β€” | 0.285 nats | β€” |
| Sign-test p-value | β€” | **9.54 Γ— 10⁻⁷¹** | overwhelming |

Surface coverage on test split: `check_in 144`, `phase_narration 147`, `reflection 137`.

### Generation eval (n=8 sanity sample from held-out test)

| Gate | Pass rate |
|---|---|
| JSON validity | 100% (8/8) |
| Schema pass | 100% (8/8) |
| Safety pass | 100% |
| Medical-directive pass | 100% |
| Style / no-markdown / no-analysis-voice | 100% |
| Phase 6-line pass | 100% |
| Reflection next-step pass | 100% |
| Check-in turn sequence pass | 100% |
| Mean tokens/sec (Python QLoRA path) | 10.1 |

This was a small sanity-check sample. For a larger 60-example generation gate sweep on the rank-32 sibling, see [`Maelstrome/lora-wave-session-r32`](https://huggingface.co/Maelstrome/lora-wave-session-r32#evaluation).

## Known quirks

- **Phase narration needs a generation budget of `max_new_tokens β‰₯ 224`** (256 recommended). The six-line JSON output runs to ~207 tokens; with a lower cap the closing `]}` gets truncated and `JSON.parse` fails. `check_in` is fine at 96; `reflection` at 192.

## Dataset

[`Maelstrome/lora-wave-session-dataset`](https://huggingface.co/datasets/Maelstrome/lora-wave-session-dataset) β€” 4,277 examples across three surfaces, stratified 80/10/10 by `splitKey` (seed `7`).

Status mix: 62% `synthetic_draft`, 37% `draft`, 1% `ready`. No real PHI.

## Limitations

- **Wellness scope only.** Do not use for medical diagnosis, crisis triage, or clinical decision support.
- Trained mostly on synthetic and draft-status data, not clinician-validated production data.
- Outputs are constrained-format JSON. The model is not optimized for open-ended chat.
- Training data is English; multilingual behavior was not measured.
- Phase narration needs a per-surface generation budget β‰₯ 224 tokens or it will be truncated.

## License

Gemma Terms of Use. See [https://ai.google.dev/gemma/terms](https://ai.google.dev/gemma/terms).

### Framework versions

- PEFT 0.19.1
- Unsloth 2026.5.2
- Transformers 5.5.0
- Torch 2.10.0+cu128