Instructions to use punsaisuwan/frankenmoe-python-typescript with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use punsaisuwan/frankenmoe-python-typescript with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir frankenmoe-python-typescript punsaisuwan/frankenmoe-python-typescript
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
Upload folder using huggingface_hub
Browse files
README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model: mistralai/Mistral-Small-3.1-24B-Instruct-2503
|
| 4 |
+
tags:
|
| 5 |
+
- lora
|
| 6 |
+
- mlx
|
| 7 |
+
- mixture-of-experts
|
| 8 |
+
- code-generation
|
| 9 |
+
- python
|
| 10 |
+
- typescript
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# FrankenMoE: Python + TypeScript Expert LoRA Adapters
|
| 14 |
+
|
| 15 |
+
Dual LoRA adapters ที่ fine-tune บน 4-bit quantized MLX version ของ
|
| 16 |
+
`mistralai/Mistral-Small-3.1-24B-Instruct-2503` แยกตามภาษาโปรแกรมมิ่ง
|
| 17 |
+
พร้อม Dispatcher script สำหรับ hot-swap ระหว่าง Expert แบบไม่ต้องโหลด
|
| 18 |
+
Base Model ใหม่
|
| 19 |
+
|
| 20 |
+
## Experts
|
| 21 |
+
|
| 22 |
+
| Expert | Language | Rank | Target Modules | Checkpoint |
|
| 23 |
+
|---|---|---|---|---|
|
| 24 |
+
| expert-1-python-v2 | Python | 16 | q,k,v,o_proj | iter 500 |
|
| 25 |
+
| expert-2-typescript | TypeScript | 8 | default (incl. MoE switch layers) | iter 100 |
|
| 26 |
+
|
| 27 |
+
## Usage
|
| 28 |
+
|
| 29 |
+
ต้องมี Base Model 4-bit อยู่ในเครื่อง (หรือดาวน์โหลดจาก [link ต้นทาง])
|
| 30 |
+
|
| 31 |
+
\`\`\`bash
|
| 32 |
+
python frankenmoe_dispatcher.py
|
| 33 |
+
\`\`\`
|
| 34 |
+
|
| 35 |
+
Dispatcher จะตรวจจับภาษาจาก Prompt และสลับ LoRA Adapter อัตโนมัติ
|
| 36 |
+
โดยจัดการทั้ง \`LoRALinear\` (attention) และ \`LoRASwitchLinear\`
|
| 37 |
+
(MoE expert MLP) อย่างถูกต้อง
|
| 38 |
+
|
| 39 |
+
## Training Details
|
| 40 |
+
|
| 41 |
+
- **Expert 1 (Python)**: แก้ไขปัญหา rank/capacity ไม่พอ (v1 broken ที่ rank=8)
|
| 42 |
+
โดยอัปเกรดเป็น rank=16, scale=8.0, ครอบคลุม 4 attention projections
|
| 43 |
+
- **Expert 2 (TypeScript)**: แก้ไข NaN loss (SAFE_PAD_TOKEN_ID=198) และ
|
| 44 |
+
Catastrophic Interference ระหว่าง function/type guard bias ผ่าน
|
| 45 |
+
dataset rebalancing และลด learning rate เป็น 1.5e-5
|
adapters/expert-1-python-v2/adapter_config.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter_path": "./adapters/expert-1-python-v2",
|
| 3 |
+
"batch_size": 1,
|
| 4 |
+
"config": "lora_config_expert1_v2.yaml",
|
| 5 |
+
"data": "./datasets/expert-1-python",
|
| 6 |
+
"fine_tune_type": "lora",
|
| 7 |
+
"grad_accumulation_steps": 1,
|
| 8 |
+
"grad_checkpoint": true,
|
| 9 |
+
"iters": 500,
|
| 10 |
+
"learning_rate": 1e-05,
|
| 11 |
+
"lora_parameters": {
|
| 12 |
+
"rank": 16,
|
| 13 |
+
"dropout": 0.0,
|
| 14 |
+
"scale": 8.0,
|
| 15 |
+
"keys": [
|
| 16 |
+
"self_attn.q_proj",
|
| 17 |
+
"self_attn.k_proj",
|
| 18 |
+
"self_attn.v_proj",
|
| 19 |
+
"self_attn.o_proj"
|
| 20 |
+
]
|
| 21 |
+
},
|
| 22 |
+
"lr_schedule": null,
|
| 23 |
+
"mask_prompt": false,
|
| 24 |
+
"max_seq_length": 2048,
|
| 25 |
+
"model": "./output-moe-mlx-4bit",
|
| 26 |
+
"num_layers": 16,
|
| 27 |
+
"optimizer": "adam",
|
| 28 |
+
"optimizer_config": {
|
| 29 |
+
"adam": {},
|
| 30 |
+
"adamw": {},
|
| 31 |
+
"muon": {},
|
| 32 |
+
"sgd": {},
|
| 33 |
+
"adafactor": {}
|
| 34 |
+
},
|
| 35 |
+
"project_name": null,
|
| 36 |
+
"report_to": null,
|
| 37 |
+
"resume_adapter_file": null,
|
| 38 |
+
"save_every": 50,
|
| 39 |
+
"seed": 0,
|
| 40 |
+
"steps_per_eval": 200,
|
| 41 |
+
"steps_per_report": 10,
|
| 42 |
+
"test": false,
|
| 43 |
+
"test_batches": 500,
|
| 44 |
+
"train": true,
|
| 45 |
+
"val_batches": 25
|
| 46 |
+
}
|
adapters/expert-1-python-v2/adapters.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:bf27527b12cf66f6c375b67b015b70bf0fead0a89b08b09dbc519f92956f17f8
|
| 3 |
+
size 33568580
|
adapters/expert-2-typescript/adapter_config.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter_path": "adapters/expert-2-typescript",
|
| 3 |
+
"batch_size": 1,
|
| 4 |
+
"config": null,
|
| 5 |
+
"data": "datasets/expert-2-typescript",
|
| 6 |
+
"fine_tune_type": "lora",
|
| 7 |
+
"grad_accumulation_steps": 1,
|
| 8 |
+
"grad_checkpoint": false,
|
| 9 |
+
"iters": 150,
|
| 10 |
+
"learning_rate": 1e-05,
|
| 11 |
+
"lora_parameters": {
|
| 12 |
+
"rank": 8,
|
| 13 |
+
"dropout": 0.0,
|
| 14 |
+
"scale": 20.0
|
| 15 |
+
},
|
| 16 |
+
"lr_schedule": null,
|
| 17 |
+
"mask_prompt": false,
|
| 18 |
+
"max_seq_length": 2048,
|
| 19 |
+
"model": "output-moe-mlx-4bit",
|
| 20 |
+
"num_layers": 16,
|
| 21 |
+
"optimizer": "adam",
|
| 22 |
+
"optimizer_config": {
|
| 23 |
+
"adam": {},
|
| 24 |
+
"adamw": {},
|
| 25 |
+
"muon": {},
|
| 26 |
+
"sgd": {},
|
| 27 |
+
"adafactor": {}
|
| 28 |
+
},
|
| 29 |
+
"project_name": null,
|
| 30 |
+
"report_to": null,
|
| 31 |
+
"resume_adapter_file": "adapters/expert-2-typescript/0000400_adapters.safetensors",
|
| 32 |
+
"save_every": 25,
|
| 33 |
+
"seed": 0,
|
| 34 |
+
"steps_per_eval": 25,
|
| 35 |
+
"steps_per_report": 10,
|
| 36 |
+
"test": false,
|
| 37 |
+
"test_batches": 500,
|
| 38 |
+
"train": true,
|
| 39 |
+
"val_batches": 25
|
| 40 |
+
}
|
adapters/expert-2-typescript/adapters.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f0aba896a07bdc398e29920ab40e69f35812e61d396c79a26952c604e86c611c
|
| 3 |
+
size 126137095
|
frankenmoe_dispatcher.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FrankenMoE Dispatcher — Rebuild-on-Swap Architecture (v4: Handle MoE Switch Layers)
|
| 3 |
+
แก้ปัญหา 'Can't convert layer of type LoRASwitchLinear to LoRA'
|
| 4 |
+
โดยตรวจจับทั้ง LoRALinear (Attention) และ LoRASwitchLinear (MoE Expert MLP)
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
from mlx.utils import tree_unflatten
|
| 8 |
+
from mlx_lm import load, generate
|
| 9 |
+
from mlx_lm.tuner.utils import linear_to_lora_layers
|
| 10 |
+
|
| 11 |
+
# 🔑 Import ทั้ง 2 Class ที่ mlx_lm ใช้ห่อ LoRA
|
| 12 |
+
# LoRALinear -> ใช้กับ Attention Layer ปกติ (q,k,v,o_proj)
|
| 13 |
+
# LoRASwitchLinear -> ใช้กับ MoE Switch/Expert MLP Layer โดยเฉพาะ
|
| 14 |
+
LORA_WRAPPER_TYPES = []
|
| 15 |
+
try:
|
| 16 |
+
from mlx_lm.tuner.lora import LoRALinear
|
| 17 |
+
LORA_WRAPPER_TYPES.append(LoRALinear)
|
| 18 |
+
except ImportError:
|
| 19 |
+
pass
|
| 20 |
+
try:
|
| 21 |
+
from mlx_lm.tuner.lora import LoRASwitchLinear
|
| 22 |
+
LORA_WRAPPER_TYPES.append(LoRASwitchLinear)
|
| 23 |
+
except ImportError:
|
| 24 |
+
pass
|
| 25 |
+
|
| 26 |
+
LORA_WRAPPER_TYPES = tuple(LORA_WRAPPER_TYPES)
|
| 27 |
+
print(f"🔎 ตรวจพบ LoRA Wrapper Types ที่ต้องจัดการ: {[t.__name__ for t in LORA_WRAPPER_TYPES]}")
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class FrankenMoEDispatcher:
|
| 31 |
+
def __init__(self, base_model_path="output-moe-mlx-4bit", max_num_layers=16):
|
| 32 |
+
print(f"🔄 กำลังโหลด Base Model: {base_model_path} ...")
|
| 33 |
+
self.model, self.tokenizer = load(base_model_path)
|
| 34 |
+
self.max_num_layers = max_num_layers
|
| 35 |
+
self.current_expert = None
|
| 36 |
+
|
| 37 |
+
self.experts = {
|
| 38 |
+
"python": {
|
| 39 |
+
"config_path": "adapters/expert-1-python-v2/adapter_config.json",
|
| 40 |
+
"weights_path": "adapters/expert-1-python-v2/0000500_adapters.safetensors",
|
| 41 |
+
},
|
| 42 |
+
"typescript": {
|
| 43 |
+
"config_path": "adapters/expert-2-typescript/adapter_config.json",
|
| 44 |
+
"weights_path": "adapters/expert-2-typescript/0000100_adapters.safetensors",
|
| 45 |
+
},
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
# Snapshot ทุก Module ใน Layer เป้าหมาย (Attention + MoE Switch + อะไรก็ตาม)
|
| 49 |
+
self._original_layers = {}
|
| 50 |
+
total_layers = len(self.model.layers)
|
| 51 |
+
self._layer_range = range(total_layers - max_num_layers, total_layers)
|
| 52 |
+
for real_idx in self._layer_range:
|
| 53 |
+
l = self.model.layers[real_idx]
|
| 54 |
+
for k, m in l.named_modules():
|
| 55 |
+
self._original_layers[(real_idx, k)] = m
|
| 56 |
+
|
| 57 |
+
print(f"✅ Base Model พร้อมใช้งาน (Snapshot {len(self._original_layers)} modules)\n")
|
| 58 |
+
|
| 59 |
+
def _restore_original_layers(self):
|
| 60 |
+
"""สแกนหา Module ที่เป็น LoRALinear หรือ LoRASwitchLinear แล้ว Restore กลับเป็นของเดิม"""
|
| 61 |
+
restored_count = 0
|
| 62 |
+
restored_types = {}
|
| 63 |
+
for real_idx in self._layer_range:
|
| 64 |
+
l = self.model.layers[real_idx]
|
| 65 |
+
restore_pairs = []
|
| 66 |
+
for k, m in l.named_modules():
|
| 67 |
+
if isinstance(m, LORA_WRAPPER_TYPES):
|
| 68 |
+
orig = self._original_layers.get((real_idx, k))
|
| 69 |
+
if orig is not None:
|
| 70 |
+
restore_pairs.append((k, orig))
|
| 71 |
+
restored_count += 1
|
| 72 |
+
type_name = type(m).__name__
|
| 73 |
+
restored_types[type_name] = restored_types.get(type_name, 0) + 1
|
| 74 |
+
if restore_pairs:
|
| 75 |
+
l.update_modules(tree_unflatten(restore_pairs))
|
| 76 |
+
return restored_count, restored_types
|
| 77 |
+
|
| 78 |
+
def load_expert(self, expert_name: str):
|
| 79 |
+
if expert_name == self.current_expert:
|
| 80 |
+
return
|
| 81 |
+
|
| 82 |
+
if expert_name not in self.experts:
|
| 83 |
+
raise ValueError(f"ไม่รู้จัก Expert: {expert_name}")
|
| 84 |
+
|
| 85 |
+
info = self.experts[expert_name]
|
| 86 |
+
with open(info["config_path"]) as f:
|
| 87 |
+
adapter_config = json.load(f)
|
| 88 |
+
|
| 89 |
+
num_layers = adapter_config.get("num_layers", 16)
|
| 90 |
+
lora_parameters = adapter_config.get("lora_parameters", {})
|
| 91 |
+
|
| 92 |
+
print(f"🔧 กำลังสลับไปใช้ Expert: '{expert_name}' "
|
| 93 |
+
f"(rank={lora_parameters.get('rank')}, "
|
| 94 |
+
f"scale={lora_parameters.get('scale')}, "
|
| 95 |
+
f"keys={lora_parameters.get('keys', 'default')})")
|
| 96 |
+
|
| 97 |
+
n, types_breakdown = self._restore_original_layers()
|
| 98 |
+
print(f" ↳ Restored {n} module(s) กลับเป็นของเดิม | breakdown: {types_breakdown}")
|
| 99 |
+
|
| 100 |
+
linear_to_lora_layers(self.model, num_layers, lora_parameters, use_dora=False)
|
| 101 |
+
self.model.load_weights(info["weights_path"], strict=False)
|
| 102 |
+
self.model.eval()
|
| 103 |
+
|
| 104 |
+
self.current_expert = expert_name
|
| 105 |
+
print(f"✅ พร้อมใช้งาน Expert: '{expert_name}'\n")
|
| 106 |
+
|
| 107 |
+
def route(self, prompt: str) -> str:
|
| 108 |
+
ts_signals = [
|
| 109 |
+
"typescript", "interface", "type guard", "generic", ": string",
|
| 110 |
+
": number", "utility type", "conditional type", "mapped type",
|
| 111 |
+
]
|
| 112 |
+
py_signals = [
|
| 113 |
+
"python", "def ", "decorator", "list comprehension",
|
| 114 |
+
"generator", "context manager",
|
| 115 |
+
]
|
| 116 |
+
prompt_lower = prompt.lower()
|
| 117 |
+
ts_score = sum(1 for kw in ts_signals if kw in prompt_lower)
|
| 118 |
+
py_score = sum(1 for kw in py_signals if kw in prompt_lower)
|
| 119 |
+
return "typescript" if ts_score > py_score else "python"
|
| 120 |
+
|
| 121 |
+
def generate_response(self, prompt: str, max_tokens: int = 300):
|
| 122 |
+
expert_name = self.route(prompt)
|
| 123 |
+
self.load_expert(expert_name)
|
| 124 |
+
|
| 125 |
+
messages = [{"role": "user", "content": prompt}]
|
| 126 |
+
formatted_prompt = self.tokenizer.apply_chat_template(
|
| 127 |
+
messages, add_generation_prompt=True, tokenize=False
|
| 128 |
+
)
|
| 129 |
+
response = generate(
|
| 130 |
+
self.model, self.tokenizer,
|
| 131 |
+
prompt=formatted_prompt, max_tokens=max_tokens, verbose=False
|
| 132 |
+
)
|
| 133 |
+
return expert_name, response
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
if __name__ == "__main__":
|
| 137 |
+
dispatcher = FrankenMoEDispatcher()
|
| 138 |
+
|
| 139 |
+
test_cases = [
|
| 140 |
+
"Write a Python function to check if a number is prime.",
|
| 141 |
+
"Write a TypeScript type guard function called isArray.",
|
| 142 |
+
"Write a Python decorator that logs execution time.",
|
| 143 |
+
"Create a TypeScript utility type Readonly that makes properties readonly recursively.",
|
| 144 |
+
"Write a Python generator function that yields Fibonacci numbers.",
|
| 145 |
+
"Write a TypeScript function isString that acts as a type guard.",
|
| 146 |
+
"Write a Python function using list comprehension to flatten a nested list.",
|
| 147 |
+
"Write a TypeScript debounce function using closures.",
|
| 148 |
+
]
|
| 149 |
+
|
| 150 |
+
for i, prompt in enumerate(test_cases, 1):
|
| 151 |
+
print("=" * 70)
|
| 152 |
+
print(f"โจทย์ที่ {i}: {prompt}")
|
| 153 |
+
print("=" * 70)
|
| 154 |
+
expert_used, response = dispatcher.generate_response(prompt)
|
| 155 |
+
print(f"[Router เลือก Expert: {expert_used}]")
|
| 156 |
+
print(response)
|
| 157 |
+
print()
|
| 158 |
+
|
| 159 |
+
print("=" * 70)
|
| 160 |
+
print("🎉 สลับ Expert สำเร็จทั้งหมด 8 รอบ โดยไม่มี Error")
|
| 161 |
+
print("=" * 70)
|