スマホのLinux(Termux/PRoot)でllama.cppの生成速度を測った(Qwen2.5 0.5B・1.5B、Q4/Q8)
スマホで小さな言語モデルを動かして、実用になる速さは出るのか。先に答えを書く。
Qwen2.5 の 0.5B は、この記事で置いた目安に届いた。1.5B の Q4_K_M は、生成だけなら届いたが、質問の読み込みまで含めると届かなかった。1.5B の Q8_0 は、速さを測る前に、メモリの条件で止まった。
ただし、この答えには但し書きが多い。測ったのは 2026-09-26 の、1台の端末だけだ。しかも、18条件のうち10条件は、測定中にスワップ(メモリの退避先)に出て無効にした。ここに並べる数字は「この端末・このときのメモリの状態で測った値」で、モデルや量子化どうしの一般的な速さの差を示すものではない。
測り方はこうだ。Android の Termux に PRoot で Ubuntu を入れた環境で、llama.cpp(言語モデルを CPU で動かすソフト)の測定用コマンド llama-bench を使い、Qwen2.5 の 0.5B と 1.5B を、それぞれ Q4_K_M と Q8_0 の2種類の量子化(重みを小さくする方式)で測った。スレッド数は 1・2・4、各条件5回だ。環境と測定の細かい手順は、記事の終わりの付録にまとめてある。
まず、「実用になる速さ」の線を引く
「実用になる」の線は、人や使い方で変わる。そこで、この記事では次の目安を自分で決めた。一般的な基準ではない。
- 日本語で300字くらいの返答を、30秒以内に書き終えること(1秒に10字)。
チャットのように質問して返事を待つ使い方で、待てる長さとして自分で置いた線だ。判定は返答の生成だけで行い、質問を読み込む時間は含めない(含めた場合の時間は結果の節に書く)。後で出す日本語のトークン数の測定(1トークンあたり 1.51字)で換算すると、300字は約198トークンで、1秒に 6.6 トークン以上の生成が必要になる。
結果: 0.5B は目安に届き、1.5B の Q4_K_M は質問の読み込みを含めると届かなかった
では、実際の値を見てみよう。有効で、5回のばらつきも小さかった条件だけを並べる。どの条件を除いたかは、付録Aに表にした。
| モデル | スレッド | 回 | 生成(tokens/s、中央値) | 5回の幅 | 最大メモリ(VmHWM) |
|---|---|---|---|---|---|
| 0.5B-Q4_K_M | 4 | 2回目 | 19.4 | 19.2〜19.8 | 約657MB |
| 0.5B-Q8_0 | 1 | 2回目 | 26.8 | 25.3〜31.2 | 約833MB |
| 0.5B-Q8_0 | 2 | 1回目 | 26.1 | 24.6〜33.6 | 約833MB |
| 0.5B-Q8_0 | 2 | 2回目 | 28.6 | 27.6〜28.9 | 約833MB |
| 0.5B-Q8_0 | 4 | 1回目 | 26.0 | 24.2〜26.8 | 約833MB |
| 0.5B-Q8_0 | 4 | 2回目 | 29.0 | 28.2〜30.5 | 約833MB |
| 1.5B-Q4_K_M | 2 | 2回目 | 8.6 | 8.5〜9.2 | 約1,299MB |
生成の速さを、条件ごとに5回の最小〜最大と中央値で並べたのが上の図だ。0.5B は Q4_K_M・Q8_0 とも中央値が 19.4〜29.0(tokens/s)の範囲に入り、1.5B の Q4_K_M は 8.6 だった。1.5B の幅は、0.5B のどの条件の幅とも重なっていない。
目安と比べるには、tokens/s より日本語の字数と秒のほうが分かりやすい。そこで、日本語の字数に直してみる。
| モデル | スレッド | 回 | 日本語の字数/秒 | 300字にかかる秒 | 質問(256トークン)の読み込み |
|---|---|---|---|---|---|
| 0.5B-Q4_K_M | 4 | 2回目 | 29.4 | 10.2秒 | —(比べられない) |
| 0.5B-Q8_0 | 1 | 2回目 | 40.5 | 7.4秒 | 3.9秒 |
| 0.5B-Q8_0 | 2 | 1回目 | 39.5 | 7.6秒 | 1.2秒 |
| 0.5B-Q8_0 | 2 | 2回目 | 43.3 | 6.9秒 | 1.4秒 |
| 0.5B-Q8_0 | 4 | 1回目 | 39.4 | 7.6秒 | —(比べられない) |
| 0.5B-Q8_0 | 4 | 2回目 | 43.9 | 6.8秒 | —(比べられない) |
| 1.5B-Q4_K_M | 2 | 2回目 | 13.0 | 23.0秒 | 15.3秒 |
- 0.5B(Q4_K_M・Q8_0): 300字を 6.8〜10.2秒で書き終える計算で、目安に入った。
- 1.5B の Q4_K_M: 有効だったのは2回目の2スレッドの1条件だけで、300字に 23.0秒。生成だけなら目安に入ったが、256トークン(日本語で約388字)の質問を読み込むのに 15.3秒かかり、合わせると 38.3秒で、30秒の目安には届かなかった。
- 1.5B の Q8_0: 2回とも、測る直前の空きメモリの条件(付録C)を満たさず、測っていない。
問いへの答えを、もう一度まとめる。0.5B は目安に届いた。1.5B の Q4_K_M は、スワップに出なかった1条件(2回目・2スレッド)で、生成だけなら目安に届いたが、質問の読み込みを含めると38.3秒で目安には届かなかった。1.5B の Q8_0 は速さを測る前にメモリの条件で止まった、という結果だ。
とはいえ、この結果をそのまま一般化はできない。1.5B の Q4_K_M は6条件(2回 × スレッド 1・2・4)のうち5条件が測定中にスワップに出て無効になり、目安に届いたのは2回目・2スレッドの1条件だけだ。1回目の同じ条件は無効だった。0.5B の Q4_K_M も、有効だったのは6条件のうち1条件にとどまる。無効にした条件の一覧と理由は付録Aにある。
もう一つ、同じ条件でも回によって速さが変わった。0.5B Q8_0 の4スレッドでは、1回目(中央値 26.0)と2回目(29.0)で5回の幅が重ならなかった。だから、1.5B Q4_K_M の 8.6 も1回だけの値として見る必要がある。この端末でいつも 1.5B の Q4_K_M が目安に届くとまでは言えない。
スレッド数を変えると、何が変わるか
次に、スレッド数の影響を見てみよう。いちばん有効な条件が多かった 0.5B の Q8_0 で比べる。
- 生成は、スレッド数を増やしてもほとんど変わらなかった。1スレッド 26.8、2スレッド 26.1・28.6、4スレッド 26.0・29.0(tokens/s、中央値。2つある所は1回目・2回目)。
- prompt 処理(質問を読み込む速さ)は、1スレッドの 63.6・65.6 から、2スレッドで 208.5・189.0 に上がった。4スレッドは2回とも変動係数が15%を超えたので、比べていない。
この端末で、このプロセスが使える CPU は4コア(0・1・4・5番)だった。2スレッドで prompt 処理が 3.28倍・2.88倍(1回目・2回目)になった理由は確かめていない。
量子化を変えると: 0.5B では Q8_0 の方が速かった(4スレッドの生成だけ)
量子化の違いも見ておく。ファイルは Q4_K_M(0.49GB)の方が Q8_0(0.68GB)より小さい。ところが、比べられた4スレッドの生成では、Q8_0 の方が速い結果だった。2回目どうしで Q4_K_M 19.4、Q8_0 29.0、1回目の Q8_0 は 26.0 だ(tokens/s、中央値)。
0.5B の Q4_K_M で有効だったのはこの1条件だけで、ほかのスレッド数では比べられていない。理由も確かめていない。そのため、「この端末の4スレッドの生成では、Q8_0 の方が速かった」ところまでにとどめる。
日本語は英語より、トークン数がどれだけ多いか
最後に、日本語と英語の違いを見る。言語モデルは文をトークンという単位に分けて扱うので、同じ意味でもトークンが多い言語は、同じ tokens/s でも書き終えるのが遅くなる。そこで、同じ意味の日本語と英語の短い文を6組、自分で作り、llama.cpp の llama-tokenize で数えた(0.5B と 1.5B は同じトークン ID になった)。
- 明日の朝は雨が降るので、傘を持って出かけます。 / It will rain tomorrow morning, so I will take an umbrella when I go out.
- このスマホでは、小さな言語モデルが手元で動きました。 / A small language model ran locally on this smartphone.
- 駅前の本屋が閉店したので、新しい漫画は通販で買っています。 / The bookstore in front of the station has closed, so I now buy new manga online.
- ファイルを保存する前に、名前と日付を確認してください。 / Before saving the file, please check the name and the date.
- 週末は友だちと三人で、新しく出たゲームを遊びました。 / On the weekend, I played a newly released game with two friends.
- 電池の残りが少ないときは、画面の明るさを下げると長持ちします。 / When the battery is low, lowering the screen brightness makes it last longer.
| # | 日本語の字数 | 日本語のトークン | 英語の単語数 | 英語のトークン | 日÷英 |
|---|---|---|---|---|---|
| 1 | 23 | 16 | 15 | 17 | 0.94 |
| 2 | 26 | 15 | 9 | 10 | 1.50 |
| 3 | 29 | 21 | 16 | 18 | 1.17 |
| 4 | 27 | 15 | 11 | 13 | 1.15 |
| 5 | 26 | 19 | 12 | 14 | 1.36 |
| 6 | 31 | 21 | 13 | 15 | 1.40 |
| 計 | 162 | 107 | 76 | 87 | 1.23 |
6組の合計では、日本語は英語の 1.23倍のトークンになった。組ごとには 0.94〜1.50倍と幅があり、1組目は日本語の方が少ない。日本語は1トークンあたり 1.51字だった。前に出した表の「日本語の字数/秒」と「300字にかかる秒」は、この 1.51字で換算している。6組だけの結果なので、文の内容が変われば比は変わる。
数字を見るときの注意
- 1台の端末・1日の測定だ。 同じ条件でも、0.5B Q8_0 の4スレッドの生成は、1回目が 24.2〜26.8、2回目が 28.2〜30.5 で、5回の幅が重ならなかった。同じ日の中でも、回によってこの程度の差が出ている。
- 温度と充電の状態は確かめられていない。 スマホは熱で速さが変わることがある。
- メモリの状態に左右された。 各条件・各モデルの判定の直前に記録した空きメモリは 2.22〜3.79GB の間で動き、無効の条件が多かったことにも影響している可能性がある(ほかのプロセスの動きは記録していないので、確かめていない)。
- VmSwap は目安の値だ。 Linux の proc のマニュアルには、この値は不正確なことがあると書かれている。また 0.5秒ごとに読んだので、それより短い間の退避は記録から漏れている可能性がある。
- 速さだけを測った。 返答の中身の良し悪しは比べていない。0.5B と 1.5B では、返せる内容の質も違うと考えられる(今回は確かめていない)。
- 短い入力・短い出力だ。 prompt 256トークン・生成64トークンで、長い会話の後半のように読み込む量が増えると、速さは変わる。
まとめ
スマホ(Termux/PRoot)の1台で測った範囲では、Qwen2.5 の 0.5B は「日本語300字を30秒以内」の目安に届いた。1.5B の Q4_K_M は、有効だった1条件で生成だけなら届いたが、質問の読み込みを含めると38.3秒で届かなかった。1.5B の Q8_0 は、メモリの条件を満たさず測っていない。ただし、条件の多くが測定中のスワップで無効になり、同じ条件でも回によって速さが変わったので、この結果は「この端末・このときの状態の値」として読んでほしい。
付録A 無効にした条件: 18条件のうち10条件
Linux では、メモリが足りなくなると、プロセスのメモリの中身の一部をスワップ(退避先の領域)に移すことがある。測定中にモデルの重みが退避されると、その分だけ速さが落ちるおそれがある。そこで、測っている llama-bench のプロセスがスワップに出た条件は、値を使わずに無効にした。さらに、同じ条件の5回の変動係数(標準偏差÷平均)が15%を超えたものは「比べられない」とした。
1回目で有効な条件が少なかったため、同じ条件でもう1回だけ全体を回すことと、両方の回の扱い(両方有効なら両方載せる。無効の値は比べるのに使わない)を、2回目の前に決めて記録した。やり直しはこの1回だけだ。
| 回 | モデル | スレッド | 測定中のスワップ(VmSwap の最大) | prompt 処理 | 生成 |
|---|---|---|---|---|---|
| 1回目 | 0.5B-Q4_K_M | 1 | 280MB → 無効 | 無効 | 無効 |
| 1回目 | 0.5B-Q4_K_M | 2 | 123MB → 無効 | 無効 | 無効 |
| 1回目 | 0.5B-Q4_K_M | 4 | 67MB → 無効 | 無効 | 無効 |
| 1回目 | 0.5B-Q8_0 | 1 | 0 kB | 63.6(変動係数 3.6%) | 比べられない(変動係数 24.8%) |
| 1回目 | 0.5B-Q8_0 | 2 | 0 kB | 208.5(変動係数 7.4%) | 26.1(変動係数 13.6%) |
| 1回目 | 0.5B-Q8_0 | 4 | 0 kB | 比べられない(変動係数 16.9%) | 26.0(変動係数 4.0%) |
| 1回目 | 1.5B-Q4_K_M | 1 | 762MB → 無効 | 無効 | 無効 |
| 1回目 | 1.5B-Q4_K_M | 2 | 434MB → 無効 | 無効 | 無効 |
| 1回目 | 1.5B-Q4_K_M | 4 | 342MB → 無効 | 無効 | 無効 |
| 1回目 | 1.5B-Q8_0 | — | 測っていない(空きメモリ不足: 空き 3.57GB の半分 < ファイル 1.89GB) | — | — |
| 2回目 | 0.5B-Q4_K_M | 1 | 98MB → 無効 | 無効 | 無効 |
| 2回目 | 0.5B-Q4_K_M | 2 | 97MB → 無効 | 無効 | 無効 |
| 2回目 | 0.5B-Q4_K_M | 4 | 0 kB | 比べられない(変動係数 47.1%) | 19.4(変動係数 1.1%) |
| 2回目 | 0.5B-Q8_0 | 1 | 0 kB | 65.6(変動係数 5.5%) | 26.8(変動係数 8.3%) |
| 2回目 | 0.5B-Q8_0 | 2 | 0 kB | 189.0(変動係数 9.8%) | 28.6(変動係数 2.0%) |
| 2回目 | 0.5B-Q8_0 | 4 | 0 kB | 比べられない(変動係数 20.7%) | 29.0(変動係数 3.4%) |
| 2回目 | 1.5B-Q4_K_M | 1 | 577MB → 無効 | 無効 | 無効 |
| 2回目 | 1.5B-Q4_K_M | 2 | 0 kB | 16.8(変動係数 1.1%) | 8.6(変動係数 2.9%) |
| 2回目 | 1.5B-Q4_K_M | 4 | 36MB → 無効 | 無効 | 無効 |
| 2回目 | 1.5B-Q8_0 | — | 測っていない(空きメモリ不足: 空き 2.90GB の半分 < ファイル 1.89GB) | — | — |
- 「測定中のスワップ」は、
llama-benchのプロセスの/proc/<pid>/statusにある VmSwap を 0.5秒ごとに読んだ最大値だ(MB は kB を1024で割った値)。 - 空きメモリ・スワップ・ファイルサイズの GB は10億バイトだ(この記事の GB はすべて同じ)。
- 0.5B の Q8_0 は、2回とも3条件すべてでスワップに出なかった。0.5B と 1.5B の Q4_K_M は、それぞれ6条件のうち5条件でスワップに出た。スワップに出た時点は、起動から2秒後〜133秒後とまちまちで、なぜ Q4_K_M の方で多かったのかは確かめていない。
- 測る前から、端末全体ではスワップが使われていた(1回目の開始時点で、約12.9GB のうち約8.8GB)。測定の前の準備の段階でも、llama.cpp の導入とモデルの取得をしていた間(llama.cpp はまだ動かしていない)に、端末全体のスワップの使用量が約0.87GB 増えていた。同じ時間帯に別の作業も動いていたため、どのプロセスによるものかは確かめていない。
付録B 測った環境
| 項目 | 内容 |
|---|---|
| 端末 | Android スマホ(Termux + proot-distro) |
| Linux | Ubuntu 26.04 LTS(aarch64)、カーネルの表示は 6.17.0-PRoot-Distro |
| CPU | lscpu の表示は Qualcomm・8コア・最大 4.74GHz。このプロセスが使えたのは4コア(nproc が 4、使える CPU は 0-1,4-5) |
| メモリ | 約11.6GB(/proc/meminfo の MemTotal)。各条件・各モデルの判定の直前に記録した空き(MemAvailable)は 2.22〜3.79GB |
| llama.cpp | Ubuntu のパッケージ llama.cpp 8681+dfsg-1(llama-bench の表示は version 8681 (Debian))。CPU のバックエンド libggml-cpu-armv9.2_2.so が読み込まれた |
| モデル | Hugging Face の Qwen 公式リポジトリ Qwen/Qwen2.5-0.5B-Instruct-GGUF・Qwen/Qwen2.5-1.5B-Instruct-GGUF の q4_k_m・q8_0。ライセンスは両方とも apache-2.0 |
同じ端末で静的サイトジェネレーターのビルド時間を測った記事もある(スマホのLinux(Termux/PRoot)でHugo・Eleventy・Astroのビルド時間を測った)。
モデルのファイルサイズは 0.5B が 0.49GB(Q4_K_M)・0.68GB(Q8_0)、1.5B が 1.12GB・1.89GB だ。取得したファイルの sha256 は、Hugging Face の API が示す値と一致した。3B は、同じ Qwen の GGUF リポジトリで表示されるライセンスが other(qwen-research)で apache-2.0 ではないため、対象にしていない。
付録C 測り方
- コマンド:
llama-bench -m <モデル> -t <1|2|4> -r 5 -p 256 -n 64 -mmp 0 -o json。prompt 処理は256トークン、生成は64トークンだ。llama-benchは各テストを-rの回数だけ繰り返し、JSON では1回ごとの値も出す。測る前に慣らしの実行が自動で入る(llama-bench --helpの--no-warmupの説明)。 -mmp 0: モデルをファイルの割り当て(mmap)ではなく、プロセスのメモリに読み込む設定だ。こうすると重みが退避されたときに VmSwap に現れるので、スワップの判定に使える。- 空きメモリの条件: 各モデルは、測る直前の MemAvailable の半分以下のファイルサイズのときだけ測った(測定の前に決めた条件)。
- 順番: 1回のセッションで 0.5B Q4_K_M → 0.5B Q8_0 → 1.5B Q4_K_M → 1.5B Q8_0、各モデルの中はスレッド 1 → 2 → 4 の順だ。1回目は 12:01〜12:28、2回目は 12:36〜12:49(日本時間)だった。
- 最大メモリ: VmHWM(プロセスが使った実メモリの最大値)を 0.5秒ごとに読んだ最大だ。
- 測定中は、測定を動かしているプログラム(AI エージェントの実行環境)が常駐していた。それ以外の作業はしていない。
出典
- 測定値(生成・prompt 処理の tokens/s、VmSwap、VmHWM、空きメモリ)とトークン数: すべて筆者が 2026-09-26 に上の環境で実行して記録したもの。日英の文は筆者が作成した。計測と集計は自作のスクリプトで行った。
- Hugging Face のモデル API(ファイルサイズ・sha256・ライセンス、取得日: 2026-09-26)
- llama.cpp「llama-bench」の README(pp・tg の意味、
-rの繰り返しと JSON の1回ごとの値、取得日: 2026-09-26) - Ubuntu Packages「llama.cpp」(26.04 の 8681+dfsg-1、取得日: 2026-09-26)
- Linux man-pages「proc_pid_status(5)」(VmSwap・VmHWM の意味と、値が不正確なことがある旨、取得日: 2026-09-26)