CUBE SUGAR CONTAINER

技術系のこと書きます。

macOS: 無線 LAN 接続用の 2 次元バーコードを作る

スマートフォンなどのカメラで読み取ると無線 LAN に接続できる 2 次元バーコードがある。 これは ZXing ("zebra crossing") という OSS のプロジェクトが提案したのが発祥のようだ。 現在では Android や iOS といったプラットフォームが対応しており、デファクトスタンダードになっている。

仕様は ZXing のリポジトリに記載されている。 といっても難しいことはなくて、特定の形式の文字列を 2 次元バーコードにエンコードするだけで作れる。

github.com

今回は macOS で実際に作る方法を紹介する。

もくじ

下準備

まずは Homebrew を使って必要なパッケージをインストールしておく。 これは文字列を 2 次元バーコードにエンコードするのに使う。

$ brew install qrencode

仕様について

仕様では、以下のような形式の文字列が定義されている。 このような文字列を 2 次元バーコードにエンコードすると、読み取ったシステムが解釈した上で無線 LAN に接続できる。

WIFI:T:WPA;S:<SSID>;P:<Password>;;

まず、文字列は必ず WIFI: というプレフィックスで始まる。 これで、無線 LAN に接続するための情報が以降に記載されていることを示している。

プレフィックス以降はコロンを挟んだキーバリュー形式になっている。 それぞれのキーバリューはセミコロンで区切られ、文字列の末尾もセミコロンで終端される。 つまり、次のような形式になる。

WIFI:<key1>:<value1>;<key2>:<value2>;...;<keyN>:<valueN>;;

一般的な環境で使うキーは TSP だろう。 それぞれ T が認証形式 (WPA など)、S が SSID、P がパスワードを示している。

たとえば WPA 認証で、SSID が TEST、パスワードが PASSWD の場合は次のような文字列になる。

WIFI:T:WPA;S:TEST;P:PASSWD;;

T:WPA を指定した場合、WPA のどのバージョンが使われているかはシステム側で判断することになるようだ。

それ以外にも、特定の状況で使われるキーがいくつか定義されている。 詳しくは先述のリポジトリの内容を確認してもらいたい。

作ってみる

仕様が分かったところで、実際に 2 次元バーコードを作ってみよう。

先述の文字列を qrencode コマンドを使って実際にエンコードする。

$ qrencode -o wifi-qr-code.png "WIFI:T:WPA;S:TEST;P:PASSWD;;"

これで wifi-qr-code.png というファイルに 2 次元バーコードが書き出される。

実際に出力された 2 次元バーコードは以下のとおり。

サンプルの 2 次元バーコード

ただし、上記の手順だとパスワードがターミナルのログに残ってしまう。 気になるときは read コマンドを介してシェル変数にパスワードを読み込むと良いだろう。 read コマンドに -s オプションをつけるとエコーバックされないのでログには残らない。 まあ、結局のところ読み取る 2 次元バーコードに平文で書き込まれるんだけどね。

たとえば、以下は WIFI_PASSWORD という名前のシェル変数に入れる場合の例になる。

$ read -s WIFI_PASSWORD

SSID も read コマンドで読み込む場合には -s はつけなくても良いだろう。

$ read WIFI_SSID

シェル変数に読み込んだ内容を元に 2 次元バーコードを作るには以下のようにする。

$ qrencode -o wifi-qr-code.png "WIFI:T:WPA;S:${WIFI_SSID};P:${WIFI_PASSWORD};;"

いじょう。


Mac で llama.cpp をビルドして GPU の有効・無効によるパフォーマンスの違いを比べてみる

Mac で llama.cpp を使う場合、最も手っ取り早い方法は Homebrew からインストールすることだろう。 コマンドラインから brew install llama.cpp するだけで、GPU での演算が有効なバイナリが得られる。 一方で、GPU を有効にした場合と無効にした場合で、どれくらいパフォーマンスに影響があるのかは自明でない。 そこで、今回は自分で llama.cpp をビルドして両者を比べてみることにした 1

使った環境は次のとおり。

$ sw_vers
ProductName:        macOS
ProductVersion:     26.1
BuildVersion:       25B78
$ uname -srm                                 
Darwin 25.1.0 arm64
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M2 Pro

もくじ

下準備

まずは、llama.cpp をビルドするのに使うパッケージなどを入れる。 Homebrew が入っていないときは、あらかじめインストールしておく。

$ brew install wget curl cmake libomp

次に llama.cpp のリポジトリをクローンしておく。

$ git clone https://github.com/ggml-org/llama.cpp.git
$ cd llama.cpp

llama.cpp で動かすモデルをダウンロードする。 特に何を使っても構わないが、大きなモデルだと環境によっては多少の時間がかかる。

$ wget -P /tmp https://huggingface.co/TheBloke/Llama-2-7B-GGUF/resolve/main/llama-2-7b.Q4_0.gguf

Homebrew でインストールした OpenMP を cmake が見つけられるように環境変数を設定する。

$ export OpenMP_ROOT=$(brew --prefix)/opt/libomp

ビルドする

GPU を有効にした状態と、無効にした状態でビルドする。 Mac の場合、GPU の有効・無効は GGML_METAL という設定項目で切り替えられる。

まずは GPU を有効にした状態から。 デフォルトで GPU が有効なので、特に明示的な指定は必要ない。

$ cmake \
  -B build_metal \
  -D BUILD_SHARED_LIBS=OFF
$ cmake \
  --build build_metal \
  --config Release \
  -j \
  --clean-first \
  --target llama-cli llama-bench llama-server

次に GPU を無効にした状態を。 こちらは GPU を無効にするために、明示的に -D GGML_METAL=OFF を指定する。

$ cmake \
  -B build_no_metal \
  -D GGML_METAL=OFF \
  -D BUILD_SHARED_LIBS=OFF
$ cmake \
  --build build_no_metal \
  --config Release \
  -j \
  --clean-first \
  --target llama-cli llama-bench llama-server

ビルドすると、各ビルドディレクトリの bin ディレクトリ以下にバイナリができる。

$ ls build_metal/bin
ggml-common.h       ggml-metal-impl.h   ggml-metal.metal    llama-bench     llama-cli       llama-server

ベンチマークする

それでは llama.cpp のベンチマークツールである llama-bench を使ってパフォーマンスを確認しよう。

まずは GPU を有効にしてビルドしたバイナリから。 -m オプションでベンチマークに使うモデルの GGUF ファイルを指定する。

$ ./build_metal/bin/llama-bench \
  -m /tmp/llama-2-7b.Q4_0.gguf
ggml_metal_device_init: tensor API disabled for pre-M5 and pre-A19 devices
ggml_metal_library_init: using embedded metal library
ggml_metal_library_init: loaded in 6.543 sec
ggml_metal_rsets_init: creating a residency set collection (keep_alive = 180 s)
ggml_metal_device_init: GPU name:   Apple M2 Pro
ggml_metal_device_init: GPU family: MTLGPUFamilyApple8  (1008)
ggml_metal_device_init: GPU family: MTLGPUFamilyCommon3 (3003)
ggml_metal_device_init: GPU family: MTLGPUFamilyMetal4  (5002)
ggml_metal_device_init: simdgroup reduction   = true
ggml_metal_device_init: simdgroup matrix mul. = true
ggml_metal_device_init: has unified memory    = true
ggml_metal_device_init: has bfloat            = true
ggml_metal_device_init: has tensor            = false
ggml_metal_device_init: use residency sets    = true
ggml_metal_device_init: use shared buffers    = true
ggml_metal_device_init: recommendedMaxWorkingSetSize  = 26800.60 MB
| model                          |       size |     params | backend    | threads |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | ------: | --------------: | -------------------: |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | Metal,BLAS |       8 |           pp512 |        390.39 ± 0.23 |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | Metal,BLAS |       8 |           tg128 |         41.57 ± 0.02 |

build: db9783738 (7310)

上記に含まれるテーブルの 2 つの行が、特定の状況におけるベンチマークのスループットを示している。

まず、test カラムが pp512 となっている行は、プロンプト処理 (Prompt Processing) のスループットを示している。 プロンプト処理のスループットは、システムのピーク演算性能に影響を受けやすい。 示されている数字 (t/s) は、入力したトークン数を、プロンプトを入力してから最初のトークンが出力されるまでの時間で割ったもの。 この数字が大きいほど、TTFT (Time To First Token) が小さくなってユーザは快適に感じる。 TTFT は、モデルが長い入力を扱う場合に特に重要になる。 512 という数字は、ベンチマークで用いた入力トークン数を示している。 今回の環境では 390.39 t/s という結果が得られた。

次に test カラムが tg128 となっている行は、トークン生成 (Token Generation) のスループットを示している。 トークン生成のスループットは、システムのメモリ帯域幅に影響を受けやすい。 示されている数字 (t/s) は、出力されたトークン数を、最初のトークンが出力されてから最後のトークンが出力されるまでの時間で割ったもの。 この数字が大きいほど、TPOT (Time Per Output Token) が小さくなってユーザは快適に感じる。 TPOT は、モデルが長い出力を扱う場合に特に重要になる。 128 という数字は、ベンチマークで用いた出力トークン数を示している。 今回の環境では 41.57 t/s という結果が得られた。

ベンチマークの読み方が分かったところで、次は GPU を無効にしたバイナリでも同じことをやってみよう。

$ ./build_no_metal/bin/llama-bench \
  -m /tmp/llama-2-7b.Q4_0.gguf
| model                          |       size |     params | backend    | threads |            test |                  t/s |
| ------------------------------ | ---------: | ---------: | ---------- | ------: | --------------: | -------------------: |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | BLAS       |       8 |           pp512 |        127.29 ± 0.61 |
| llama 7B Q4_0                  |   3.56 GiB |     6.74 B | BLAS       |       8 |           tg128 |         28.72 ± 0.08 |

build: db9783738 (7310)

今度は、同じテスト項目でもスループットが大きく落ちている。 プロンプト処理では 127.29 t/s と、GPU を有効にした場合に比べて約 32.6% の結果となった。 トークン生成では 28.72 t/s と、GPU を有効にした場合と比べて約 69% の結果となった。

考察

GPU を無効にしたバイナリでは、プロンプト処理とトークン生成のいずれもスループットが落ちている。 ただし、受ける影響の度合いは両者で異なっており、プロンプト処理の方がより大きくスループットを落としている。

これは、前述した通りそれぞれの処理が何処に主なボトルネックを持つかに由来するものと考えられる。 プロンプト処理の主なボトルネックは演算である。 したがって、GPU の有無に影響を受けやすい。 一方で、トークン生成の主なボトルネックはメモリの帯域である。 したがって、特にユニファイドメモリアーキテクチャの Mac では GPU の有無に影響を受けにくいのだろう。

まとめ

今回は Mac を使って llama.cpp をビルドして GPU の有効・無効によるパフォーマンスの違いを比べてみた。 GPU を無効にした場合には、プロンプト処理とトークン生成のいずれもスループットは落ちた。 一方で、スループットの落ちる程度は両者で差が見られることが分かった。



  1. どちらかというと、比べるための方法をメモしておく意味合いが強い

Raspberry Pi のファイルシステムを RAM ディスクで運用する

一般的に Raspberry Pi は SD カードにファイルシステムを構築して動作させることが多い。 ただ、SD カードには書き込み回数に制限があるため、製品やワークロードによっては寿命に到達するリスクがある。 また、SD カードに固有の問題ではないものの、不意の電源断などでファイルシステムが破損するリスクもある。

上記のようなリスクを低減するためにファイルシステム全体を RAM ディスクで運用する方法が考えられる。 SD カードのファイルシステムを読み取り専用にした上で、そこからの差分をオンメモリのファイルシステム (tmpfs) で読み書きする。 こうしておくと、普段は SD カードへの書き込みが生じないため SD カードの書き込み回数を消費しない。 また、SD カードのファイルシステムは読み取り専用なので、電源断が生じても破損するリスクを下げられる。

デメリットとして、当然のことながらオンメモリで管理している差分は電源が切れると揮発してしまう。 つまり、RAM ディスクを有効にしている間に書き込んだログや設定は消えてしまう。

Raspberry Pi OS (Trixie) にはファイルシステム全体を RAM ディスクで運用するための機能が公式で用意されている。 今回はその使い方を見ていく。

使った環境は次のとおり。 筐体は Raspberry Pi 5 のメモリが 8GB のモデルになる。

$ cat /etc/*-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
VERSION_CODENAME=trixie
DEBIAN_VERSION_FULL=13.2
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
$ uname -srm
Linux 6.12.47+rpt-rpi-2712 aarch64

もくじ

RAM ディスクを有効にする

RAM ディスクを有効にするには、管理者権限で raspi-config コマンドを実行する。

$ sudo raspi-config

以下のように選択肢を選んでいく。

4 Performance Options

P2 Overlay File System Enable/disable read-only file system

Would you line the overlay file system to be enabled?

<はい>

The overlay file system is enabled.

<了解>

Would you like the boot partition to be write-protected?

どちらでも良いが安全側に倒すのであれば <はい>

The boot partition is read-only.

<了解>

<Finish>

Would you like to reboot now?

<はい>

ここまででデバイスが再起動する。

動作を確認する

再起動後にファイルシステムの状態を確認すると /overlayroot のファイルシステムで 4GB の容量になっている。

$ df -h
ファイルシス   サイズ  使用  残り 使用% マウント位置
udev             3.9G     0  3.9G    0% /dev
tmpfs            1.6G   15M  1.6G    1% /run
/dev/mmcblk0p2   117G  8.4G  104G    8% /media/root-ro
tmpfs-root       4.0G  4.6M  4.0G    1% /media/root-rw
overlayroot      4.0G  4.6M  4.0G    1% /
tmpfs            4.0G  368K  4.0G    1% /dev/shm
tmpfs            5.0M   48K  5.0M    1% /run/lock
tmpfs            1.0M     0  1.0M    0% /run/credentials/systemd-journald.service
tmpfs            4.0G   16K  4.0G    1% /tmp
...

/etc/fstab を見ると、どのように実現されているかが分かりやすい。

$ grep overlay /etc/fstab 
#  This fstab is for overlayroot. The real one can be found at
#      sudo overlayroot-chroot
proc /proc proc defaults 0 0 # overlayroot:fs-virtual
PARTUUID=edd367c7-01 /boot/firmware vfat defaults,ro 0 2 # overlayroot:fs-unsupported
/media/root-ro/ / overlay lowerdir=/media/root-ro/,upperdir=/media/root-rw/overlay/,workdir=/media/root-rw/overlay-workdir/_ 0 1

OverlayFS を使って、次のようにディレクトリを重ね合わせている。

  • lowerdir
    • /media/root-ro/
  • upperdir
    • /media/root-rw/overlay/,
  • workdir
    • /media/root-rw/overlay-workdir/_

マウントされている状態を確認すると、/media/root-ro が SD カードのファイルシステムになっている。 ro フラグがついているので読み取り専用でマウントされている。

$ mount | grep root-ro
/dev/mmcblk0p2 on /media/root-ro type ext4 (ro,relatime)
overlayroot on / type overlay (rw,relatime,lowerdir=/media/root-ro,upperdir=/media/root-rw/overlay,workdir=/media/root-rw/overlay-workdir/_,uuid=on)

/media/root-rw の方は tmpfs で作成されたファイルシステムになっている。 こちらは rw フラグなので読み書きができる。

$ mount | grep root-rw
tmpfs-root on /media/root-rw type tmpfs (rw,relatime)
overlayroot on / type overlay (rw,relatime,lowerdir=/media/root-ro,upperdir=/media/root-rw/overlay,workdir=/media/root-rw/overlay-workdir/_,uuid=on)

以上のように、SD カードのファイルシステムをベースにして、その上に tmpfs のファイルシステムを重ねているようだ。

RAM ディスクを無効にする

RAM ディスクの状態を解除したいときは、有効にしたときと逆をすれば良い。

つまり、管理者権限で raspi-config を実行する。

$ sudo raspi-config

そして Would you line the overlay file system to be enabled?<いいえ> を選択すれば良い。

デバイスを再起動すれば RAM ディスクの状態が解除されている。

参考

なお、OverlayFS 自体への理解は以下が参考になるかもしれない。

blog.amedama.jp

いじょう。


Raspberry Pi OS (Trixie) で NIC に固定 IP アドレスを付与する

Raspberry Pi OS (Trixie) で NIC に固定 IP アドレスを付与する方法をメモしておく。 どうやら Bookworm 以降は Network Manager を使ってネットワークを設定するようになったらしい。

www.raspberrypi.com

公式のドキュメントには、固定 IP アドレスを付与する方法として DHCP サーバで MAC アドレスにアドレスを対応させる方法が書いてある。 それはそれとして、端末側で完結した形で固定 IP アドレスを設定したい。

使った環境は次のとおり。

$ cat /etc/*-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"
VERSION_ID="13"
VERSION="13 (trixie)"
VERSION_CODENAME=trixie
DEBIAN_VERSION_FULL=13.2
ID=debian
HOME_URL="https://www.debian.org/"
SUPPORT_URL="https://www.debian.org/support"
BUG_REPORT_URL="https://bugs.debian.org/"
$ uname -srm
Linux 6.12.47+rpt-rpi-2712 aarch64

もくじ

NIC の状態を確認する

ひとまず NIC の状態を確認する。 有線 LAN に eth0、無線 LAN に wlan0 が使えることが分かる。

$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DORMANT group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff

nmcli を使って設定する

今回は nmcli コマンドを使ってターミナルから設定していく。

まずはイーサネットに設定された connection があるか確認する。

$ nmcli connection show | grep -i ethernet

もし既存の connection が不要なときは nmcli connection delete で削除する。

$ sudo nmcli connection delete <connection-name>

新たに eth0 を使う connection を作成する。 以下では connection に wire という名前をつけている。

$ sudo nmcli connection add type ethernet con-name wire ifname eth0
$ nmcli connection show | grep ethernet
wire               80f2a4d0-e95a-4644-af7d-f4d50b437bd0  ethernet  --

connection に固定で付与したい IPv4 アドレスを ipv4.addresses に指定する。

$ sudo nmcli connection modify wire ipv4.addresses "172.16.X.X/16"

connection を down / up して設定を反映する。 もし有線 LAN 経由で操作しているときは接続が切れてしまうので注意する。

$ sudo nmcli connection down wire
$ sudo nmcli connection up wire

NIC の状態を確認すると、指定したアドレスが付与されている。 それとは別に DHCP でもアドレスが付与されているが、特に不都合もないので気にしないでおく。

$ ip address show eth0 
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
    inet 172.16.X.X/16 brd 172.16.255.255 scope global noprefixroute eth0
       valid_lft forever preferred_lft forever
    inet 172.16.X.Y/16 brd 172.16.255.255 scope global secondary dynamic noprefixroute eth0
       valid_lft 259196sec preferred_lft 259196sec
    inet6 XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX:XXXX/64 scope global dynamic noprefixroute 
       valid_lft 2591989sec preferred_lft 604789sec
    inet6 fe80::XXXX:XXXX:XXXX:XXXX/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever

いじょう。


Python: uv を使って手軽にフリースレッド版の CPython を試す

CPython 3.14 から、フリースレッドモード (Free-threaded mode) が正式にサポートされた 1。 フリースレッドモードでは、従来の CPython に存在した GIL (Global Interpreter Lock) の制約が無くなる。 GIL の制約が無くなると、これまでは基本的にマルチプロセスを使っていた並列処理がマルチスレッドでも書けるようになる。

ただし、フリースレッドモードにはいくつか注意点がある。 まず、CPython のバージョン 3.14 ではフリースレッドモードがデフォルトで無効になっている。 有効にするには、CPython をビルドするときにオプション (--disable-gil) で明示的に指定しなければいけない 2。 そして、フリースレッドモードが有効なときはプログラムのシングルスレッド性能が 5 ~ 10% 程度低下する。 つまり、並列処理は書きやすくなる一方で逐次処理は少し遅くなる。

今回は uv 3 を使うことで手軽にフリースレッドモードが有効な CPython を試してみる。 フリースレッドモードが有効な CPython が、本当にマルチスレッドで CPU コアを使い切れるのかを確認する。

使った環境は次のとおり。

$ sw_vers      
ProductName:        macOS
ProductVersion:     15.7.1
BuildVersion:       24G231
$ sysctl machdep.cpu.brand_string
machdep.cpu.brand_string: Apple M3
$ sysctl hw.logicalcpu
hw.logicalcpu: 8
$ uv -V
uv 0.9.2 (Homebrew 2025-10-10)

もくじ

下準備

まずは uv と GNU time をインストールしておく。 CPU の状況を観測する手段が何かしらあれば GNU time は無くても構わない。

$ brew install uv gnu-time

uv でインストールする Python で、フリースレッドモードが有効なビルドには名前の末尾に t がつく。 なので、まずはフリースレッドモードが有効な CPython 3.14 である 3.14t をインストールしよう。

$ uv python install 3.14t

比較対象として、フリースレッドモードが無効な通常の CPython 3.14 もインストールしておこう。

$ uv python install 3.14

サンプルコード

次に、それぞれのビルドで実行するサンプルコードを示す。 サンプルコードでは CPU の論理コア数よりも 1 少ない数のスレッドを起動してビジーループさせる。 GIL の制約が無ければ、このプログラムで CPU 1 コアよりも多いリソースを消費できるはず。

import threading
import os
import sys
import time


def _busy_loop(thread_id):
    """ビジーループする関数"""
    print(f"starting thread: {thread_id}")
    while True:
        pass


def main():
    # CPU 論理コア数より 1 少ない数のスレッドを起動する
    num_threads = os.cpu_count() - 1
    for i in range(num_threads):
        thread = threading.Thread(target=_busy_loop, args=(i,))
        # 親スレッドが止まったら子スレッドも停止する
        thread.daemon = True
        # スレッドを起動する
        thread.start()

    # メインスレッドはスリープさせる
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        sys.exit(0)


if __name__ == "__main__":
    main()

フリースレッドモードが無効なビルドで実行した場合

まずは通常の、フリースレッドモードが無効なビルドから試してみよう。

uv run コマンドに --script オプションを渡して先ほどのサンプルコードを実行する。 同時に --python オプションで実行する Python 実行環境を選ぶ。 このとき、末尾に t のつかない、フリースレッドモードが無効なビルドを指定する。 gtime コマンドを先頭につけることで、プログラムが消費する CPU のリソースに関する情報を出力できる。

$ gtime uv run --script --python 3.14 busyloop.py 
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6

実行しているのは 8 論理 CPU コアを積んだマシンなので 7 つのスレッドが走る。 その上で、リソースモニターなどを確認すると CPU 1 コア分のリソースが消費されているはず。 これは、同時に実行されるカーネルスレッドが GIL によって 1 つに制限されているため。

$ gtime uv run --script --python 3.14 busyloop.py 
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6
^C5.06user 0.04system 0:05.14elapsed 99%CPU (0avgtext+0avgdata 15312maxresident)k
0inputs+0outputs (929major+1467minor)pagefaults 0swaps

しばらく実行して満足したら Ctrl-C でプログラムの実行を止めよう。 上記の gtime コマンドの出力にも 99%CPU という表記が確認できる。

フリースレッドモードが有効なビルドで実行した場合

次は先ほどと同じことをフリースレッドモードが有効なビルドで試す。 違いは --python の引数の末尾に t をつけるだけ。

$ gtime uv run --script --python 3.14t busyloop.py
starting thread: 0
starting thread: 1
starting thread: 2
starting thread: 3
starting thread: 4
starting thread: 5
starting thread: 6
^C29.49user 0.04system 0:04.31elapsed 684%CPU (0avgtext+0avgdata 18992maxresident)k
0inputs+0outputs (1002major+1707minor)pagefaults 0swaps

今度はビジーループする 7 つのスレッドで CPU 7 コア分のリソースが消費されるはず。 上記の gtime コマンドの出力にも 684%CPU という表記が確認できる。

まとめ

今回は uv を使ってフリースレッドモードが有効な CPython 3.14 を動作させてみた。 uv は Python の実行環境まで管理できるので、こういった場面で便利だと感じられる。

なお、フリースレッドモードを公式にサポートしたバージョンの CPython 3.14 は 2025-10-07 にリリースされたばかり。 そのため、フリースレッドモードが有効なビルドを実用する上では、サードパーティー製のパッケージ群の対応を待つ時間が必要なはず。

また、将来的には CPython でフリースレッドモードがデフォルトで有効になる「フェーズ 3」も計画されている 4。 マルチスレッドで並列処理が書けるのは本当に嬉しくて、実用的になるはずの将来が楽しみで仕方がない。


batt で MacBook のバッテリー充電を制御する

リチウムイオンバッテリーは、充電量が 0% か 100% に近い状態で使い続けると劣化しやすいことが知られている。 また、充放電のサイクルが増えると少しずつではあるが着実に劣化していく。 リチウムイオンバッテリーが劣化すると、製品の設計上の容量よりも電気を蓄える力が落ちて駆動時間が短くなる。

したがって、MacBook のバッテリーの寿命を延ばすためには、次のような状態にしたい。

  • バッテリーが劣化しにくい充電量を維持する
    • 特定の充電量に到達したら充電を停止する
  • バッテリーの充放電サイクルを増やさない
    • 使うときは充電ケーブルから給電し続ける (バッテリーの出力を使わない)

しかし、残念ながら上記は macOS の標準的な機能では実現することが難しい。

なお、現在の macOS にも標準で「バッテリー充電の最適化」という機能はある。 この機能を有効にすると、たまに充電が 80% でしばらく止まることがある。 しかし、この機能を使ったとしてもユーザが明示的に充電を制御することはできない。 充電ケーブルをつないだままにしていると、そのうち充電量が 100% になってしまう。

そこで、今回は batt というアプリケーションを紹介する。 このアプリケーションを使うと MacBook で明示的にバッテリーの充電を制御できるようになる。

github.com

なお、類似のアプリケーションとしては battery 1 というのもある。

使った環境は次のとおり。

$ sw_vers 
ProductName:        macOS
ProductVersion:     15.7.1
BuildVersion:       24G231
$ batt version
v0.5.3 9c9e5e3ef4b1edd93d050aaddd5f9b91ddb6c1ac

もくじ

下準備

まず、Homebrew を使って batt をインストールする。

$ brew install batt

batt のサービスを起動する。 このとき管理者権限が必要なので sudo(8) をつける。

$ sudo brew services start batt

サービスが起動すると batt コマンドが使えるようになる。

$ batt status       
Charging status:
  Allow charging: ✔ (refreshes can take up to 2 minutes)
    Your Mac will charge, but you are not plugged in yet.
  Use power adapter: ✔
    Your Mac will use power from the wall (to operate or charge), if it is plugged in.

Battery status:
  Current charge: 57%
  State: discharging
  Full capacity: 4556 mAh
  Charge rate: -9.8 W
  Voltage: 11.65 V

Battery configuration:
  Upper limit: 80%
  Lower limit: 78%
  Prevent idle-sleep when charging: ✔
  Disable charging before sleep if charge limit is enabled: ✔
  Prevent system-sleep when charging: ✘
  Allow non-root users to access the daemon: ✘
  Control MagSafe LED: ✘

バッテリーの充電量に上限を設ける

まずは、最も一般的なユースケースと考えられるバッテリーの充電量に上限を設定する方法について。 これには batt limit コマンドを使う。 何処まで充電するかをパーセンテージで引数に指定する。

$ batt limit 70

実行すると、次のように充電量の上限が反映される。

$ batt status        
Charging status:
  Allow charging: ✔ (refreshes can take up to 2 minutes)
    Your Mac will charge, but you are not plugged in yet.
  Use power adapter: ✔
    Your Mac will use power from the wall (to operate or charge), if it is plugged in.

Battery status:
  Current charge: 57%
  State: discharging
  Full capacity: 4558 mAh
  Charge rate: -3.3 W
  Voltage: 11.70 V

Battery configuration:
  Upper limit: 70%
  Lower limit: 68%
  Prevent idle-sleep when charging: ✔
  Disable charging before sleep if charge limit is enabled: ✔
  Prevent system-sleep when charging: ✘
  Allow non-root users to access the daemon: ✘
  Control MagSafe LED: ✘

なお、電源が切れている状態では制限が効かない点には注意が必要になる。

バッテリーの充電を一時的に停止する

その他に、一時的にバッテリーの充電を停止することもできる。 これには batt adapter コマンドを使う。

まず、デフォルトではアダプタが有効になっている。 現在の状況は batt adapter status コマンドで確認できる。

$ batt adapter status
INFO[2025-10-13T17:24:37+09:00] power adapter is enabled

ここで、batt adapter disable を実行すると、充電が停止する。

$ batt adapter disable
INFO[2025-10-13T17:26:34+09:00] daemon responded: "ok"                       
INFO[2025-10-13T17:26:34+09:00] successfully disabled power adapter

確認すると、次のようにアダプタが無効になっている。

$ batt adapter status 
INFO[2025-10-13T17:26:56+09:00] power adapter is disabled

batt adapter enable コマンドを実行すると、充電が再開する。

$ batt adapter enable
INFO[2025-10-13T17:27:08+09:00] daemon responded: "ok"                       
INFO[2025-10-13T17:27:08+09:00] successfully enabled power adapter

batt による制御を停止する

また、batt による制御を止めたいときは次のように batt disable すれば良い。

$ batt disable

いじょう。


Android: GitHub にある Obsidian のデータを Termux + GitHub CLI で同期する

最近、情報を記録するのに Obsidian を使い始めた。 ただ、使い始める上で問題が一つあった。 それは、複数のデバイスでデータを同期する方法について。 Obsidian は、基本的にローカルのファイルシステムで Markdown を管理する。 そのため、複数のデバイスでデータを同期したい場合には、その方法を考える必要がある。 公式には Obsidian Sync という同期のためのサービスがあるものの月額で料金がかかる。 まずはもうちょっと気軽に始めたかったので、それ以外の選択肢を検討し始めた。

そして、ひとまず GitHub にプライベートリポジトリを作って管理する方法に落ち着いた。 このやり方には、次のような利点があると思う。

  • 複数の異なるプラットフォームのデバイスで同期しやすい
  • ファイルの世代管理ができる

今回は、そのやり方で Android を使ってデータ (Vault) を同期する方法について記録しておく。 Web で事例を調べると、デバイスに SSH の秘密鍵を置くやり方が多かった。 今回は、代わりに GitHub CLI を使って PAT を発行する。 ターミナルエミュレータには Termux というアプリを使ってみた。

もくじ

下準備

まずは Android に Termux のアプリをインストールする。

play.google.com

同様に Obsidian のアプリもインストールする。

play.google.com

Termux で GitHub にログインする

Termux のアプリを開いたら GitHub CLI をインストールする。

$ pkg install gh

インストールしたら GitHub CLI で GitHub にログインする。 これには gh auth login コマンドを使う。

$ gh auth login

対話的にログインとセットアップのやり方を聞かれる。

Where do you use GitHub?
> GitHub.com

What is your preferred protocol for Git operations on this host?
> HTTPS

How would you linke to authenticate GitHub CLI?
> Login with a web browser

Login with a web browser を選択したら、ブラウザで以下の URL にアクセスする。

github.com

そして、Termux の方に表示されているワンタイムコードを入力する。

First copy your one-time code: XXXX-XXXX

入力したら Termux 経由のアクセスを承認する。

ローカルのファイルシステムを操作できるようにする

デフォルトで Termux はデバイスのファイルシステムにアクセスできない。 そこで、アクセスできるようにセットアップする。

そのために termux-setup-storage コマンドを実行する。

$ termux-setup-storage

コマンドを実行するとホームディレクトリに storage というシンボリックリンクができる。

$ ls
storage

この storage からデバイスのファイルシステムにアクセスできる。

リポジトリをクローンする

すでに既存の Obsidian 用のリポジトリがあるときは、そのリポジトリをデバイスのファイルシステムにクローンする。

たとえば storage/documents の下に repos みたいなディレクトリを作る。

$ mkdir -p storage/documents/repos
$ cd storage/documents/repos

作成したディレクトリで gh repo clone コマンドを実行してリポジトリをクローンする。 <username> には自身のアカウント名、<repo-name> にはリポジトリの名前を入れる。

$ gh repo clone <username>/<repo-name>

あるいは、まだリポジトリがないときは gh repo create コマンドで対話的に作成する。

$ gh repo create

もちろん、リポジトリを作る作業はパソコンなど操作性に優れた環境を使った方が楽だろう。

クローンしたリポジトリを Obsidian から使う

リポジトリはデバイスのファイルシステムにクローンされている。 そのため、あとは Obsidian からそのディレクトリを vault として指定するだけで利用できる。

Obsidian を開いたら「Open folder as vault」を選択して、先ほどクローンしたディレクトリを指定する。

Git プラグインをセットアップする

このままでも Termux から操作すればリポジトリを管理できる。 しかし、Obsidian の方からコミットやプッシュできた方が便利なので設定していく。

まずは Obsidian のアプリの設定を開いて「Community plugins」に移動する。 デフォルトでは Community plugins は無効になっているため、まずは有効にする。

そして Community plugins の「Browse」を選択する。 一覧の中から「Git」プラグインを探してインストールする。 インストール直後にはプラグインが無効になっているため有効にする。

有効にしたらプラグインのオプションを開く。 下の方にスクロールして「Authentication/commit author」の項目を埋めていく。

まず、「Username」や「Auther name」は GitHub のアカウント名を入れれば良い。

「Author email」は、GitHub の Web サイトを確認していれる。 GitHub の Web サイトをブラウザで開いたら「Settings > Emails」に移動する。 下の方に「Keep my email addresses private」という項目があるので、無効になっているときは有効にする。 その上で、記載されている <random>+<username>@users.noreply.github.com 的なフォーマットのメールアドレスをコピーする。 コピーした内容を、Obsidian の方の「Author email」に入れる。

「Password/Parsonal acccess token」は GitHub CLI で発行する。 Termux の方に戻って以下のコマンドを実行する。

$ gh auth token

表示されたトークンをコピーして「Password/Parsonal acccess token」にペーストする。

以上で Git プラグインをセットアップできた。

Git プラグインからファイルをプッシュする

次に Git プラグインが正常に動作することを確認する。

Obsidian のアプリの右下のハンバーガーメニューから「Open Git source control」を開く。 まずはダウンロードする感じのボタンを押下して Git リポジトリを Pull できることを確認する。 うまくいかない場合にはアプリを開き直したり、パソコンから一旦リポジトリにプッシュしたりしてみよう。 Pull できれば、Commit や Push についても問題なくできるはず。

まとめ

今回は Android で GitHub にある Obsidian の Vault を同期する方法について書いた。