客戶要求程式碼不能上傳雲端,他想在自己的筆電上跑一個 80B 等級的本機程式碼助手
Janas 是一個推論引擎負責「載入模型並讓它一個字一個字回答」的程式,模型本身只是一大包數字,要靠它才會動。它的賣點很直接:模型檔比記憶體RAM,電腦做事時放資料的工作桌面,很快但容量有限還大,也能跑。原文實測:48.4GB 的 Qwen3-Next-80B,在 32GB 記憶體的筆電上,寫字速度 24 tok/s(開啟草稿加速後 37 tok/s),同一台機器上的 llama.cpp 只有 2.8 tok/s。
訣竅在於這類模型是 MoE(混合專家)一個模型裡藏了很多組「專家」小網路,每處理一個字只會叫其中幾個出來工作,其餘睡覺架構。檔名 A3B 的意思就是每次只啟用約 30 億參數。既然大部分專家此刻在睡覺,就沒必要全部塞進記憶體。
所以 Janas 把專家的權重模型學到的數字,專家的「知識」就存在這些數字裡留在 SSD固態硬碟,比記憶體慢很多但便宜又大 上,需要哪個才讀進來。這就像圖書館只把今天要用的書搬到桌上,其他留在書庫。
要注意:這是原文作者在自己那台機器(Intel Core Ultra 9 185H、NVMe SSD、Linux)上的測量,原文自己也說「換一台機器數字會不同」,並附了 janas-bench 讓你自己量。
| 模型 | 讀 prompt:Janas | 讀 prompt:llama.cpp | 寫字:Janas | 寫字:llama.cpp |
|---|---|---|---|---|
| Qwen3-Next-80B-A3B(48.4GB,超過記憶體) | 62 | 16 | 24 | 2.8 |
| Qwen3-30B-A3B | 135 | 122 | 32 | 15 |
| Qwen3.8-27B(dense,17.6GB) | 30.6 | 25.6 | 4.4 | 2.1 |
| Qwen3-30B-A3B 3-bit(unsloth UD-Q3_K_XL) | 100 | 112 | 36 | 11 |
這是下載專案裡 ExpertCache 的核心:用 LRU(最久沒用先淘汰)管理「哪些專家留在記憶體」。這是概念模擬,不是 Janas 的真實程式碼。
def get(self, idx): if idx in self.cache: self.cache.move_to_end(idx) self.hits += 1 return self.cache[idx] self.misses += 1 with open(self.path(idx), 'rb') as f: data = f.read() self.cache[idx] = data if len(self.cache) > self.slots: self.cache.popitem(last=False) return data這是一個真實小專案:下載(或複製)檔案,照步驟在你電腦上跑起來。
import os
import random
import tempfile
from collections import OrderedDict
NUM_EXPERTS = 64
EXPERT_BYTES = 256 * 1024
TOP_K = 4
TOKENS = 400
HOT = list(range(12))
def make_store(folder):
for i in range(NUM_EXPERTS):
path = os.path.join(folder, 'expert_%d.bin' % i)
with open(path, 'wb') as f:
f.write(os.urandom(EXPERT_BYTES))
class ExpertCache:
def __init__(self, folder, slots):
self.folder = folder
self.slots = slots
self.cache = OrderedDict()
self.hits = 0
self.misses = 0
def path(self, idx):
return os.path.join(self.folder, 'expert_%d.bin' % idx)
def get(self, idx):
if idx in self.cache:
self.cache.move_to_end(idx)
self.hits += 1
return self.cache[idx]
self.misses += 1
with open(self.path(idx), 'rb') as f:
data = f.read()
self.cache[idx] = data
if len(self.cache) > self.slots:
self.cache.popitem(last=False)
return data
def pick_experts(rng, hot_ratio):
chosen = set()
while len(chosen) < TOP_K:
if rng.random() < hot_ratio:
chosen.add(rng.choice(HOT))
else:
chosen.add(rng.randrange(NUM_EXPERTS))
return chosen
def run(folder, slots, hot_ratio):
rng = random.Random(42)
cache = ExpertCache(folder, slots)
for _ in range(TOKENS):
for idx in pick_experts(rng, hot_ratio):
cache.get(idx)
total = cache.hits + cache.misses
mb = cache.misses * EXPERT_BYTES / 1024 / 1024
print('快取槽 %2d 個 | 命中率 %5.1f%% | 從磁碟讀了 %7.1f MB' % (slots, 100.0 * cache.hits / total, mb))
def main():
with tempfile.TemporaryDirectory() as folder:
make_store(folder)
total_mb = NUM_EXPERTS * EXPERT_BYTES / 1024 / 1024
print('模型共 %d 個專家,合計 %.0f MB;每個 token 啟用 %d 個\n' % (NUM_EXPERTS, total_mb, TOP_K))
for hot_ratio in (0.7, 0.0):
label = '專家選擇集中(70%% 落在 12 個熱門專家)' if hot_ratio else '專家選擇完全平均'
print('情境:' + label)
for slots in (4, 16, 64):
run(folder, slots, hot_ratio)
print()
if __name__ == '__main__':
main()