2つの p-kernel で、1つの推論をしてみた

当時の開発ログを見返しながらのメモ。

つながった何台かで、1つの AI の計算を分け合えるか試した。

■脳はおもちゃの大きさ

動かした Transformer はとても小さい。入力は4つの数字、ヘッド2つ、答えは3択。重みは全部で568個。
それでも仕組みは大きなモデルと同じ形。

■部品はそろっていた

K-DDS、中継、SWIM、生きている台数で動き方を変える「degrade」は既にあった。足りなかったのは起動時の配線と、シェルの `infer` コマンドだけ。

2台そろうと degrade が「2台モード」に切り替わる。infer を打つと、1台目がヘッド0を計算して、入力を中継越しに2台目へ。2台目がヘッド1を計算して返して、1台目が2つを合わせて答えを出す。

■毎回でたらめな場所で落ちる

中継越しの2台構成が毎回落ちる。しかも落ちるアドレスが毎回でたらめ。

推論の配線を止めても落ちるので、犯人は別。
中継とやりとりする部分で 1416バイトの受け皿を関数内の変数として4か所で確保していて、T-Kernel のタスクの小さなスタック(2〜4KB)をあふれさせていた。あふれた分が隣のタスクの戻り先を上書きして、でたらめな場所へ飛んでいた。static にして解決。
ベアメタル版には中継がないので今まで出なかった。

ほか:
・2台目が1回の問い合わせに同じ答えを100回くらい返していたのを修正
・パケットを1つ落としただけで「相手が死んだかも」と騒いでいたのを、2回続けて返事がないときだけに。2台だと代わりに様子を見てくれる3台目がいないので

■3台モードと最初のプルリクエスト

3台以上のときの分け方(各ノードが手元の KV キャッシュで注意を計算して持ち寄る)も試したら、送るデータが K-DDS の上限128バイトを超えていて、今まで一度も分散では動いていなかったことが判明。エラーが黙って捨てられて1台で計算していた…
上限を広げて、翌日には2台分の返事を集められるようにした。

5/22 からの13コミットをまとめて、GitHub のプルリクエスト #1 に。5/31 に取り込み。

※この日の実験は、中継も含めて全部1台の機械の中。

※リポジトリ: https://github.com/monyuonyu/p-kernel

※この記事は、p-kernel の開発を手伝ってもらっている AI(Claude)と一緒に書いています。

中継サーバを書いて、初めての Android アプリを作った

当時の開発ログを見返しながらのメモ。

同じ機械の中ならつながるようになったので、次はスマホ。
ただスマホは携帯回線の NAT の向こうにいるので、外から直接は話しかけられない。

■小さな中継

みんなが話しかけられる場所に中継(relay)を1つ置く。
5/22 の最初の版は C で200行ちょっと。12バイトのヘッダに「誰から・誰へ」を書いて送ると、中継が転送するだけ。

相手は IP・ポートではなくノード番号で覚える。スマホが Wi-Fi から携帯回線に切り替わっても、次に何か送れば住所が勝手に更新される。つなぎ直しの処理が要らない。

最初のテストで、終了の合図を送っても中継が止まらずに固まった。signal() だと受信待ちが自動でやり直される設定になるのが原因。sigaction() に変更。

■認証をつける

5/26 に認証を追加。送る側と中継が同じ鍵を持って、パケットごとに HMAC-SHA256。同じパケットの再送も弾く。
SHA-256 は OpenSSL を使わずに自前。中継は「1ファイル置けば動く」ままにしたかったので。

気をつけたのは確認の順番。署名が正しいのを先に確認して、合っていたときだけ再送チェック用の記録を更新する。逆にすると、でたらめなパケットで記録を進められてしまう。

■カーネルをライブラリにする

Android アプリからはカーネルを共有ライブラリ(.so)として読み込む。5/22 に試したら起動途中で落ちた。

最初は切り替え処理のアセンブリを疑ったけど違った。原因はメモリの端を決める定数。組み込み向けの「メモリの終わりはこのへん」という値が、アドレスがランダムに散らばる環境では的外れになって、メモリの大きさの計算がおかしくなっていた。疑う場所が1段ずれていた。

■APK ができた

26日の最後に APK が1つできた。3.5MB。中に p-kernel と中継につなぐ部分。
標準では充電中だけ動く。アプリを閉じても止まらないように、通知を出したまま動く形(フォアグラウンドサービス)。

ビルド環境は aarch64 の Linux だったけど、Android の開発ツールは x86_64 用しかない。ツールの実行ファイルを1つずつ qemu-x86_64 越しに呼ぶスクリプトで包んで無理やり動かした。遅いけど動く。

ヘッダを探す順番を1つ間違えて、エラーの定義が丸ごと別物になり1時間溶かしたのもこの日…

この日はビルドして中身を確かめたところまで。実際にスマホで動かしたのはもう少しあと。

※リポジトリ: https://github.com/monyuonyu/p-kernel

※この記事は、p-kernel の開発を手伝ってもらっている AI(Claude)と一緒に書いています。

aarch64 と x86_64 の p-kernel 同士をつないでみた

当時の開発ログを見返しながらのメモ。

前の日に Linux 上で aarch64 版と x86_64 版が動いたので、この2つをつないでみた。CPU が違っても仲間としてつながるか。

■朝の片付け

x86_64 版は 64ビット共通のヘッダを aarch64 のフォルダから借りてビルドしていたので、共通の置き場(lp64/)に移動。
そしたら x86_64 版のビルドで「aarch64 です」という印まで立っていたことが発覚。借りたヘッダの1つが無条件に立てていた。たまたま壊れていなかっただけ。

■何も変えずにつながった

aarch64 版と x86_64 版(qemu-x86_64 越し)を同じ機械で1つずつ起動。コードは1行も変えずにつながった。

[swim] node 0 discovered  (via rx)
[degrade] *** level change: FULL -> REDUCED  alive=2

通信部分のソースは両アーキテクチャで1バイトも違わないので当然といえば当然だけど、実際に見るとやっぱりうれしい。

■データを流すと自分の声しか聞こえない

K-DDS のトピックに、両方のノードから「n0 aarch64 t=1」みたいに2秒ごとに送って、相手のも受け取る小さなデモを作った。
が、最初はどっちにも自分が送ったものしか出ない。

原因は2つ。
・ネットワークの初期化(pmesh_init)が受け口の表を毎回ゼロにしていた。K-DDS は先に起動して登録しているので、あとから初期化されると登録ごと消える
・Linux 版がそもそも pmesh_init を呼んでいなかった

表を消す処理を外して起動順を直したら届いた。

[kdemo-rx] n0 aarch64 t=1
[kdemo-rx] n1 x86_64  t=1

同じ消し方のバグはベアメタルの x86 版にも潜んでいた。

■メモ

この日の実験は全部同じ機械の中。別々の機械をネットワークでつなぐのはまだ先。
ヘッダを共通にしたおかげで構造体の並びがそろっていて、相手のバイト列をそのまま読めることは確認できた。

※リポジトリ: https://github.com/monyuonyu/p-kernel

※この記事は、p-kernel の開発を手伝ってもらっている AI(Claude)と一緒に書いています。

自作OSを、Linuxの上で1つのプログラムとして動かしてみた

当時の開発ログを見返しながらのメモ。

2026-05-21、p-kernel が初めて Linux の上でふつうのプログラムとして起動した。

■ラズパイが買えなかった

前の日に ARM(aarch64)に移植したばかりで、次は Raspberry Pi 3B+ の実機で動かすつもりだった。
が、その月はラズパイを買う余裕がなかった。実機が無い。

そこで「Linux の上に寄生させて動かせないかな」とAIに相談。
Linux の中で Linux を動かす User-Mode Linux の p-kernel 版。これなら Linux を持っている人なら誰でも試せるし、Android の中身も Linux なのでスマホにもつながる。

そのとき打った一言がログにそのまま残っていた。

偉大な目標のため お願いします!

今読むとちょっと熱い…
名前は UMP(User-Mode p-kernel)。今の Linux 版・Android 版は全部ここから。結果的にはラズパイが買えなくてよかった。

■タスク切り替えはアセンブリで

ucontext を使えば楽だけど、POSIX で非推奨なので避けてアセンブリで書いた。aarch64 用で17命令。最初から2つのタスクが交互に動いた。

タイマー割り込みの代わりは 10ms ごとの SIGALRM。シグナルが来たときに、Linux が保存したレジスタ一式を別のタスクのものに書き換えておくと、シグナルから戻ったときには別のタスクになっている。

■止まった原因は2つ

T-Kernel 本体をつないだら起動の途中で止まった。

1. 割り込みを止める命令がベアメタル用の特権命令のままだった。ふつうのプログラムからは実行できないので見えないところで落ちていた。Linux 用はフラグを立てるだけの版に差し替え。
2. スタックが16バイト境界にそろっていなかった。aarch64 はそろってないとスタックに触った瞬間に落ちる。ベアメタルではたまたまそろっていただけ。

直したら出た。

[BOOT] Starting T-Kernel...
[T-Kernel] Initial task started
 p-kernel  [linux / aarch64 userspace]
  T-Kernel is alive inside a Linux process.

■x86_64 は一発

同じ日の夜に x86_64 版。aarch64 で踏んだ罠の直しが共通コードに入っていたので、最初のビルドでシェルのプロンプトまで来た。
(作業していたのは aarch64 の機械なので、確認は qemu-x86_64 越し)

■うれしいところ

1台の Linux で何個でも起動できる。同じ日のうちに2つ立ち上げて、SWIM でお互いを見つけるところまで確認。
実機を並べなくても分散の実験ができるのは大きい。

※リポジトリ: https://github.com/monyuonyu/p-kernel

※この記事は、p-kernel の開発を手伝ってもらっている AI(Claude)と一緒に書いています。

カーネルに多数決でリーダーを決めさせてみた

当時の開発ログを見返しながらのメモ。

3月22日の README の未実装リスト(Phase 10)に、Raft、分散 Attention、MoE、「自己増殖 — 新ノードが接続された瞬間に OS カーネルを自動プッシュ転送」が並んでいた。
4月4日の夜に raft.c・spawn.c・moe.c を追加。合わせて1250行くらい。

■Raft

何台かの中から1台のリーダーを多数決で選ぶ仕組み。
リーダーからのハートビートがしばらく来ないと立候補。1回の任期(term)で投票できるのは1票だけ。過半数の票を取れたらリーダーになって、100ms ごとにハートビートを出す。

本来の Raft は立候補までの待ち時間をランダムにして同時立候補を避けるけど、ここでは「300ms + ノード番号 × 50ms」と番号でずらしていた。
なので一番せっかちなのは node0。コミットに「node0 が term=1 で LEADER に就任することを QEMU で確認済み」とあるけど、ある意味予定どおり。

リーダーが配る「ログ」は、数字の key と value が最大16個で、受け取ったら表示するだけ。

■MoE

名前は大きいけど、中身はセンサーの温度を 20度未満・35度未満・それ以上 の3クラスに分けて、そのクラスで一番成績のいいノードに推論を回すだけ。成績は K-DDS の "moe/score" で共有。

■自己増殖(のつもりだった)

spawn.c がやっていたのは、新しいノードにクラスタの状態(任期、リーダー、各ノードの生死)を送ることだけ。カーネル本体は配っていない。README も「クラスタ状態を自動 push」に書き直した。

本当にカーネルを配れたのは翌朝の「ネットブート自己複製」。向きが逆で、ディスクのない新しい機械が小さなローダで起動して「カーネルください」と叫ぶと、動いているノードの kserve が kernel.elf を 1KB ずつ送り返す。

新ノード → "KLRQ"(ください)
kserve  → "KLRS"(始めます)
        → "KLRD" × N(1KB ずつ)
        → "KLRE"(終わり)

ディスクなしのノードが起動して、組み込みの自己テストも 5/5。
押しつけるより、欲しい側が頼みに来る方に落ち着いた。

■16バイトのずれ

翌々日、ネットワーク越しにカーネルを受け取る途中で止まる問題を修正。
NIC(RTL8139)のリング状の受信バッファを 8192+16 で折り返していたのが原因。実物は 8192 で折り返すので16バイトずれていた。直したら約9MB 受け取って起動できた。

※リポジトリ: https://github.com/monyuonyu/p-kernel

※この記事は、p-kernel の開発を手伝ってもらっている AI(Claude)と一緒に書いています。