2025/04/05

OpenHandsをローカルLLMで動かす

OpenHandsをローカルLLMで動かしてみます。

OpenHandsの準備


https://github.com/All-Hands-AI/OpenHands/のQuick Startに従って、dockerを動かすだけです。 Rootless dockerでも動作します。

/var/run/docker.sockを使ってOpenHandsがdockerを制御するようでしたので、今回は、KVM上でUbuntu24.04を動作させ、その中でAllHandsを動作させました。

ollamaの準備


ローカルLLMについてはollamaで動作させます。

ollamaもdockerイメージがhttps://hub.docker.com/r/ollama/ollamaで配布されているのでそれをそのまま利用します。 こちらもrootless dockerでも動作します。

$ docker run -d --gpus=all -v ollama:/root/.ollama -p 11434:11434 --name ollama ollama/ollama
を実行するとollamaが立ち上がります。
$ docker exec -it ollama bash
でdocker内に入って、ollamaを実行すると
root@xxxxxxxx:/# ollama
Usage:
  ollama [flags]
  ollama [command]

Available Commands:
  serve       Start ollama
  create      Create a model from a Modelfile
  show        Show information for a model
  run         Run a model
  stop        Stop a running model
  pull        Pull a model from a registry
  push        Push a model to a registry
  list        List models
  ps          List running models
  cp          Copy a model
  rm          Remove a model
  help        Help about any command

Flags:
  -h, --help      help for ollama
  -v, --version   Show version information

Use "ollama [command] --help" for more information about a command.
となり、ollamaが実行できることが確認できます。

ollamaのモデルの準備


試している環境ではGPUメモリが8GBなので、小さめのLLMを探してきます。

以下のものを試してみましたが、hhao/qwen2.5-coder-tools:7bがかろうじて動く程度で、ほかはOpenHandsで使うには適切ではないLLMでした。

hhao/qwen2.5-coder-tools:7b    1fbf62f22cd4    4.7 GB
codegemma:7b                   0c96700aaada    5.0 GB
llama3.2:3b                    a80c4f17acd5    2.0 GB
codellama:7b                   8fdf8f752f6e    3.8 GB
qwen2.5-coder:3b               e7149271c296    1.9 GB
opencoder:1.5b                 8573dfc23c16    1.4 GB
opencoder:8b                   cd882db52297    4.7 GB
もっと良いLLMがあるかもしれませんが、ここではhhao/qwen2.5-coder-tools:7bを使って進めます。runすると
/# ollama run hhao/qwen2.5-coder-tools:7b
>>> Send a message (/? for help)
と表示され、OpenHandsからアクセスできる状態になりました。試しにメッセージを入れてみると、
>>> Pythonでhelloと出力するスクリプトを作成してください
```python
print("hello")
```
とか
>>> rustでhelloと出力するコードを書いて、ビルドして実行する方法を書いてください
```json
{
  "name": "create_file",
  "arguments": {
    "filename": "main.rs",
    "content": "fn main() {\n    println!(\"Hello\");\n}"
  }
}
```
のように出力されました。日本語でも動きそうです。OpenHandsから呼び出すときにollama runが実行されている必要はありません。

OpenHandsの設定


KVM内からollamaにアクセスできることを確認します。
curl http://host.ip.address:11434/api/generate -d '{
  "model": "hhao/qwen2.5-coder-tools:7b",
  "prompt":"Write a bash script that prints hello"
}'
json形式の応答が返ってくれば、動作しています。

docker run -it --rm -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.29-nikolaik \
       -e LOG_ALL_EVENTS=true \
       -e LOG_JSON=false \
       -e LOG_LEVEL=DEBUG \
       -e DEBUG=true \
       -e OLLAMA_API_BASE=http://host.ip.address:11434 \
       -v /var/run/docker.sock:/var/run/docker.sock \
       -v ~/.openhands-state:/.openhands-state \
       -p 3000:3000 \
       --add-host host.docker.internal:host-gateway  \
       --name openhands-app \
       docker.all-hands.dev/all-hands-ai/openhands:0.29
host.ip.addressは適切な値に書き換えてください。

KVM内のブラウザでlocalhost:3000にアクセスします。 左下の⚙アイコンをクリックして、

Custom model: ollama/hhao/qwen2.5-coder-tools:7b
Base URL: http://host.ip.address:11434
API Key: なんでもOK
Agent: CodeActAgent
と設定します。

これで動くようになります。指示として例えば

Could you write a bash script that just prints "こんにちは" ?
を入力すると動き始め、下図のようになりました。
ただ、ファイルはつくれたもののその後はうまく動作しなくなってしまいました。

OpenHandsを活用するには、もっと賢いLLMが必要なようです。

2025/02/22

FLUX.1で画像生成

FLUX.1のSchnellモデルを使った画像生成をしてみます。

FLUXのオリジナルの実装


オリジナルの実装はこちらです。

モデル: https://huggingface.co/black-forest-labs/FLUX.1-schnell
コード: https://github.com/black-forest-labs/flux

指示にしたがって環境を設定し、実行すると、44.5GBのダウンロードがはじまります。

これは src/flux/util.py を読むと google/t5-v1_1-xxl をロードするようになっており、 https://huggingface.co/google/t5-v1_1-xxl/tree/main を見るとちょうどモデルサイズが44.5GBなので、おそらくこれをダウンロードしようとしているのだと思われます。

https://huggingface.co/black-forest-labs/FLUX.1-schnell/tree/main に配置されているテキストエンコーダが使われない理由は謎です。

ダウンロードサイズは大きいですが https://github.com/black-forest-labs/flux/issues/7 によると、16GBくらいVRAMがあればよさそうです。sequential offloadをすれば2GBくらいらしいのですが、cli.pyのオプションを見てもそれらしきものを見つけることができませんでした。

量子化版


とはいえもう少し小さいモデルでまずは動かしてみたいので、量子化済みのモデルで試してみます。いくつかHuggingFaceにアップロードされていますが、今回はhttps://huggingface.co/aifoundry-org/FLUX.1-schnell-Quantizedを使ってみます。

現時点では[WIP]と書かれているものの動かし方がいちおうは記載されているので、そのとおりに実行してみます。

最初に table-diffusion.cpp をダウンロードしてビルドします。

次に、 Schnell-Q2_KAutoencoderCLIP_LT5XXL をダウンロードして、models/以下に配置します。

これで準備は完了です。先ほどダウンロードした一番小さいモデル(Q2_K)で試してみます。

./build/bin/sd --diffusion-model models/flux1-schnell-Q2_K.gguf --vae models/ae.safetensors --clip_l models/clip_l.safetensors --t5xxl models/t5xxl_fp16.safetensors -p "a frog holding a sign saying 'hi' " -o ../frog.png -v --cfg-scale 1.0 --sampling-method euler -v --seed 42 --steps 4
を実行します。

以下の様な蛙の画像が出力されます。

他にも

./build/bin/sd --diffusion-model models/flux1-schnell-Q2_K.gguf --vae models/ae.safetensors --clip_l models/clip_l.safetensors --t5xxl models/t5xxl_fp16.safetensors -p "Photo-realistic Rabbit running in meadow" -o ../frog.png -v --cfg-scale 1.0 --sampling-method euler -v --seed 10 --steps 20
を実行すると

となります。

T5XXLはモデルサイズは大きいもののGPUメモリはあまり必要ないようで、デフォルトの512x512のサイズの画像生成時に観測できた範囲ではGPUメモリの使用量は4.7GBくらいでした。

./build/bin/sd --diffusion-model models/flux1-schnell-Q4_K.gguf --vae models/ae.safetensors --clip_l models/clip_l.safetensors --t5xxl models/t5xxl_fp16.safetensors -p "Photo-realistic Rabbit running in meadow" -o ../frog.png -v --cfg-scale 1.0 --sampling-method euler -v --seed 13 --steps 20
のようにQ2_KではなくQ4_Kを使ってみると
となります。

Q2_KではなくQ4_Kのモデルの方を使った場合、デフォルトの512x512のサイズの画像生成時に観測できた範囲ではGPUメモリの使用量は7.4GBくらいでした。

試した範囲では実行結果に再現性があり、コマンドの引数が同じであれば同じ結果が出力されるようです。

2025/02/10

Landlockによる制限をかけるときにGPUを使うには

Landlock (を使ったsandboxer) による制限をかけるときにnvidia-smiを実行するには、次のパスを許可すれば良いようです。

LL_FS_RW=$(ls /dev/nvidia* | tr '\n' ':' | sed 's/:$//') LL_FS_RO="/usr/lib:/usr/bin" ./sandboxer nvidia-smi
RO側はもう少し制限をかけることができるかもしれません。

nvidia-smiだけでなく、NVidiaのGPUを計算に利用するには、

LL_FS_RW="/proc:"$(ls /dev/nvidia* | tr '\n' ':' | sed 's/:$//') LL_FS_RO="/usr/lib:/usr/bin" ./sandboxer /path/to/app
のように/procを読み書き権限で追加する必要があるようです。 アプリケーション依存かもしれませんので、必ずしもこれだけで使えるようになるとは限りません。

2025/02/02

uvのインストール

最近のUbuntuやDebianのシステムに入っているPythonでpip installしようとすると、

error: externally-managed-environment
と言われ、venv環境を作れと言ってきます。

全体で使いたいパッケージをいちいちvenv環境に入ってインストールして、というのは手間なので、 エラーメッセージに書かれているとおり、Debian/Ubuntuのパッケージにある pipx をインストールします。

そのうえで、

$ pipx install uv
とすると、uvをインストールできて、全体でuvが使えるようになります。

編集距離の計算

編集距離(Edit Distance, Levenshtein Distance)の計算のライブラリは今となってはたくさんありそうですが、 いい感じに表示できる jiwer (https://jitsi.github.io/jiwer/) というPythonのライブラリがあったので、メモ。

こんな感じで書けます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import jiwer
import jaconv

def main():
    reference = "hello world"
    hypothesis = "hello duck"

    error = jiwer.wer(reference, hypothesis)
    print(error)
    error = jiwer.cer(reference, hypothesis)
    print(error)
    print("--------------")
    output = jiwer.process_characters(reference, hypothesis)
    print(jiwer.visualize_alignment(output))

    print("--------------")
    output = jiwer.process_characters("あいうえお", "あううお")
    prev_hyp = False
    for line in jiwer.visualize_alignment(output).split("\n"):
        zen = False
        if prev_hyp:
            prev_hyp = False
            zen = True
        if line[:4] == "REF:":
            zen = True
        if line[:4] == "HYP:":
            zen = True
            prev_hyp = True
        if zen:
            line = jaconv.h2z(line, kana=True, digit=True, ascii=True)
        print(line)

if __name__ == "__main__":
    main()
結果は
0.5
0.45454545454545453
--------------
sentence 1
REF: hello world
HYP: hello duck*
           SSSSD

number of sentences: 1
substitutions=4 deletions=1 insertions=0 hits=6

cer=45.45%

--------------
sentence 1
REF: あいうえお
HYP: あうう*お
      S D 

number of sentences: 1
substitutions=1 deletions=1 insertions=0 hits=3

cer=40.00%

のようになります。

2024/07/14

話者ベクトル

話者ベクトルをhttps://github.com/speechbrain/speechbrain.gitで取得してみます。

話者ベクトルの計算にはEncoderClassifier.encode_batchを使います。サンプリング周波数16kHzの音声データをこの関数に入力すると話者ベクトルが得られます。

それ以外の部分は音声の読み込みとt-SNEによる2Dベクトル化、散布図描画のための色の選択と、散布図の作成をしているだけです。

35行目でEncoderClassifierを作成して、52行目で話者ベクトルを取得します。

audio-16k.listは1行ごとに.wavファイルへのパスが書かれていることを想定しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import sys
from collections import defaultdict
import numpy as np
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
import torchaudio

sys.path.append("speechbrain")
from speechbrain.inference import EncoderClassifier

def make_colormap(label_list, min_count):
    color_map = defaultdict(int)
    for label in label_list:
        color_map[label] += 1

    color_map_sorted = []
    for k, v in color_map.items():
        color_map_sorted.append((v, k))
    color_map_sorted = reversed(sorted(color_map_sorted))

    colors = ["b", "g", "r", "c", "m", "y", "k", "C0", "C1", "C2", "C3", "C4", "C5", "C6", "C7", "C8", "C9"]
    cindex = 0
    for v, k in color_map_sorted:
        if v < min_count:
            color_map[k] = None
            continue
        color_map[k] = colors[cindex] if cindex < len(colors) else "grey"
        cindex += 1
    color_list = []
    for i in range(len(label_list)):
        color_list.append(color_map[label_list[i]])
    return color_list

def main(wav_list):
    ec = EncoderClassifier.from_hparams(
            source="speechbrain/spkrec-ecapa-voxceleb",
            savedir="cache",
            run_opts={"device": "cuda"},
            use_auth_token=False
         )
    ec.hparams.label_encoder.ignore_len()
    embed_list = []
    label_list = []
    with open(wav_list) as wavs:
        i = 0
        for line in wavs:
            i += 1
            line = line.strip()
            signal, fs = torchaudio.load(line)
            assert fs == 16000
            # Get a speaker vector
            embed_list.append(ec.encode_batch(signal, wav_lens=None).squeeze(dim=1).cpu().numpy())
            label_list.append(line.split("/")[1].split("_")[0]) # Get speaker name
            if i % 100 == 0:
                print(i)
    label_list = make_colormap(label_list, min_count=100)

    # Filter speaker vectors
    embed_list = [embed_list[i] for i in range(len(label_list)) if label_list[i] is not None]
    label_list = [label_list[i] for i in range(len(label_list)) if label_list[i] is not None]
    embed_list = np.concatenate(embed_list)

    tsne = TSNE(n_components=2, random_state=123)
    vec_2d = tsne.fit_transform(embed_list)

    plt.figure(figsize=(10, 8))
    plt.scatter(vec_2d[:,0], vec_2d[:,1], alpha=0.7, c=label_list)
    plt.title("t-SNE visualization of speaker vectors")
    plt.xlabel("t-SNE feature 1")
    plt.ylabel("t-SNE feature 2")
    plt.grid(True, linestyle="--", alpha=0.7)
    plt.savefig("spk_distribution.png", dpi=300, bbox_inches="tight")
    plt.close()

if __name__ == "__main__":
    main("audio-16k.list")
実行すると、以下のような散布図が得られます。少し混じっているところはあるものの、色ごとに、つまり話者ごとにクラスタが異なっていることが分かります。

2024/07/06

moreの挙動が変化した

Debianを11から12にアップグレードしたところ、moreコマンドのデフォルトの挙動が変化しました。

具体的には、表示するテキストの行が画面の行数よりも短いときは、単にcatで出力される状態であったのに、画面をクリアして画面の上部に表示されるようになりました。

lessでいうと、less -Fの挙動をしていたものが、less -cに似た挙動に変更されてしまいました。しかも、元の画面を削除して復旧しないという挙動です。

less -Fが手間なのでmoreを使っていたのに、これは困ったことになってしまいました。

Perplexityによると

Linuxのmoreコマンドの動作変更は、util-linux パッケージのバージョン2.38で導入されました。この変更により、表示する内容が端末の画面高さよりも少ない行数の場合でも、画面の上部から表示されるようになりました。 この変更は、より一貫性のある動作を提供し、ユーザーエクスペリエンスを向上させることを目的としています。以前のバージョンでは、内容が短い場合に画面の下部に表示されることがあり、これが一部のユーザーにとって混乱を招く原因となっていました。 util-linux 2.38は2022年4月にリリースされており、この版で上記の動作変更が実装されました。したがって、util-linux 2.37.2と2.38.1の間で発生したこの変更は、正確には2.38で導入されたものです。
とのこと。

moreコマンドが含まれているlinux-utilのバージョンは、Debian 11では2.36.1、Debian 12では2.38.1です。また、Ubuntu 22.04のmoreはDebian 11と挙動が同じで、そのバージョンは2.37.2です。

そこで、linux-utilのログを調べてみると、

2021-06-01	build-sys: release++ (v2.37)v2.37
2022-01-31	build-sys: release++ (v2.38-rc1)v2.38-rc1
2022-03-28	build-sys: release++ (v2.38)v2.38
でした。これらの間のどこかに変更が含まれているはずです。

調べてみると

commit df6b29d3b8e9a55d2bfa69c593e17da4d11a3fac
AuthorDate: Wed Sep 29 14:50:14 2021 +0200
CommitDate: Wed Sep 29 14:50:14 2021 +0200

    more: POSIX compliance patch preventing exit on EOF without -e
で-eオプションが導入されて、デフォルトの挙動が変わっていました。とても煩わしいです。

さらにその後のコミットをたどっていくと、なんと、

commit 28b391ce7e58f8327c092b3911c05f526d0ad586
AuthorDate: Wed Jun 15 10:03:44 2022 +0200
CommitDate: Wed Jun 15 10:03:44 2022 +0200

    more: restore exit-on-eof if POSIXLY_CORRECT is not set
    
    In version 2.38, exit-on-eof has been disabled by default. This change
    is annoying for users and forces many users to use 'alias more="more
    -e"'. It seems better to force POSIX lovers to use POSIXLY_CORRECT
    env. variable and stay backwardly compatible by default.
    
    Addresses: https://github.com/util-linux/util-linux/issues/1703
    Addresses: https://bugzilla.redhat.com/show_bug.cgi?id=2088493
 
がコミットされていて、POSIXでないとやだ!っていう人は環境変数POSIXLY_CORRECTを使いなさいという形に戻っていました。

Debian 12のパッケージ更新タイミングの運のなさよ。とりあえず、alias more="more -e"で回避するしか無さそうです。

2024/06/09

Wasserstein distance の発音

2つの確率分布間の距離を表す Wasserstein distance の発音は https://en.wikipedia.org/wiki/Wasserstein_metric によると

Most English-language publications use the German spelling "Wasserstein" (attributed to the name "Vaseršteĭn" (Russian: Васерштейн) being of Yiddish origin)
と書かれていて、Wassersteinはドイツ語スペルなので、そのまま読むと「ヴァッサーシュタイン」になるはず。

キリル文字の「Васерштейн」を変換表で変換すると「Vasershteyn」または「Vasershtein」になるので、これを英語読みすると「ヴァッサーシュタイン」。

AIのゴッドファーザーと呼ばれているなかの一人であるLeCunの投稿でも https://x.com/ylecun/status/991375003626233858

Hint: "Wasserstein", as in "Wasserstein distance" is pronounced "vassershtaeen" not "wassersteen".
とあるので、やはり「ヴァッサーシュタイン」。

一方、なぜか日本語文献を読むと Wikipediaでも 転移学習でも「ワッサースタイン」と書かれている。

どうして日本ではドイツ語表記したスペルの英語読みが広がっているんだろう?

Einsteinは「アインシュタイン」と呼んで、「アインスタイン」とは書かないのに、不思議である。