1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gemma4 26b MoEをノートパソコンで可能な限り省電力で動かす

1
Last updated at Posted at 2026-10-06

この記事はなにか

ローカルAIの一つであるGemma4 26b MoEをローミドルクラスノート用GPUで動かした時のワッパ最適化の話です。
このモデル、なんとローミドルクラスノートで動くんですが、普通に動かすと爆熱🔥であり、爆音📢であり、膝上低温火傷で歩行に支障を来す事確定です。しかし、爆熱🔥を避ける設定を探りあてました。この設定ならばかなり良い感じに動きます。

ただし、環境はlinuxです。

Gemma4 26b MoEとは

知っている人は飛してください。
Gemma4 26b MoEはGoogle製のLLMのローカルモデルです。ローカルLLMはollamaとかで動かせます。その中には精度が低くて計算が少ないものから、精度が高くて計算が多いものまであります。
では、精度と速度を両立させるにはどうすればいいでしょうか?
それには巨大モデルの一部だけ常時GPUに載せて使い、滅多に使わないものは適宜CPUで使うという方法があります。
そういうやりかたをするのをMixture of Expertsといいます。これにより、少ないGPUメモリでも軽快に動かせるのです。
Gemma4 26bはそのようなモデルの一つです。

性能は高いです。例えば、所詮ベンチマークにすぎませんが、下記のベンチマークではgpt-oss 120bを上まわっています。

このローカルAIを普通のゲーミングノートやクリエイターPCで動かすと…マジで爆熱でファンは最大で悲しみに包まれます。そこで、性能を保ちつつ爆熱にならないようにワッパを削るのが本稿のテーマです。

どの程度ならこのAIは動くのか?

僕のノートパソコンの性能を示します。

項目 パーツ 性能
CPU intel core i7 13700H p core 6, e core 8
GPU RTX 4060 メモリ8Gb, 消費電力 35W
Memory 64Gb
SSD 2TB
OS Arch linux

これで動くのです。どの程度かというと…余裕です。
何故余裕かって?大規模なモデルでも一部だけしか動かさないから、GPUメモリが8GBでも大丈夫なのです。

前提の仮説

ゲーミング/クリエーターノートの様な非AI PCの場合、AI性能は高くともその性能には無駄があるかもしれません。そして、GPGPUにおいてボトルネックはメモリ転送速度である事が多いです。は並列計算大好きおじさんだから詳しいのです。
他にボトルネックがあるならば、あんまりクロック周波数を高めても無駄じゃね?
そんで、ボトルネックに丁度あわせた効率の良いクロック周波数というのが存在するんじゃね?

実験の目的

目的としては僕のノートパソコンで...
最高のLLM性能の出る最も低いCPU/GPUクロックを探る、です。

ここで、使用するCPUはe coreのみとします。理由はe coreはスループット特化であり、コア数を確保でき、AIが動いていてもp coreの日常使用を邪魔しないからです。AIはバックグラウンドで動かす。いいね?

事前の観察

事前の観察により、今回の条件ではe coreを使う量は6コアを越えない事がわかりました。なので、CPUはe core6つ分だけ確保しています。
その方がバックグラウンドでAIを動かすという条件に近いしね。

CPU設定

僕はArch linuxを使っているので簡単です。WindowsやMacでの設定のしかたは知らないので有識者は教えてほしいです。僕はこうします。

cpupower -c 14-19 frequency-set -u ○○MHz

ここで-c 14-19は使用するコアを指定しています。で、そのコアを使ってプログラムを走らせるにはこう。

taskset -c 14-19 ○○

です。ここで

taskset -c 14-19 ollama serve

とするとollamaが使用するコアを限定できます。

GPU設定

僕のはnvidiaなので、nvidia固有の設定になるんですが、大したことはない。

nvidia-smi -lgc $LOW/$HIGH

みたいにすれば$LOWから$HIGHの間におさまります。これでGPUのクロックを固定できます。

残念ながら僕はCPUの消費電力を測る方法をしりませんので、CPUの消費電力は今回ははかりません。

ここで、CPUのクロックやGPUのクロックを設定していますが、CPUについては上限を決めているだけだしサーマルスロットリングは普通におこるので、その点あまり正確な値ではありません。目標クロックと思ってください。

レギュレーション

上記のようにGPUとCPUの条件を用意し、それぞれの条件で2回ollama上のgemma4 26b MoEに質問しました。2回目のToken/Secondsの値を測定しました。
プロンプトは下記の2とおり。

  • 「Hello」
  • 「Linuxでのシグナルプログラミングについて200字程度で簡単に解説してください。」

測定したクロック周波数は以下のとおり

  • CPU Clockは800~3600MHzを400MHzおきに
  • GPU Clockは300~2400MHzを300MHzおきに

コード

長いので見たければ展開してください。
コードは適当な使い捨てコード。

ベンチマークのコード
from subprocess import run, PIPE
from time import sleep
import json
import re
from threading import Thread

CLOCKS = range(800, 3601, 400)
GPU_CLOCKS = range(300, 2401, 300)


class State:
    def __init__(self):
        self.running: bool = True


state = State()


class Unit:
    def __init__(self, value: float, unit: str):
        self.value, self.unit = value, unit

    @classmethod
    def from_str(cls, data):
        return Unit(*data.strip().split(' '))

    def __repr__(self):
        return f'{self.value} {self.unit}'


def get_free():
    free = run(['free', '-m'], stdout=PIPE)
    pat = re.compile('\\w+/?\\w+')
    headers, values = [
        pat.findall(n.strip()) for n in
        free.stdout.decode().strip().split('\n')[:2]
    ]
    result = {
        h: float(v)
        for h, v in zip(headers, values[1:])
    }
    return result

def get_cpu(): 
    cpu = run(['cpupower', '-c', '14-19', 'frequency-info', '--freq'], stdout=PIPE)
    pat = re.compile('\\w+')
    cpu_res = (n.strip() for n in cpu.stdout.decode().strip().split('\n') if n[0] == ' ')
    return [
        pat.findall(n)[3]
        for n in 
        cpu_res
    ]


def get_nvidia():
    nvidia = run(
        ['nvidia-smi',
         '--query-gpu=memory.used,utilization.gpu,power.draw,clocks.sm',
         '--format=csv'],
        stdout=PIPE
    )
    headers, values = [
        n.strip().split(',') for n in
        nvidia.stdout.decode().strip().split('\n')
    ]
    values = [Unit.from_str(v) for v in values]
    headers = [Unit.from_str(v) for v in headers]
    result = {
        h.value: float(v.value) for h, v in zip(headers, values)
    }
    return result


class Box:
    def __init__(self):
        self.data = []

    def get(self, name, func):
        return func([d[name] for d in self.data])

    def add(self, item):
        self.data.append(item)


def outer_bench():
    state.running = True
    free = Box()
    nvidia = Box()
    while state.running:
        free.add(get_free())
        nvidia.add(get_nvidia())
        sleep(0.5)
    print(
        f'"avail_min": {free.get('available', min)}, '
        f'"avail_max": {free.get('available', max)}, '
        f'"util": {nvidia.get('utilization.gpu', max)}, '
        f'"power": {nvidia.get('power.draw', max)}, '
        f'"memory": {nvidia.get('memory.used', max)}, '
        f'"gpu_min": {nvidia.get('clocks.current.sm', min)}, '
        f'"gpu_ave": {nvidia.get('clocks.current.sm',
                               lambda x: sum(x) / len(x))}, '
        f'"gpu_max": {nvidia.get('clocks.current.sm', max)}'
    )


def one_bench(prompt):
    thread = Thread(target=outer_bench)
    # Warm up
    run(
        ['curl', '-s', 'http://localhost:11434/api/generate',
         '-d', '''{
      "model": "gemma4:26b-32k",
      "prompt": "hello",
      "stream": false
    }'''
         ],
        stdout=PIPE
    )
    # Start bench
    thread.start()
    result = run(
        ['curl', '-s', 'http://localhost:11434/api/generate',
         '-d', f'''{{
      "model": "gemma4:26b-32k",
      "prompt": "{prompt}",
      "stream": false
    }}'''
         ],
        stdout=PIPE
    )
      #"prompt": "hello",
      #"prompt": "Linuxでのシグナルプログラミングについて200字程度で簡単に解説してください。",
    data = json.loads(result.stdout)
    print(f'"speed": {data['eval_count'] / data['eval_duration'] * 1000000000},')
    state.running = False
    thread.join()


def bench():
    first = True
    for cpu_clock in CLOCKS:
        run(
            ['cpupower', '-c', '14-19', 'frequency-set',
             '-u', f'{cpu_clock}MHz'],
            stdout=PIPE
        )
        for gpu_clock in GPU_CLOCKS:
            if not first:
                print(',')
            first = False
            print('{')
            run(
                ['nvidia-smi', '-lgc', f'{gpu_clock},{gpu_clock}'],
                stdout=PIPE
            )
            print(f'"CPU": {cpu_clock}, "GPU": {gpu_clock},')
            one_bench("Linuxでのシグナルプログラミングについて200字程度で簡単に解説してください。")
            print('}')


bench()

Plotのコード
from matplotlib import pyplot as plt
from pathlib import Path
import json
from itertools import product
import seaborn as sns
data = json.loads(Path('res_long.json').read_text())
title = 'Signal'

CLOCKS = list(range(800, 3601, 400))
GPU_CLOCKS = list(range(300, 2401, 300))
print(len(CLOCKS))
print(len(GPU_CLOCKS))

def get_clocks(data, cpu, gpu, key = None):
    for d in data:
        if d["CPU"] == cpu and d["GPU"] == gpu:
            if key:
                return d[key]
            else:
                return d



heat = [
    [get_clocks(data, c, g, key='speed') for c in CLOCKS]
    for g in GPU_CLOCKS
]

ax = sns.heatmap(heat, annot=True, fmt='.1f', annot_kws=dict(fontsize=16))
ax.set_xticklabels(CLOCKS, fontsize=16)
ax.set_yticklabels(GPU_CLOCKS, fontsize=16)
ax.set_xlabel('CPU clock (MHz)', fontsize=20)
ax.set_ylabel('GPU clock (MHz)', fontsize=20)
ax.invert_yaxis()
ax.set_title(f'LLM speed (Gemma4 MoE 26b) Token/Second [{title}]', fontsize=28)
plt.show()

heat = [
    [get_clocks(data, c, g, key='power') for c in CLOCKS]
    for g in GPU_CLOCKS
]

ax = sns.heatmap(heat, annot=True, fmt='.1f', annot_kws=dict(fontsize=16))
ax.set_xticklabels(CLOCKS, fontsize=16)
ax.set_yticklabels(GPU_CLOCKS, fontsize=16)
ax.set_xlabel('CPU clock (MHz)', fontsize=20)
ax.set_ylabel('GPU clock (MHz)', fontsize=20)
ax.invert_yaxis()
ax.set_title(f'GPU Electric Power [{title}]', fontsize=28)
plt.show()

heat = [
    [get_clocks(data, c, g, key='speed') / get_clocks(data, c, g, key='power') for c in CLOCKS]
    for g in GPU_CLOCKS
]

ax = sns.heatmap(heat, annot=True, fmt='.2f', annot_kws=dict(fontsize=16))
ax.set_xticklabels(CLOCKS, fontsize=16)
ax.set_yticklabels(GPU_CLOCKS, fontsize=16)
ax.set_xlabel('CPU clock (MHz)', fontsize=20)
ax.set_ylabel('GPU clock (MHz)', fontsize=20)
ax.invert_yaxis()
ax.set_title(f'Speed / GPU Electric Power [{title}]', fontsize=28)
plt.show()

結果

速度

まずはToken/Secondsの結果です。
Helloの場合

Figure_1.png

シグナルプログラミングの場合
Figure_2.png

CPUで高クロックでは途中でサーマルスロットリングなのか、クロックの低下が認められました。

GPU消費電力

次にGPUの消費電力です。
Helloの場合
Figure_3.png

シグナルプログラミングの場合
Figure_4.png

GPU限定のワッパ

CPUのワッパを測定する方法をしらなかったので、GPUのワッパを測りました。

Helloの場合
Figure_5.png

シグナルプログラミングの場合
Figure_6.png

考察

速度

Helloみたいな専門知識いらない会話でも、シグナルプログラミングの話題でも、いずれにせよGPUとCPUのクロックが高い方が有利な傾向です。あたりまえですね。
ただ、よく見てほしい。
CPUクロックが2400~3600MHzの場合、むしろ3600MHzの方が遅い結果になっています。これは多分爆熱になることによってクロックが強制的にさがってしまった事によるものと思います。実際に動いている時にhtopの画面見てたらクロックはブレブレでした。冬にやればまた違ったかも?
んで、CPUで2400MHz、GPUで1200MHz〜1500MHz以降は性能の伸びがプラトーに達しています。きっと、メモリの転送速度などがボトルネックにもなっているのでしょう。

ワッパ

そんで、ワッパを見れば900〜1500MHzでワッパが最強ですね。ここについてはCPUが加味されていないのであんまり意味のある数字ではないかもしれませんが、それでも性能の伸びがプラトーに達する1200MHz~1500MHzで良いのがいいですね。
というか、2100MHz以上のフルパワーで回してもあんま意味ないことがわかります。
あと、CPUのクロックを2800MHz以上に上げるとクソうるさいです。

で、実際の性能はいいのか?

このパソコンはGPUメモリが8GBしかないのに25 Token/Secondsで出力できていました。もちろん、chatgptのようなクラウドにはかないませんが、使い方次第といったところと思います。
基本、実力自体はクラウドのフロンティアモデルには大きく劣るのですが、あまり外に出したくない情報を処理するならば実用範囲内とは思います。

この結果から思うこと

  • AIの性能はフルパワーを出せばいいというものではない。
  • サーマルスロットリングを招く設定はむしろ性能を悪化させる。
  • Gemma4 26b MoEは個人のPCでバックグラウンドでも動かせる。
  • 適切な設定で爆熱と爆音を回避した上で十分な性能を出せる。

感想

ゲーミングノートでも熱くならないローカルAIについての考察でした。
AIは気軽に使えたら嬉しいですよね。
僕はクロックをおとして運用しようと思いました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?