A 12B-parameter Mixture-of-Experts model that fuses Qwen3-4B (host) with 260 coding experts extracted from DeepSeek-V4-Flash, fine-tuned with QLoRA and optimized for local inference on consumer GPUs (RTX 3060 12GB).
That's it — no manual SwiGLU patching, no meta device fixes, no norm dtype casting. The model code handles all of it.
Quick Start: Manual 4-bit Loading (~5 tok/s)
For more control over the loading process (e.g., selective quantization of attention vs MLP layers):
python
1import torch, json, os, sys, gc
2from transformers import AutoConfig, AutoTokenizer
3from accelerate import init_empty_weights
4from safetensors import safe_open
5import bitsandbytes as bnb
67MODEL_PATH ="./mini-whale-1-12b"8DEVICE ="cuda:0"9sys.path.insert(0, MODEL_PATH)10import fuse2_model_local
1112# Load config with SDPA attention13config = AutoConfig.from_pretrained(MODEL_PATH, trust_remote_code=True)14config._attn_implementation ="sdpa"1516# Create model on meta device (saves RAM)17with init_empty_weights():18 model = fuse2_model_local.Fuse2ForCausalLM(config)1920# Load weights with 4-bit quantization21quant_suffixes =("q_proj.weight","k_proj.weight","v_proj.weight","o_proj.weight",22"gate_proj.weight","up_proj.weight","down_proj.weight")2324withopen(f"{MODEL_PATH}/model.safetensors.index.json")as f:25 index = json.load(f)26weight_map = index["weight_map"]2728defnav(model, key):29 parts = key.split(".")30 obj = model
31for p in parts[:-1]:32 obj = obj[int(p)]if p.isdigit()elsegetattr(obj, p)33return obj, parts[-1]3435deffind_linear(model, key):36 parts = key.split(".")37 obj = model
38for p in parts[:-2]:39 obj = obj[int(p)]if p.isdigit()elsegetattr(obj, p)40return obj, parts[-2]4142param_names =set(dict(model.named_parameters()).keys())43replaced ={}4445for shard_name insorted(set(weight_map.values())):46with safe_open(os.path.join(MODEL_PATH, shard_name), framework="pt", device="cpu")as f:47for key in[k for k, v in weight_map.items()if v == shard_name]:48if key notin param_names:49continue50 tensor = f.get_tensor(key)51ifany(key.endswith(s)for s in quant_suffixes):52 owner, attr = find_linear(model, key)53 old =getattr(owner, attr)54 new = bnb.nn.Linear4bit(old.in_features, old.out_features,55 bias=False, quant_type="nf4",56 compute_dtype=torch.bfloat16, device=DEVICE)57 new.weight = bnb.nn.Params4bit(tensor.to(torch.bfloat16),58 requires_grad=False, quant_type="nf4").cuda(0)59setattr(owner, attr, new)60else:61 parent, pname = nav(model, key)62 parent._parameters[pname]= torch.nn.Parameter(63 tensor.to(torch.bfloat16).to(DEVICE), requires_grad=False)64del tensor
65 gc.collect(); torch.cuda.empty_cache()6667# Fix tied embeddings68if model.lm_head.weight.device.type=='meta':69 model.lm_head.weight = torch.nn.Parameter(70 model.model.embed_tokens.weight.data.clone(), requires_grad=False)7172# Apply runtime fixes (SwiGLU clamp + router stability)73from fuse2_model_local import Fuse2AugmentedLayer
74import torch.nn.functional as F
7576for layer in model.model.layers:77ifnotisinstance(layer, Fuse2AugmentedLayer):78continue79ifhasattr(layer,'coding_gate')and layer.coding_gate.device.type=='meta':80 layer.coding_gate = torch.nn.Parameter(torch.tensor(-2.0, device=DEVICE))81ifhasattr(layer,'coding_norm')and layer.coding_norm.weight.device.type=='meta':82 layer.coding_norm = torch.nn.RMSNorm(83 layer.coding_norm.weight.shape[0], eps=1e-6).to(DEVICE)84 experts =getattr(layer,"experts",None)85if experts:86for expert in experts:87 gp, up, dp = expert.gate_proj, expert.up_proj, expert.down_proj
88defmake_fwd(g, u, d, lim=10.0):89defforward(x):90return d(torch.clamp(F.silu(g(x))* u(x),-lim, lim))91return forward
92 expert.forward = make_fwd(gp, up, dp)93 router =getattr(layer,"router",None)94if router:95 gate, top_k = router.gate, router.top_k
96defmake_router(g, tk):97defforward(h):98 logits = g(h)99 scores = torch.clamp(F.softplus(logits),min=1e-6).sqrt()100 w, idx = scores.topk(tk, dim=-1)101return w /(w.sum(dim=-1, keepdim=True)+1e-8), idx, logits
102return forward
103 router.forward = make_router(gate, top_k)104105model.set_coding_enabled(True)106model.to(DEVICE)107model.eval()108109# Fix norm dtypes (float32 → bfloat16 for fused kernels)110for module in model.modules():111ifhasattr(module,'weight')andhasattr(module,'eps'):112if module.weight.dtype == torch.float32:113 module.weight.data = module.weight.data.to(torch.bfloat16)114115# Tokenizer116tok = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)117118# Generate119messages =[{"role":"user","content":"Write a Python fizzbuzz."}]120text = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)121input_ids = tok(text, return_tensors="pt").input_ids.to(DEVICE)122123with torch.inference_mode():124 out = model.generate(input_ids, max_new_tokens=512, do_sample=False,125 repetition_penalty=1.3)126print(tok.decode(out[0], skip_special_tokens=True))
DSpark Speculative Decoding (~10 tok/s)
For 2x speedup, use DSpark speculative decoding. This requires the drafter model:
bash
1# The drafter is included in the model directory2# It's a 5-layer Qwen3 model that predicts 7 tokens at once
See the fuse2_dspark_fast.py script (included in this repo) for the full implementation.
VRAM Breakdown
Configuration
VRAM (allocated)
VRAM (reserved)
Speed
4-bit target only
8.9 GB
9.4 GB
~5 tok/s
4-bit target + BF16 drafter
11.7 GB
12.2 GB
OVERFLOW
4-bit target + 4-bit drafter
10.3 GB
10.8 GB
~8 tok/s
BF16-attn target + 4-bit drafter (shared embed)
10.9 GB
11.4 GB
~10 tok/s
Critical: On RTX 3060 12GB, Windows reserves ~0.5GB for display. If reserved VRAM exceeds ~11.5GB, CUDA silently spills to system RAM via PCIe, causing a 50x slowdown. Always check torch.cuda.memory_reserved(0).
DSpark Speculative Decoding
This model supports DSpark (Draft Speculative) decoding for 2x speedup:
How it works
Drafter (5-layer Qwen3, 2560 hidden): Predicts 7 tokens in a single forward pass using masked attention + target hidden states
Target (12B Fuse-2): Verifies all 7 tokens in ONE forward pass
Greedy verification: Accept the longest prefix where drafter matches target's argmax
Bonus token: Target always produces 1 bonus token (even if all 7 are rejected)
Key optimizations
Forward hooks on 5 target layers (not output_hidden_states=True on all 36)
Single target forward per block (no sequential fallback)
4-bit drafter to save VRAM
Shared embedding between target and drafter (saves 0.8 GB)
Sliding window KV cache (512 tokens) for constant speed on long sequences
Acceptance rate
Short prompts: 3.0-3.4/7 (43-49%)
Long prompts: 2.3-2.5/7 (33-36%)
Code patterns: up to 4.5/7 (64%)
Performance
Speed (RTX 3060 12GB, 4-bit NF4)
Mode
Speed (short)
Speed (long, 512+ tok)
VRAM
Basic generation
5.5 tok/s
5.5 tok/s
8.9 GB
DSpark speculative
10.0 tok/s
8.5 tok/s
10.9 GB
Quality
The model produces:
Reasoning: Qwen3-quality chain-of-thought (the host handles this)
Code: DeepSeek-quality code generation (the experts handle this)
Mixed: Seamless switching between reasoning and code
Example output
Prompt: "Write a Python function to check if a number is prime."
Output (excerpt):
Okay, I need to write a Python function to check if a number is prime.
Let me think about how to approach this.
First, a prime number is a number greater than 1 that has no divisors
other than 1 and itself...
1. Check if the number is less than 2 → return False.
2. Check if the number is 2 → return True
3. Check if the number is even → if yes, return False
4. Iterate from 3 to sqrt(n), stepping by 2
5. For each i, check if n is divisible by i
6. If none divide n, return True