M2 Pro Mac mini 32GB RAM + SINQ + DeepSeek R1 32B Q4を使おうとする

AIモデルのメモリ使用量を60~70%も削減し安価で低性能なハードウェアでも動作するようにできるオープンソースの量子化手法「SINQ」をHuaweiが発表 - GIGAZINE

この記事読んで「ええやん」と思った。要は量子化ビット数Q4で精度めっちゃあがったよ、ということらしい。タイトルのとおりの手順と検証メモ。SINQのチュートリアルを環境にあわせて書き換えて実行する感じ。

結論的にM2 Pro Mac mini 32GBで、一応実行はやったと思うんだが、ちょっと使えないレベルの遅さではあった。うーん。使い方が違うのか、MPSで最適化されてないのか。

目次

環境

不要なアプリを終了する

大量のメモリを消費するので、不要なアプリは全部終了する。アクティビティモニタでメモリくってるやつ全部終了する。普通に落ちかねない

ダウンロードから量子化コードの実行まで

SINQを入れる

https://github.com/huawei-csl/SINQ

# 1. Clone the repository
git clone https://github.com/huawei-csl/SINQ.git
cd SINQ

# 2. Install dependencies
pip install -r req.txt

# 3. Install SINQ
pip install .

モデルのダウンロード

ツールのインストール。

pip install huggingface-hub

ダウンロード。32Bの場合。

huggingface-cli download deepseek-ai/DeepSeek-R1-Distill-Qwen-32B --local-dir ./deepseek_32b_fp16

14Bの場合は

huggingface-cli download deepseek-ai/DeepSeek-R1-Distill-Qwen-14B --local-dir ./deepseek_14b_fp16

ただ実際にやると、1つ8GBあるxxx.safetensorsのダウンロードが99%で切れるという嫌がらせが発生した。パラレルでダウンロードするとそうなる気がする。しかしなんで毎回99%でキレますか……。これについては仕方ないので、不足分を個別でダウンロードした。ブラウザからダウンロードするなら以下から。

CLIから個別でやるなら以下。個別でやっても止まる。100%で止まる。ブラウザが一番安定している……?

# ディレクトリ作る
LOCAL_MODEL_DIR="./deepseek_32b_fp16"
mkdir -p $LOCAL_MODEL_DIR

# 不足しているやつを落とす
huggingface-cli download deepseek-ai/DeepSeek-R1-Distill-Qwen-32B \
    --local-dir $LOCAL_MODEL_DIR \
    --include "model-00003-of-000008.safetensors" \
    --resume

deepseek_32b_fp16ディレクトリを確認して揃っていればOK。

量子化コードの実行

量子化コードの実行。以下を実行するとdeepseek_32b_q4_sinqに保存される。14Bは32を14に置換。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from sinq.patch_model import AutoSINQHFModel
from sinq.sinqlinear import BaseQuantizeConfig

model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-32B"
local_path = "./deepseek_32b_fp16"

tokenizer = AutoTokenizer.from_pretrained(local_path)

# 修正: offload関連のオプションを削除し、シンプルにロード
model = AutoModelForCausalLM.from_pretrained(
    local_path, 
    torch_dtype=torch.bfloat16,
    device_map="cpu",  # まずCPUにロード
    low_cpu_mem_usage=True  # メモリ効率的なロード
)

quant_cfg = BaseQuantizeConfig(
    nbits=4,
    group_size=64,
    tiling_mode="1D",
    method="sinq"
)

# Macのデバイス判定
if torch.backends.mps.is_available():
    current_device = "mps"
else:
    current_device = "cpu"

# 量子化実行
qmodel = AutoSINQHFModel.quantize_model(
    model,
    tokenizer=tokenizer,
    quant_config=quant_cfg,
    compute_dtype=torch.bfloat16,
    device=current_device
)

# 保存
save_dir = "deepseek_32b_q4_sinq"
AutoSINQHFModel.save_quantized_safetensors(
    qmodel,
    tokenizer,
    save_dir,
    verbose=True,
    max_shard_size="4GB",
)

テストコードの実行

import torch
from transformers import AutoTokenizer
from sinq.patch_model import AutoSINQHFModel

# 保存したディレクトリ
save_dir = "deepseek_32b_q4_sinq"

# トークナイザーのロード
tokenizer = AutoTokenizer.from_pretrained(save_dir)

# Macのデバイス判定
if torch.backends.mps.is_available():
    device = "mps"
    print("🚀 Using MPS (Apple Silicon GPU)")
else:
    device = "cpu"
    print("💻 Using CPU")

# 量子化モデルのロード
qmodel = AutoSINQHFModel.from_quantized_safetensors(
    save_dir,
    device=device,  # CUDA → MPS or CPU
    compute_dtype=torch.bfloat16,
)

print("✅ Model loaded successfully!")

# 推論テスト
prompt = "Hi"
inputs = tokenizer(prompt, return_tensors="pt").to(device)  # 同じデバイスに

print(f"\n📝 Prompt: {prompt}")
print("🤔 Generating response...\n")

with torch.inference_mode():
    out_ids = qmodel.generate(
        **inputs, 
        max_new_tokens=10,  # 短い応答
        do_sample=False,
        pad_token_id=tokenizer.eos_token_id  # 警告を避ける
    )

response = tokenizer.decode(out_ids[0], skip_special_tokens=True)
print(f"🤖 Response:\n{response}")

実行するととても頑張ることになる。スワップやばかったのでアプリ終了しまくったら10GBくらいになった。乗り切らないのか……。

CPUはそれほどでもない。

13分ほど経過したら返事がきた。

0.013tokens/秒ってことなの……?

14Bで再チャレンジ

14Bにして再チャレンジした。

import torch
from transformers import AutoTokenizer
from sinq.patch_model import AutoSINQHFModel
import time

save_dir = "deepseek_14b_q4_sinq"
tokenizer = AutoTokenizer.from_pretrained(save_dir)

# 明示的にMPSを指定
device = "mps"
print(f"🚀 Using device: {device}")

qmodel = AutoSINQHFModel.from_quantized_safetensors(
    save_dir,
    device=device,
    compute_dtype=torch.bfloat16,
)

# デバイス確認
print(f"🔍 Model is on: {next(qmodel.parameters()).device}")

# 短いテスト
prompt = "Hi"
inputs = tokenizer(prompt, return_tensors="pt").to(device)
print(f"🔍 Input is on: {inputs['input_ids'].device}")

print("\n⏳ Generating 20 tokens...\n")
start = time.time()

with torch.inference_mode():
    out_ids = qmodel.generate(
        **inputs,
        max_new_tokens=20,
        do_sample=False,
        pad_token_id=tokenizer.eos_token_id
    )

elapsed = time.time() - start
print(tokenizer.decode(out_ids[0], skip_special_tokens=True))
print(f"\n⏱️  {elapsed:.1f}s ({20/elapsed:.2f} tokens/sec)")

結果

m2: ~/git/tama/SINQ> python test_code_14b_2.py 
You are using the default legacy behaviour of the <class 'transformers.models.llama.tokenization_llama_fast.LlamaTokenizerFast'>. This is expected, and simply means that the `legacy` (previous) behavior will be used so nothing changes for you. If you want to use the new behaviour, set `legacy=False`. This should only be set if you understand what it means, and thoroughly read the reason why this was added as explained in https://github.com/huggingface/transformers/pull/24565 - if you loaded a llama tokenizer from a GGUF file you can ignore this message.
🚀 Using device: mps
100%|████████████████████████████████████████████████| 147/147 [00:00<00:00, 65072.58it/s]
100%|████████████████████████████████████████████████| 337/337 [00:00<00:00, 30431.46it/s]
🔍 Model is on: mps:0
🔍 Input is on: mps:0

⏳ Generating 20 tokens...

Hi<think>

</think>

Hello! How can I assist you today? 😊

⏱️  171.5s (0.12 tokens/sec)

10倍速い……けど……。0.12 tokens/sec……。

CPUで再チャレンジ

もしかしてCPUのほうが早かったりするのかな、と思って再チャレンジ。

m2: ~/git/tama/SINQ> python test_code_14b_cpu.py 
You are using the default legacy behaviour of the <class 'transformers.models.llama.tokenization_llama_fast.LlamaTokenizerFast'>. This is expected, and simply means that the `legacy` (previous) behavior will be used so nothing changes for you. If you want to use the new behaviour, set `legacy=False`. This should only be set if you understand what it means, and thoroughly read the reason why this was added as explained in https://github.com/huggingface/transformers/pull/24565 - if you loaded a llama tokenizer from a GGUF file you can ignore this message.
💻 Using device: cpu
100%|████████████████████████████████████████████████| 147/147 [00:00<00:00, 70207.55it/s]
100%|████████████████████████████████████████████████| 337/337 [00:00<00:00, 31066.19it/s]
🔍 Model is on: cpu

⏳ Generating 20 tokens on CPU...

Hi<think>

</think>

Hello! How can I assist you today? 😊

⏱️  CPU: 280.0s (0.07 tokens/sec)

oh... 0.07 tokens/sec かぁ……。

ollama

一応ollamaでも超簡易的に見てみる。

m2: ~/git/tama/SINQ> time ollama run deepseek-r1:14b "Hi"
Hello! How can I assist you today? 😊


real	0m8.628s
user	0m0.026s
sys	0m0.029s

m2: ~/git/tama/SINQ> time ollama run deepseek-r1:14b "Hi"
Hello! How can I assist you today? 😊


real	0m1.854s
user	0m0.017s
sys	0m0.017s

初回はモデルをロードしてなかったので。でもはやい。2回目が10トークンで1.9sくらいだから、5tokens/sくらいはあるんよな。

ついでに32Bも動かす。

m2: ~/git/tama/SINQ> time ollama run deepseek-r1:32b "Hello"
Hello! How can I assist you today? 😊


real	0m19.901s
user	0m0.038s
sys	0m0.052s
m2: ~/git/tama/SINQ> time ollama run deepseek-r1:32b "Hi"
Hello! How can I assist you today? 😊


real	0m3.248s
user	0m0.017s
sys	0m0.030s

3tokens/s くらい出ている。

所感

ollamaだと同じdeepseek-r1のQ4_K_Mで普通に使えているので、これは明らかに遅いなぁ。なんか間違えているかなぁ。

LLMになんか情報ない?と聞きつつ調べても直接的なのはみつからなんだが、まぁしかしチュートリアルもcuda決め打ちだし、MPS対応がまだ最適化されてないんじゃね、というような感じ。とすると、Macだと結局ollama使い続けるのが楽かつ現実的という話になるね。うーん。

気が向いたら持っているRTX4070でもチャレンジしてみる。14BのQ4ならギリのるはず……。

この記事をいいなと思っていただけた方、よければ高評価・チャンネル登録……はないので、コメント・SNSでシェア・ブックマーク、RSSフィード登録を、よろしくお願い致します。

コメント

コメントする

目次