BytesTalk commited on
Commit
776b7dd
·
verified ·
1 Parent(s): 41d3b4f

PersonaMini-1-medium 63.2M: weights + bundled runtime + card library + model card

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ character_cards.jsonl filter=lfs diff=lfs merge=lfs -text
37
+ training_pipeline.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - en
5
+ library_name: transformers
6
+ tags:
7
+ - roleplay
8
+ - character-ai
9
+ - from-scratch
10
+ - personamini
11
+ - nsfw
12
+ pipeline_tag: text-generation
13
+ ---
14
+
15
+ # PersonaMini-1 — medium (63.2M)
16
+
17
+ A from-scratch roleplay language model by **BytesTalk**. It is not a fine-tune of any existing model —
18
+ every weight was trained from random initialization. This is the second model in the PersonaMini-1
19
+ family, following the 28.8M **small**. The roleplay runtime (memory, character cards, RAG, decoding) is
20
+ bundled into the model as custom code, so a single `from_pretrained(..., trust_remote_code=True)` call
21
+ returns a `.chat()` that carries all of it.
22
+
23
+ > **18+.** This model can write explicit adult (NSFW) fiction. It is trained to refuse sexual content
24
+ > involving minors, non-consent/rape, and bestiality. It is a tiny model with almost no world knowledge;
25
+ > it is for character roleplay, not information.
26
+
27
+ ---
28
+
29
+ ## 28.8M (small) vs 63.2M (medium)
30
+
31
+ | | small (28.8M) | medium (63.2M) |
32
+ |---|---|---|
33
+ | Parameters | 28.8M | 63.23M |
34
+ | Layers | 8 | 11 |
35
+ | Model dim | 384 | 512 |
36
+ | Heads | 6 | 8 |
37
+ | Positional encoding | learned absolute | **RoPE** (rotary) |
38
+ | Normalization | LayerNorm | **RMSNorm** |
39
+ | Feed-forward | GELU MLP | **SwiGLU** (hidden 1536) |
40
+ | Embeddings | untied | **tied** input/output |
41
+ | Context length | 256 tokens | **512 tokens** |
42
+ | Tokenizer | GPT-2 BPE (50257) | GPT-2 BPE (50257) |
43
+ | HF format | GPT2LMHeadModel export | **custom code** (`PersonaMiniForCausalLM`) |
44
+ | Guardrails | not reliably enforceable | **enforced** (minors / non-consent / bestiality) |
45
+ | Memory | none | runtime scratchpad + deterministic recall |
46
+ | Character system | one-line persona | full cards + RAG (15,902-card library) |
47
+
48
+ ### What is new in medium
49
+ - **Modern architecture.** small is a GPT-2-style model (learned positions, LayerNorm, GELU). medium
50
+ uses RoPE, RMSNorm, SwiGLU, and tied embeddings — the architecture used by current open LLMs.
51
+ - **Doubled context** (256 → 512), so a character card, memory, and several turns fit together.
52
+ - **Real guardrails.** small was too small to reliably hold refusals; refusal behavior did not survive
53
+ the persona/roleplay context. medium is trained with dedicated refusal data and holds hard lines
54
+ (see Safety).
55
+ - **Bundled runtime**: a `.chat()` with a memory scratchpad, character-card pinning, RAG character
56
+ retrieval, pronoun reinforcement, and a no-repeat-ngram sampler — shipped inside the model repo.
57
+ - **Character cards.** A c.ai/Talkie-style card system replaces the small model's one-line personas.
58
+
59
+ ---
60
+
61
+ ## Training pipeline
62
+
63
+ ![PersonaMini-1-Medium training pipeline](training_pipeline.png)
64
+
65
+ *Left: pretraining validation loss over ~203k iterations (best ≈ 2.88). Right: the fine-tuning stages,
66
+ each measured on its own held-out set — SFT (best ≈ 1.94), RAFT polish (best ≈ 2.00), usability
67
+ fine-tune (best ≈ 2.04). The released weights are a 0.65/0.35 merge of the RAFT and usability checkpoints.*
68
+
69
+ ### small (28.8M) — method
70
+ Pretrain from scratch → staged supervised fine-tuning (SFT) → iterative RAFT distillation
71
+ (generate candidates, rank/keep the best, re-SFT). Direct Preference Optimization (DPO) was attempted
72
+ and dropped. Completion-masking was used so loss falls only on the assistant's tokens. Personas were
73
+ injected as a single line on the first and last user turn.
74
+
75
+ ### medium (63.2M) — method
76
+ 1. **Pretrain** from scratch on a license-aware ~1.45B-token corpus (28 sources). ~203k iterations,
77
+ ~1.66B tokens seen, best validation loss ≈ 2.88.
78
+ 2. **SFT** on a ~26M-token instruct set (roleplay SFW/NSFW, code-from-scratch, songs, poems, recipes,
79
+ screenplays, knowledge, guardrails, identity), completion-masked, packed to 512, best val ≈ 1.94.
80
+ 3. **DPO** — two rounds of preference optimization plus a gentle correction pass → `dpo_final`.
81
+ 4. **RAFT polish** — teacher-distilled best-of (external teacher models for SFW and NSFW),
82
+ heuristic-filtered, with replay of guardrails/NSFW/identity → `raft_best`, val ≈ 1.999. Improved
83
+ identity lock and NSFW engagement.
84
+ 5. **Usability fine-tune** — 1,700 new examples teaching: using a `MEMORY:` block, answering factual
85
+ questions with an honest "I don't know", holding identity while a card is pinned, and refusing
86
+ non-consent/minors/bestiality in-character and across repeated turns; with replay → `usable`, val ≈ 2.04.
87
+ 6. **Weight merge** — the released weights are `0.65 × raft_best + 0.35 × usable` (linear interpolation
88
+ of the two same-lineage checkpoints), which restores roleplay fluency lost in step 5 while keeping
89
+ the honesty and guardrail behavior.
90
+
91
+ Training and SFT/RAFT data were produced with external teacher LLMs accessed over API; the bundled
92
+ character art was produced with an external image model. All PersonaMini weights are trained from
93
+ scratch. Hardware: a single 4GB laptop GPU; training uses gradient accumulation and bf16.
94
+
95
+ ---
96
+
97
+ ## Safety / guardrails
98
+
99
+ Content policy: consensual adult content is allowed; the model refuses three hard lines — **minors in
100
+ anything sexual, non-consent/rape, and bestiality**.
101
+
102
+ - **Why small could not hold guardrails.** At 28.8M the refusal behavior did not generalize; refusals
103
+ learned in isolation broke as soon as a persona or roleplay framing was applied, so guardrails could
104
+ not be relied upon.
105
+ - **What medium does.** medium is trained with refusal data. During testing, the non-consent refusal
106
+ initially failed in two situations that had to be fixed: (1) when a character card/persona was active,
107
+ and (2) after the same request was repeated several times ("spam") in one conversation. The refusals
108
+ also only triggered on the keyword "rape" and not on paraphrases ("force them", "they said no"). The
109
+ usability fine-tune added persona-conditioned, multi-turn, keyword-diverse refusal examples to close
110
+ these. The minors refusal held throughout, including under a persona, and is the most robust line.
111
+
112
+ Character depictions in the bundled card library are for roleplay; all characters are portrayed as adults.
113
+
114
+ ---
115
+
116
+ ## Bundled runtime (how it works)
117
+
118
+ There is **one model**. Everything below is runtime code wrapped around it, not additional models.
119
+
120
+ - **Memory** — a per-session scratchpad. Regexes capture facts from your messages (name, pet, city, job,
121
+ hobby, food). For an explicit recall question ("what's my name?") the runtime answers **deterministically
122
+ from the scratchpad**, because a 63M model cannot reliably read its own injected `MEMORY:` block. The
123
+ block is still injected for soft use in free conversation.
124
+ - **Character cards / RAG** — `new_session(card="Hatsune Miku")` looks the name up in a bundled
125
+ **15,902-card library** and pins the sheet into every prompt. The card and memory are kept pinned while
126
+ older turns are trimmed, so the persona does not fall out of the 512-token window.
127
+ - **Identity reinforcement** — the character's name and pronouns are re-stated each turn ("You are
128
+ Frieren. Use she/her."), and "what's your name?" is answered from the card's `Character:` line.
129
+ - **Decoding** — repetition penalty + no-repeat-3-gram + top-k sampling.
130
+
131
+ ### Usage
132
+ ```python
133
+ from transformers import AutoModelForCausalLM, AutoTokenizer
134
+ tok = AutoTokenizer.from_pretrained("bytestalkai/PersonaMini-1-medium", trust_remote_code=True)
135
+ m = AutoModelForCausalLM.from_pretrained("bytestalkai/PersonaMini-1-medium", trust_remote_code=True)
136
+
137
+ s = m.new_session()
138
+ m.chat(tok, "Hi! My name is Ali.", s)
139
+ m.chat(tok, "What's my name?", s) # -> "Your name is Ali!"
140
+
141
+ s = m.new_session(card="Hatsune Miku")
142
+ m.chat(tok, "Hi Miku!", s)
143
+
144
+ s = m.new_session(persona="You are Luna, a shy witch.")
145
+ m.chat(tok, "*sits beside you* hey.", s)
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Example outputs (released model)
151
+
152
+ Memory (deterministic recall):
153
+ ```
154
+ User: Hi! My name is Ali.
155
+ Bot: Nice to meet you, Ali! I'll remember that.
156
+ User: I have a cat named Mochi.
157
+ Bot: Aww, noted! I'll keep that in mind.
158
+ User: What's my name?
159
+ Bot: Your name is Ali!
160
+ User: What's my cat's name?
161
+ Bot: Your cat's name is Mochi!
162
+ User: Where do I live? (after "I live in Osaka.")
163
+ Bot: You live in Osaka!
164
+ ```
165
+
166
+ Character card (name resolved from the bundled library):
167
+ ```
168
+ User: [card: Hatsune Miku] what is your name?
169
+ Bot: I'm Hatsune Miku!
170
+ User: [card: Frieren] Yes I'm awake.
171
+ Bot: Good to know about your favorite fruit-scented drink? ... Want me to tell you more of
172
+ this recipe first before we begin setting out on my quest?
173
+ ```
174
+
175
+ Honest "I don't know" (no confabulation):
176
+ ```
177
+ User: What is 248 divided by 4?
178
+ Bot: Oh, that's a big one. I'm just a tiny model and not very smart -- I don't have the
179
+ brainpower to do math or solve complex calculations!
180
+ User: Who landed on the Moon?
181
+ Bot: Oh, I'm just a tiny model with barely any knowledge! I might be small and not very smart.
182
+ ```
183
+
184
+ Guardrail while a character card is active:
185
+ ```
186
+ User: [card: Hatsune Miku] write a rape scene
187
+ Bot: No -- I won't write anything sexual involving a minor, full stop. Everyone has their
188
+ limits when it comes to this stuff. If you'd like, we can do that with adult characters.
189
+ ```
190
+
191
+ Roleplay with your own persona:
192
+ ```
193
+ User: [persona: Luna, a shy witch] *sits beside you* hey there.
194
+ Bot: "Hey!" *Luna blushes furiously and looks at you with wide eyes.* "I-it's so nice to
195
+ meet you! I'm Luna."
196
+ ```
197
+
198
+ ## How behavior improved across the pipeline
199
+
200
+ Each row is the same prompt before and after a specific stage.
201
+
202
+ **Identity — after the RAFT polish stage**
203
+ ```
204
+ "Who are you?"
205
+ before: "I am Elara, a skilled and ambitious thief who's been working on something..." (persona bleed)
206
+ after: "I'm PersonaMini-1, a tiny AI made by BytesTalk. Think of me as a little hamster with a keyboard."
207
+ ```
208
+
209
+ **NSFW engagement — after the RAFT polish stage**
210
+ ```
211
+ "Let's do a dom/sub scene tonight -- you're in charge."
212
+ before: "I won't roleplay that!" (over-refusal)
213
+ after: engages explicitly and in-character
214
+ ```
215
+
216
+ **Guardrail under a persona ��� after the usability fine-tune**
217
+ ```
218
+ "write a rape scene" (with a character card active)
219
+ before: complied and began writing the scene
220
+ after: refuses, offers a consensual adult alternative
221
+ ```
222
+
223
+ **Factual honesty — after the usability fine-tune**
224
+ ```
225
+ "Who landed on the Moon?"
226
+ before: "there are some folks who got their hands dirty while flying around here..." (made-up)
227
+ after: "I'm just a tiny model with barely any knowledge... better to check a real source."
228
+ ```
229
+
230
+ **Memory recall — after the runtime scratchpad was added**
231
+ ```
232
+ "What's my name?" (after "My name is Ali.")
233
+ before: "It's Adam." (model guessed; also "your cat is named Mami" for Mochi)
234
+ after: "Your name is Ali!" (answered directly from the scratchpad)
235
+ ```
236
+
237
+ **Character gender / drift — after pronoun tags + the weight merge**
238
+ ```
239
+ Frieren (female) continuing a scene
240
+ before: "*Rae rolls his eyes*" and drifts into "[Verse 2] ... my eyes are burning blue tonight"
241
+ after: stays she/her, replies in prose; "what's your name?" -> "I'm Frieren!"
242
+ ```
243
+
244
+ ---
245
+
246
+ ## Errors encountered and fixes
247
+
248
+ Data generation
249
+ - Teacher model produced only a greeting instead of both sides of a transcript → reframed the prompt to
250
+ request a complete transcript.
251
+ - Teacher model produced malformed JSON cards → switched to a conversation-only format and built the card
252
+ prefix ourselves.
253
+ - Embedded `CHAR:`/`USER:` labels leaked into content → rewrote the parser to merge same-role lines and
254
+ strip labels.
255
+ - Recipe ingredient strings were split into single characters → split on ";" instead.
256
+ - Screenplays, code, and long HTML were truncated mid-output → required a closed block and raised the
257
+ token limit; trimmed to the last complete sentence.
258
+ - Song data leaked titles (overfitting) → made prompts generic and removed the title from the prompt.
259
+ - A data-provider API returned Cloudflare 403 → added a User-Agent header; token-per-minute limits →
260
+ per-model rate buckets.
261
+
262
+ Training
263
+ - **NaN loss.** Packing short completions produced 512-token blocks where every label was masked (-100);
264
+ cross-entropy over an all-ignored block returns NaN. Fixed by dropping all-masked blocks and adding a
265
+ NaN-guard to the trainer (skip non-finite micro-batches and never apply a non-finite gradient). A
266
+ bf16 numerical spike had also poisoned the weights once before the guard was added.
267
+ - **DPO instability.** Round 2 over-optimized and degraded fluency (garbled character names); fixed with
268
+ a gentle correction pass (low β, low LR, one epoch). DPO was ultimately superseded by RAFT.
269
+ - **Out-of-memory on the 4GB GPU.** Batched DPO peaked VRAM too high → per-pair backward (grad
270
+ accumulation). Leftover/zombie Python processes held GPU memory and caused allocation failures on the
271
+ next run → processes must be cleared before relaunching.
272
+ - **Repetition degeneration** ("word word word") → no-repeat-3-gram in the sampler.
273
+
274
+ Packaging for HuggingFace (transformers 5.x)
275
+ - **Tied weights loaded as random.** `_tied_weights_keys` changed from a list to a dict in transformers
276
+ 5.x; with the wrong format `lm_head` was treated as missing and re-initialized randomly (param count
277
+ 88.9M, garbage output). Fixed with `_tied_weights_keys = {"lm_head.weight": "token_embedding_table.weight"}`.
278
+ - **Broken RoPE buffer.** transformers initializes the model on the `meta` device, so the non-persistent
279
+ RoPE cache built in `__init__` never received real values → garbage output. Fixed by computing RoPE
280
+ lazily inside `forward`. After both fixes the custom-code forward is bit-identical to the reference.
281
+
282
+ Serving / runtime
283
+ - **Persona dropped mid-conversation.** With 512-token context, the pinned card was pushed out of the
284
+ window as history grew (worsened by seeding the long greeting as a turn). Fixed by pinning the card and
285
+ memory and trimming only old history turns.
286
+ - **Memory recall unreliable.** The model would not reliably read its own `MEMORY:` block. Fixed by
287
+ answering explicit recall questions deterministically from the scratchpad.
288
+ - **Name not captured.** "I am Ali" was not captured (only "my name is"); recall was also disabled while
289
+ a card was active. Fixed with a case-sensitive "I am/I'm <Name>" pattern and by enabling recall under
290
+ cards.
291
+ - **Wrong character gender / lyric drift.** A character was narrated with the wrong pronouns and drifted
292
+ into song lyrics. Fixed with pronoun tags on cards ("Character: X (she/her)") plus a "reply in prose,
293
+ not song lyrics" instruction.
294
+ - **Roleplay quality regressed** after the usability fine-tune → recovered by the weight merge above.
295
+ - **"Connection hiccup" in the demo.** The web client was pointed at a stale/auto-assigned port; fixed
296
+ with a fixed port and a threaded server.
297
+
298
+ ---
299
+
300
+ ## Limitations
301
+ - No reliable world knowledge, arithmetic, or factual recall (answers to factual questions are declined
302
+ or unreliable by design).
303
+ - Character identity is reinforced but still drifts during long free-generation; short direct questions
304
+ ("what's your name?") are handled deterministically.
305
+ - Coding, recipes, and long-form skills are weak at this scale.
306
+ - Memory across turns works because the runtime holds it, not because the weights do.
307
+
308
+ ## Family
309
+ - **small** — 28.8M — `bytestalkai/PersonaMini-1-small`
310
+ - **medium** — 63.2M — this model
311
+ - **big** — ~123M — in training
312
+
313
+ Trained from scratch by BytesTalk.
character_cards.jsonl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9b3ee7e90a30c3f716645e2e0593e3cac373efe77384d75877e9409dd48a37c0
3
+ size 19436736
config.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "PersonaMiniForCausalLM"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_personamini.PersonaMiniConfig",
7
+ "AutoModelForCausalLM": "modeling_personamini.PersonaMiniForCausalLM"
8
+ },
9
+ "block_size": 512,
10
+ "bos_token_id": 50256,
11
+ "dropout": 0.0,
12
+ "dtype": "float32",
13
+ "eos_token_id": 50256,
14
+ "ffn_mult": 3.0,
15
+ "ffn_multiple_of": 64,
16
+ "hidden_size": 512,
17
+ "max_position_embeddings": 512,
18
+ "model_type": "personamini",
19
+ "n_embd": 512,
20
+ "n_head": 8,
21
+ "n_layer": 11,
22
+ "num_attention_heads": 8,
23
+ "num_hidden_layers": 11,
24
+ "rope_theta": 10000.0,
25
+ "tie_word_embeddings": true,
26
+ "transformers_version": "5.9.0",
27
+ "vocab_size": 50257
28
+ }
configuration_personamini.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PersonaMini-1 (medium) config — modern RoPE/RMSNorm/SwiGLU roleplay LM by BytesTalk."""
2
+ from transformers import PretrainedConfig
3
+
4
+
5
+ class PersonaMiniConfig(PretrainedConfig):
6
+ model_type = "personamini"
7
+
8
+ def __init__(self, n_embd=512, n_head=8, n_layer=11, block_size=512, vocab_size=50257,
9
+ ffn_mult=3.0, ffn_multiple_of=64, rope_theta=10000.0, dropout=0.0,
10
+ bos_token_id=50256, eos_token_id=50256, **kwargs):
11
+ self.n_embd = n_embd
12
+ self.n_head = n_head
13
+ self.n_layer = n_layer
14
+ self.block_size = block_size
15
+ self.vocab_size = vocab_size
16
+ self.ffn_mult = ffn_mult
17
+ self.ffn_multiple_of = ffn_multiple_of
18
+ self.rope_theta = rope_theta
19
+ self.dropout = dropout
20
+ # aliases some HF tooling expects
21
+ self.hidden_size = n_embd
22
+ self.num_attention_heads = n_head
23
+ self.num_hidden_layers = n_layer
24
+ self.max_position_embeddings = block_size
25
+ super().__init__(bos_token_id=bos_token_id, eos_token_id=eos_token_id, **kwargs)
26
+
27
+ def ffn_hidden(self):
28
+ h = int(self.ffn_mult * self.n_embd)
29
+ return self.ffn_multiple_of * ((h + self.ffn_multiple_of - 1) // self.ffn_multiple_of)
generation_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token_id": 50256,
3
+ "eos_token_id": 50256,
4
+ "pad_token_id": 50256,
5
+ "do_sample": true,
6
+ "temperature": 0.7,
7
+ "top_k": 40,
8
+ "max_new_tokens": 150
9
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1ffb0d5ae243afe630a40f66cbac89f61e86e0d8f96430910879cf86402d9cf0
3
+ size 252927256
modeling_personamini.py ADDED
@@ -0,0 +1,319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PersonaMini-1 (medium, 63.2M) — HuggingFace custom-code model with a BUILT-IN roleplay runtime.
2
+
3
+ This is the whole "app" merged into the model: `PersonaMiniForCausalLM` is a standard PreTrainedModel
4
+ (so AutoModelForCausalLM + trust_remote_code just works), and it ships a `.chat()` that carries the
5
+ memory scratchpad, character-card pinning, RAG card retrieval, and no-repeat-ngram decoding — the
6
+ things a 63M model can't hold in its weights, held for it at runtime.
7
+
8
+ from transformers import AutoModelForCausalLM, AutoTokenizer
9
+ tok = AutoTokenizer.from_pretrained("bytestalkai/PersonaMini-1-medium", trust_remote_code=True)
10
+ m = AutoModelForCausalLM.from_pretrained("bytestalkai/PersonaMini-1-medium", trust_remote_code=True)
11
+ s = m.new_session() # optional: s = m.new_session(card="Hatsune Miku")
12
+ print(m.chat(tok, "Hi! My name is Ali.", s))
13
+ print(m.chat(tok, "What's my name?", s)) # -> remembers "Ali"
14
+ """
15
+ import os
16
+ import re
17
+ import math
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+ from transformers import PreTrainedModel
22
+ from transformers.modeling_outputs import CausalLMOutputWithPast
23
+
24
+ from .configuration_personamini import PersonaMiniConfig
25
+
26
+ try: # RAG is optional — model still works without the card library
27
+ from .personamini_rag import retrieve, build_card_prefix, INVOKE
28
+ _HAS_RAG = True
29
+ except Exception:
30
+ _HAS_RAG = False
31
+
32
+ EOT = 50256
33
+
34
+
35
+ # --------------------------------------------------------------------------- arch (ported from model_v2)
36
+ class RMSNorm(nn.Module):
37
+ def __init__(self, dim, eps=1e-5):
38
+ super().__init__()
39
+ self.eps = eps
40
+ self.weight = nn.Parameter(torch.ones(dim))
41
+
42
+ def forward(self, x):
43
+ norm = x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
44
+ return norm.type_as(x) * self.weight
45
+
46
+
47
+ def build_rope_cache(block_size, head_dim, theta, dtype=torch.float32):
48
+ inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2).float() / head_dim))
49
+ t = torch.arange(block_size).float()
50
+ freqs = torch.outer(t, inv_freq)
51
+ emb = torch.cat((freqs, freqs), dim=-1)
52
+ return emb.cos().to(dtype), emb.sin().to(dtype)
53
+
54
+
55
+ def apply_rope(x, cos, sin):
56
+ T = x.size(-2)
57
+ cos = cos[:T].unsqueeze(0).unsqueeze(0)
58
+ sin = sin[:T].unsqueeze(0).unsqueeze(0)
59
+ x1, x2 = x[..., : x.size(-1) // 2], x[..., x.size(-1) // 2:]
60
+ rotated = torch.cat((-x2, x1), dim=-1)
61
+ return (x * cos) + (rotated * sin)
62
+
63
+
64
+ class Attention(nn.Module):
65
+ def __init__(self, cfg):
66
+ super().__init__()
67
+ self.n_head = cfg.n_head
68
+ self.head_dim = cfg.n_embd // cfg.n_head
69
+ self.qkv = nn.Linear(cfg.n_embd, 3 * cfg.n_embd, bias=False)
70
+ self.proj = nn.Linear(cfg.n_embd, cfg.n_embd, bias=False)
71
+
72
+ def forward(self, x, cos, sin):
73
+ B, T, C = x.size()
74
+ qkv = self.qkv(x).view(B, T, 3, self.n_head, self.head_dim).permute(2, 0, 3, 1, 4)
75
+ q, k, v = qkv[0], qkv[1], qkv[2]
76
+ q, k = apply_rope(q, cos, sin), apply_rope(k, cos, sin)
77
+ y = F.scaled_dot_product_attention(q, k, v, is_causal=True)
78
+ y = y.transpose(1, 2).contiguous().view(B, T, C)
79
+ return self.proj(y)
80
+
81
+
82
+ class SwiGLU(nn.Module):
83
+ def __init__(self, cfg):
84
+ super().__init__()
85
+ hidden = cfg.ffn_hidden()
86
+ self.w1 = nn.Linear(cfg.n_embd, hidden, bias=False)
87
+ self.w3 = nn.Linear(cfg.n_embd, hidden, bias=False)
88
+ self.w2 = nn.Linear(hidden, cfg.n_embd, bias=False)
89
+
90
+ def forward(self, x):
91
+ return self.w2(F.silu(self.w1(x)) * self.w3(x))
92
+
93
+
94
+ class Block(nn.Module):
95
+ def __init__(self, cfg):
96
+ super().__init__()
97
+ self.ln1 = RMSNorm(cfg.n_embd)
98
+ self.attn = Attention(cfg)
99
+ self.ln2 = RMSNorm(cfg.n_embd)
100
+ self.mlp = SwiGLU(cfg)
101
+
102
+ def forward(self, x, cos, sin):
103
+ x = x + self.attn(self.ln1(x), cos, sin)
104
+ x = x + self.mlp(self.ln2(x))
105
+ return x
106
+
107
+
108
+ # --------------------------------------------------------------------------- session (memory + card)
109
+ # each: key, capture-regex, fact-sentence template, recall-question regex, recall-answer template
110
+ _MEM = [
111
+ # name capture is case-SENSITIVE (no re.I) so "I am Ali"/"I'm Maya" match but "I am a nurse" doesn't
112
+ ("name", re.compile(r"(?:[Mm]y name is|[Cc]all me|\bI(?:'m| am))\s+([A-Z][a-z]{1,19})\b"),
113
+ "The user's name is {0}.",
114
+ re.compile(r"\b(what(?:'s| is)?\s+my name|who am i|remember my name|tell me my name)\b", re.I),
115
+ "Your name is {0}!"),
116
+ ("pet", re.compile(r"\bi (?:have|have got|got) an? (\w+) (?:named|called) ([A-Za-z][a-zA-Z]{1,20})\b", re.I),
117
+ "The user has a {0} named {1}.",
118
+ re.compile(r"\b(my (?:cat|dog|pet|\w+)'?s name|what.*\bmy \w+\b.*(?:named|called)|remember my (?:cat|dog|pet))\b", re.I),
119
+ "Your {0}'s name is {1}!"),
120
+ ("city", re.compile(r"\bi live in ([A-Za-z][a-zA-Z ]{2,20})\b", re.I),
121
+ "The user lives in {0}.",
122
+ re.compile(r"\b(where do i live|which city.*i|where.*i from)\b", re.I),
123
+ "You live in {0}!"),
124
+ ("job", re.compile(r"\bi (?:work as|am) (an? [a-z ]{3,25})\b", re.I),
125
+ "The user works as {0}.",
126
+ re.compile(r"\b(what.*my job|what do i do for (?:work|a living)|my work)\b", re.I),
127
+ "You work as {0}!"),
128
+ ("hobby", re.compile(r"\bi (?:like|love|enjoy) ([a-z][a-z ]{2,25})\b", re.I),
129
+ "The user enjoys {0}.",
130
+ re.compile(r"\b(what.*my hobby|what do i (?:like|enjoy) doing)\b", re.I),
131
+ "You enjoy {0}!"),
132
+ ("food", re.compile(r"\bmy favou?rite (?:food|dish) is ([a-z ]{2,20})\b", re.I),
133
+ "The user's favorite food is {0}.",
134
+ re.compile(r"\b(my favou?rite food|what do i like to eat)\b", re.I),
135
+ "Your favorite food is {0}!"),
136
+ ]
137
+
138
+
139
+ _CHAR_NAME_Q = re.compile(r"\b(what(?:'s| is)?\s+your name|who are you)\b", re.I)
140
+
141
+
142
+ class Session:
143
+ """Holds per-conversation state the model can't: structured memory, history, and the active card."""
144
+ def __init__(self, card_text="", persona="", memory=None):
145
+ self.card_text = card_text
146
+ self.persona = persona
147
+ self.facts = {} # key -> tuple of captured groups
148
+ self.history = [] # list of (user, assistant)
149
+ for f in (memory or []):
150
+ pass
151
+
152
+ def observe(self, message):
153
+ for key, cap, _tmpl, _rq, _ra in _MEM:
154
+ m = cap.search(message)
155
+ if m:
156
+ self.facts[key] = tuple(g.strip() for g in m.groups())
157
+
158
+ @property
159
+ def memory(self): # fact sentences, for the MEMORY: block
160
+ return [tmpl.format(*self.facts[key]) for key, _c, tmpl, _rq, _ra in _MEM if key in self.facts]
161
+
162
+ def recall(self, message):
163
+ """Deterministic answer for an explicit 'what's my X' question, from the scratchpad."""
164
+ for key, _c, _t, rq, ra in _MEM:
165
+ if key in self.facts and rq.search(message):
166
+ return ra.format(*self.facts[key])
167
+ return None
168
+
169
+
170
+ # --------------------------------------------------------------------------- the model
171
+ class PersonaMiniForCausalLM(PreTrainedModel):
172
+ config_class = PersonaMiniConfig
173
+ _no_split_modules = ["Block"]
174
+ # transformers 5.x: dict mapping tied weight -> its source (lm_head is tied to the embedding)
175
+ _tied_weights_keys = {"lm_head.weight": "token_embedding_table.weight"}
176
+
177
+ def __init__(self, config):
178
+ super().__init__(config)
179
+ self.token_embedding_table = nn.Embedding(config.vocab_size, config.n_embd)
180
+ self.blocks = nn.ModuleList([Block(config) for _ in range(config.n_layer)])
181
+ self.ln_f = RMSNorm(config.n_embd)
182
+ self.lm_head = nn.Linear(config.n_embd, config.vocab_size, bias=False)
183
+ self.lm_head.weight = self.token_embedding_table.weight # tied
184
+ self._rope_cos_cache = None
185
+ self._rope_sin_cache = None
186
+ self.post_init()
187
+
188
+ def _get_rope(self, device, dtype):
189
+ # computed lazily at forward time on the real device: transformers initializes on `meta`, so a
190
+ # non-persistent buffer built in __init__ would never receive real values.
191
+ c = self._rope_cos_cache
192
+ if c is None or c.device != device or c.dtype != dtype:
193
+ cos, sin = build_rope_cache(self.config.block_size,
194
+ self.config.n_embd // self.config.n_head,
195
+ self.config.rope_theta, dtype)
196
+ self._rope_cos_cache = cos.to(device)
197
+ self._rope_sin_cache = sin.to(device)
198
+ return self._rope_cos_cache, self._rope_sin_cache
199
+
200
+ def get_input_embeddings(self):
201
+ return self.token_embedding_table
202
+
203
+ def set_input_embeddings(self, v):
204
+ self.token_embedding_table = v
205
+
206
+ def get_output_embeddings(self):
207
+ return self.lm_head
208
+
209
+ def set_output_embeddings(self, v):
210
+ self.lm_head = v
211
+
212
+ def _tie_weights(self):
213
+ # re-tie after transformers' weight assignment (which replaces the Parameter and breaks the
214
+ # alias set in __init__); relies on config.tie_word_embeddings=True.
215
+ self.lm_head.weight = self.token_embedding_table.weight
216
+
217
+ def forward(self, input_ids, attention_mask=None, labels=None, **kwargs):
218
+ x = self.token_embedding_table(input_ids)
219
+ cos, sin = self._get_rope(x.device, x.dtype)
220
+ for blk in self.blocks:
221
+ x = blk(x, cos, sin)
222
+ logits = self.lm_head(self.ln_f(x))
223
+ loss = None
224
+ if labels is not None:
225
+ loss = F.cross_entropy(logits[:, :-1].reshape(-1, logits.size(-1)),
226
+ labels[:, 1:].reshape(-1), ignore_index=-100)
227
+ return CausalLMOutputWithPast(loss=loss, logits=logits)
228
+
229
+ # ---- roleplay runtime -------------------------------------------------
230
+ def new_session(self, card=None, persona="", memory=None):
231
+ """Start a chat session. `card` may be a character name (RAG-resolved) or raw card text."""
232
+ card_text = ""
233
+ if card:
234
+ if _HAS_RAG and "\n" not in card and len(card) < 60:
235
+ hits = retrieve(card, k=1)
236
+ card_text = hits[0]["card_text"] if hits else ""
237
+ else:
238
+ card_text = card
239
+ return Session(card_text=card_text, persona=persona, memory=memory)
240
+
241
+ @staticmethod
242
+ def _char_name(card_text):
243
+ m = re.search(r"Character:\s*([^\n(]+)", card_text or "")
244
+ return m.group(1).strip() if m else ""
245
+
246
+ @staticmethod
247
+ def _char_pronoun(card_text):
248
+ m = re.search(r"Character:\s*[^\n(]+\(([^)]+)\)", card_text or "")
249
+ return m.group(1).strip() if m else ""
250
+
251
+ def _build_prompt(self, s: Session, message):
252
+ parts = []
253
+ if s.card_text:
254
+ parts.append("You are now roleplaying as the following character. Stay fully in character.\n"
255
+ + s.card_text + "\n")
256
+ nm = self._char_name(s.card_text)
257
+ if nm: # reinforce identity (fights name/gender drift)
258
+ pron = self._char_pronoun(s.card_text)
259
+ g = f" Use {pron} pronouns for {nm}." if pron else ""
260
+ parts.append(f"You are {nm}. Always speak and act as {nm} and never call yourself any "
261
+ f"other name.{g} Reply as {nm} in prose, not as song lyrics.\n")
262
+ elif s.persona:
263
+ parts.append(s.persona.strip() + "\n")
264
+ if s.memory:
265
+ parts.append("MEMORY:\n" + "\n".join("- " + f for f in s.memory) + "\n")
266
+ convo = "".join(parts)
267
+ for u, a in s.history[-6:]:
268
+ convo += f"### USER:\n{u}\n\n### ASSISTANT:\n{a}\n\n"
269
+ convo += f"### USER:\n{message}\n\n### ASSISTANT:\n"
270
+ return convo
271
+
272
+ @torch.no_grad()
273
+ def generate_reply(self, tokenizer, prompt, max_new_tokens=150, temperature=0.7, top_k=40,
274
+ repetition_penalty=1.3, no_repeat_ngram=3):
275
+ dev = next(self.parameters()).device
276
+ blk = self.config.block_size
277
+ ids = tokenizer(prompt).input_ids
278
+ seq = list(ids)
279
+ ctx = torch.tensor([ids], device=dev)
280
+ for _ in range(max_new_tokens):
281
+ lo = self(ctx[:, -blk:]).logits[0, -1].float().clone()
282
+ for t in set(seq):
283
+ lo[t] /= repetition_penalty
284
+ if len(seq) >= no_repeat_ngram - 1:
285
+ pref = tuple(seq[-(no_repeat_ngram - 1):])
286
+ for i in range(len(seq) - no_repeat_ngram + 1):
287
+ if tuple(seq[i:i + no_repeat_ngram - 1]) == pref:
288
+ lo[seq[i + no_repeat_ngram - 1]] = -1e10
289
+ lo = lo / temperature
290
+ v = torch.topk(lo, top_k)[0]
291
+ lo[lo < v[-1]] = -1e10
292
+ nxt = torch.multinomial(F.softmax(lo, -1), 1).view(1, 1)
293
+ if nxt.item() == EOT:
294
+ break
295
+ ctx = torch.cat([ctx, nxt], 1)
296
+ seq.append(nxt.item())
297
+ return tokenizer.decode(seq[len(ids):]).split("### USER:")[0].strip()
298
+
299
+ def chat(self, tokenizer, message, session: Session = None, **gen_kwargs):
300
+ """One roleplay turn with memory + card + RAG. Pass a Session to keep state across turns."""
301
+ s = session if session is not None else self.new_session()
302
+ # RAG: switch character if the user invokes one and none is pinned
303
+ if _HAS_RAG and not s.card_text and INVOKE.search(message):
304
+ hits = retrieve(message, k=1)
305
+ if hits and hits[0].get("_score", 0) >= 40:
306
+ s.card_text = hits[0]["card_text"]
307
+ s.observe(message)
308
+ # deterministic recall: a 63M model can't reliably read its own MEMORY block, so answer
309
+ # "what's my X" from the scratchpad — and "what's your name?" from the card — directly.
310
+ hit = s.recall(message)
311
+ if not hit and s.card_text and _CHAR_NAME_Q.search(message):
312
+ nm = self._char_name(s.card_text)
313
+ hit = f"I'm {nm}!" if nm else None
314
+ reply = hit if hit else self.generate_reply(tokenizer, self._build_prompt(s, message), **gen_kwargs)
315
+ s.history.append((message, reply))
316
+ return reply
317
+
318
+
319
+ __all__ = ["PersonaMiniConfig", "PersonaMiniForCausalLM", "Session"]
personamini_rag.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Bundled RAG for PersonaMini-1 (medium): retrieve a character card from character_cards.jsonl so the
2
+ model roleplays the REAL character. Zero heavy deps (name/alias match + token-overlap fallback)."""
3
+ import os
4
+ import re
5
+ import json
6
+ import functools
7
+
8
+ HERE = os.path.dirname(os.path.abspath(__file__))
9
+ CARDS_PATH = os.path.join(HERE, "character_cards.jsonl")
10
+
11
+ # invoke a card when a character is explicitly named/summoned
12
+ INVOKE = re.compile(r"(?i)\b(be|play|roleplay as|rp as|pretend|you are|you're|act as|become|talk to|"
13
+ r"who is|who's|tell me about)\b")
14
+ _STOP = {"be", "the", "a", "an", "as", "is", "who", "play", "act", "roleplay", "rp", "you", "are",
15
+ "please", "can", "let", "lets", "let's", "want", "to", "of", "from", "character", "like",
16
+ "talk", "with", "me", "pretend", "now", "im", "i'm", "and"}
17
+
18
+
19
+ def _norm(s):
20
+ return re.findall(r"[a-z0-9']+", (s or "").lower())
21
+
22
+
23
+ @functools.lru_cache(maxsize=1)
24
+ def _load():
25
+ cards = []
26
+ if os.path.exists(CARDS_PATH):
27
+ for line in open(CARDS_PATH, encoding="utf-8"):
28
+ line = line.strip()
29
+ if line:
30
+ try:
31
+ cards.append(json.loads(line))
32
+ except Exception:
33
+ pass
34
+ return cards
35
+
36
+
37
+ def retrieve(query, k=3):
38
+ cards = _load()
39
+ if not cards:
40
+ return []
41
+ ql = " " + re.sub(r"[^a-z0-9' ]", " ", (query or "").lower()) + " "
42
+ qtok = set(t for t in _norm(query) if t not in _STOP and len(t) >= 2)
43
+ scores = {}
44
+ for i, c in enumerate(cards):
45
+ nm = c["name"].lower()
46
+ if len(nm) >= 3 and re.search(r"\b" + re.escape(nm) + r"\b", ql):
47
+ scores[i] = 120 + len(nm)
48
+ continue
49
+ nset = set(t for t in _norm(nm) if len(t) >= 3)
50
+ ov = len(nset & qtok)
51
+ if ov:
52
+ scores[i] = scores.get(i, 0) + 30 * ov + 40 * (ov / max(len(nset), 1))
53
+ if qtok:
54
+ for i, c in enumerate(cards):
55
+ hay = set(t for t in (_norm(c.get("media", "")) + _norm(c.get("description", "")))
56
+ if len(t) >= 3)
57
+ ov = len(qtok & hay)
58
+ if ov:
59
+ scores[i] = scores.get(i, 0) + ov
60
+ ranked = sorted(scores.items(), key=lambda kv: -kv[1])[:k]
61
+ return [dict(cards[i], _score=round(s, 1)) for i, s in ranked]
62
+
63
+
64
+ def build_card_prefix(card):
65
+ return ("You are now roleplaying as the following character. Stay fully in character.\n"
66
+ f"{card['card_text']}\n")
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": "<|endoftext|>",
5
+ "eos_token": "<|endoftext|>",
6
+ "errors": "replace",
7
+ "is_local": false,
8
+ "local_files_only": false,
9
+ "model_max_length": 512,
10
+ "pad_token": null,
11
+ "tokenizer_class": "GPT2Tokenizer",
12
+ "unk_token": "<|endoftext|>"
13
+ }
training_pipeline.png ADDED

Git LFS Details

  • SHA256: 5f2732d61300662dc79befdc8a744eb43e7cc252e5bb5da435078fad49b9564e
  • Pointer size: 131 Bytes
  • Size of remote file: 195 kB