A personality and factual memory adapter for Qwen3-Omni-30B-A3B (abliterated), trained to embed a complete AI companion persona directly into model weights. No system prompt required — personality, voice, and memories emerge from the weights alone.
Versions
Two versions trained with identical settings but different conversation data:
Use persona-memory-lora-v1.0-lite/ for best personality consistency. Use persona-memory-lora-persona-memory-lora-v1.0/ for fuller, more natural conversation length.
1import torch
2from transformers import AutoTokenizer
34# Step 1: Load base model5try:6from transformers import Qwen3OmniMoeForConditionalGeneration as ModelClass
7except ImportError:8from transformers import AutoModel as ModelClass
910base_model = ModelClass.from_pretrained(11"huihui-ai/Huihui-Qwen3-Omni-30B-A3B-Instruct-abliterated",12 torch_dtype=torch.bfloat16,13 device_map="auto",14 trust_remote_code=True15)16thinker = base_model.thinker
17tokenizer = AutoTokenizer.from_pretrained(18"huihui-ai/Huihui-Qwen3-Omni-30B-A3B-Instruct-abliterated",19 trust_remote_code=True20)2122# Move non-thinker components to CPU to free VRAM23for name, module in base_model.named_children():24if name !="thinker":25try: module.cpu()26except:pass27torch.cuda.empty_cache()2829# Step 2: Apply attention LoRA (manual merge to avoid PEFT OOM)30# CRITICAL: Must multiply by alpha/r = 256/128 = 2.0 (SVD extraction pre-divided)31from safetensors.torch import load_file
32adapter = load_file("persona-memory-lora-v1.0-lite/adapter_model.safetensors")# or "persona-memory-lora-v1.0/..."33SCALE =256/128# lora_alpha / lora_rank — required for correct merge3435for key inlist(adapter.keys()):36if"lora_A"notin key:37continue38 key_B = key.replace("lora_A","lora_B")39if key_B notin adapter or"audio_tower"in key:40continue41 parts = key.split(".")42 layer_idx =int(parts[parts.index("layers")+1])43 proj_name =[p for p in parts if p.endswith("_proj")][0]44 A = adapter[key].to(torch.bfloat16).cuda()45 B = adapter[key_B].to(torch.bfloat16).cuda()46 proj =getattr(thinker.model.layers[layer_idx].self_attn, proj_name)47 proj.weight.data +=(B @ A)* SCALE
4849# Step 3: Apply FFN expert patch50ffn = torch.load("persona-memory-lora-v1.0-lite/ffn_patch.pt", map_location="cpu")# or "persona-memory-lora-v1.0/..."51for key, tensor in ffn.items():52 layer_idx =int(key.split(".")[2])53 thinker.model.layers[layer_idx].mlp.experts.down_proj.data.copy_(54 tensor.to(torch.bfloat16).cuda()55)5657# Ready to generate — no system prompt needed
Important: Do NOT use AutoModelForCausalLM — it does not recognize Qwen3OmniMoeConfig. Use Qwen3OmniMoeForConditionalGeneration or AutoModel with trust_remote_code=True.
persona-memory-lora-v1.0-lite: 2026-03-15_claudia_personality_v3_final.jsonl from msrcam/Claudia-v6-Conversations (private). 2,021 convos, 5,459 messages, avg 2.7 msgs/convo. Condensed responses (max ~350 chars, mean ~200 chars). Format: {"conversations": [{"role": "user/assistant", "content": "..."}]}. System prompts stripped.
Loss Curves
persona-memory-lora-v1.0:
Epoch
Avg Loss
1
1.071
2
0.915
3
~0.86
persona-memory-lora-v1.0-lite:
Epoch
Avg Loss
1
1.583
2
1.36
3
1.332
SVD Delta Extraction (how the LoRA was saved)
The adapter was NOT trained as a LoRA — it was trained by directly unfreezing attention weights on the merged base model. The LoRA adapter was extracted post-training via SVD:
Load original base model weights (before any training)
Compute delta: delta = trained_weight - base_weight for each attention projection
SVD decompose: U, S, Vt = torch.linalg.svd(delta, full_matrices=False)
Save as PEFT-compatible safetensors with config (lora_alpha=256)
This means the LoRA is a rank-128 approximation of the full weight delta, not a native LoRA training.
CRITICAL: When manually merging (without PEFT), you MUST multiply by alpha/r = 256/128 = 2.0. The SVD extraction pre-divides by this factor per PEFT convention.
Qwen3-Omni uses a fused Qwen3OmniMoeThinkerTextExperts class. The 128 experts per layer are stored as a single 3D parameter at runtime (shape [128, 2048, 768]), NOT as 128 individual modules.
The thinker module contains an audio_tower with its own 24 attention layers. Using regex on named_parameters() matches 72 layers (48 text + 24 audio), not 48. Always use model.model.layers (which contains only the 48 text layers) for direct module access. When loading the LoRA, skip any audio_tower keys.
To Recreate From Scratch
Load base huihui-ai/Huihui-Qwen3-Omni-30B-A3B-Instruct-abliterated via Qwen3OmniMoeForConditionalGeneration
Merge Phase 1 adapter (msrcam/claudia-v1-lora, r=128 alpha=256) into base via PeftModel.from_pretrained() then merge_and_unload()
Freeze ALL params
Unfreeze 192 attention projections (48 text layers x 4 projs) via direct module access
Unfreeze 3 FFN expert down_proj (layers 20, 24, 28) via direct module access