コンテンツにスキップ

はじめての実行

このチュートリアルでは、自分で作る小さなランダム初期化のモデルを使って、主なコマンドをすべて動かします。ダウンロードもGPUも不要で、ノートPCでも1分ほどで終わります。

この章でやること:

  1. 2層のLlama型モデル(320,896パラメータ)を作る
  2. 調査し、量子化の計画を確認する
  3. 三値に量子化する
  4. 重みを2bitに詰め、元に戻し、結果を検査する
  5. 品質を測り、三値モデルと元のモデルを比較する
  6. TUIで結果を見る

なぜランダムなモデルを使うのか

実際のモデルは、数分から数時間と数GBのディスクが必要です。小さなモデルなら各ステップが一瞬で終わるので、各コマンドが何をして何を書き出すかをすぐ確認できます。出力は意味のない文字列ですが、それが ステップ6で役に立ちます。OpenTernaryがこれを品質の結果として認めない様子を確認できるからです。

以下の出力はすべて、LinuxのCPU版PyTorch 2.15.0とOpenTernary 0.1.0a2(OPENTERNARY_LANG=ja)で実際に実行したものです。同じバージョンなら、調査とcontentのfingerprint(sha256:a1fd…、sha256:2a25…)は一致します。 manifestのfingerprintにはパスや時刻が含まれるので一致しません。時間も環境によって変わります。

始める前に

OpenTernaryとPyTorchが入っている必要があります(インストール)。GPUがなければ openternary setup --backend cpu で十分です。

空のフォルダを作って、その中で作業します:

mkdir openternary-demo
cd openternary-demo

1. 小さなモデルを作る

次のスクリプトを make_tiny_model.py として保存します:

make_tiny_model.py
"""Create a tiny, randomly initialised Llama model for trying OpenTernary offline.

The weights are random: the model produces gibberish. It exists only so that every
command in the tutorial can run on a laptop CPU in seconds, without downloads.
"""

import json
import sys
from pathlib import Path

import torch
from tokenizers import Tokenizer, models, pre_tokenizers
from transformers import LlamaConfig, LlamaForCausalLM, PreTrainedTokenizerFast

out = Path(sys.argv[1] if len(sys.argv) > 1 else "tiny-llama")
out.mkdir(parents=True, exist_ok=False)
torch.manual_seed(0)

# A character-level tokenizer: every printable ASCII character is one token.
specials = ["<unk>", "<s>", "</s>", "<pad>"]
vocab = {token: i for i, token in enumerate(specials + [chr(c) for c in range(32, 127)])}
backend = Tokenizer(models.WordLevel(vocab=vocab, unk_token="<unk>"))
backend.pre_tokenizer = pre_tokenizers.Split("", behavior="isolated")
tokenizer = PreTrainedTokenizerFast(
    tokenizer_object=backend, unk_token="<unk>", bos_token="<s>", eos_token="</s>", pad_token="<pad>"
)
tokenizer.chat_template = "{% for m in messages %}{{ m['content'] }}{% endfor %}"
tokenizer.save_pretrained(out)

config = LlamaConfig(
    vocab_size=len(vocab),
    hidden_size=128,
    intermediate_size=256,
    num_hidden_layers=2,
    num_attention_heads=4,
    num_key_value_heads=2,
    max_position_embeddings=512,
    bos_token_id=1,
    eos_token_id=2,
    pad_token_id=3,
    tie_word_embeddings=False,
)
LlamaForCausalLM(config).save_pretrained(out)
# Marks the model as synthetic: OpenTernary will never report quality acceptance for it.
(out / "openternary-fixture.json").write_text(json.dumps({"pretrained": False, "family": "llama", "seed": 0}))
print(f"wrote {out}")

このスクリプトには torch と transformers が必要です。どちらも openternary setup が入れた環境にあるので、その環境のPythonで実行します:

& "$(uv tool dir)\openternary\Scripts\python.exe" make_tiny_model.py
"$(uv tool dir)/openternary/bin/python" make_tiny_model.py
uv run python make_tiny_model.py
wrote tiny-llama

tiny-llama フォルダは、Hugging Faceからダウンロードしたモデルと同じ形をしています。config.json、 model.safetensors、トークナイザのファイルに加えて、このモデルが人工のものであることを示す openternary-fixture.json が1つ入っています。

2. 調査と計画

inspect は、既定ではsafetensorsファイルのヘッダだけを読みます。すべてのテンソルを一覧し、どれを量子化できるか判定します。重みは読み込みません:

openternary inspect tiny-llama
                Inspection — tiny-llama
┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Section               ┃ Value                       ┃
┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Revision              │ unknown                     │
│ Adapter               │ transformers                │
│ Architecture          │ ['LlamaForCausalLM']        │
│ Text layers           │ ?                           │
│ Hidden / Intermediate │ ? / ?                       │
│ Vocab                 │ ?                           │
│ Total tensors         │ 21                          │
│ Quantizable           │ 14 (0.919027)               │
│ Total params          │ 320,896                     │
│ Quantizable params    │ 294,912                     │
│ BF16 GB               │ 0.0012                      │
│ FP32 GB               │ 0.0012                      │
│ Fingerprint           │ sha256:a1fd8a0cf17a9fd22... │
└───────────────────────┴─────────────────────────────┘
Inspection completed: /home/user/openternary-demo/runs/2026-11-03_tiny-llama-naive-g128
inspection.json saved to /home/user/openternary-demo/runs/2026-11-03_tiny-llama-naive-g128/inspection.json

21個のテンソルのうち14個が量子化の対象です。2つのデコーダ層それぞれにある7つの線形層(q_proj・k_proj・ v_proj・o_proj・gate_proj・up_proj・down_proj)で、パラメータの91.9%を占めます。埋め込み・出力層・正規化の重みは元の精度のまま残ります。? の行は、汎用のTransformers用アダプタが読まない項目で、Gemma 4用のアダプタでは値が入ります。

plan はさらに一歩進めて、quantize が何をするかを、やはり何も読み込まずに示します:

openternary plan tiny-llama
╭─ 計画(ダウンロード・読込・書込なし) ──────────╮
│ 項目               値                           │
│ ─────────────────────────────────────────────── │
│ モデル             tiny-llama                   │
│ backend / 方式     ternary / absmean → ternary  │
│ scale / group      per_tensor / g128            │
│ アダプタ           transformers                 │
│ 対象               14 / 21                      │
│ デバイス / dtype   cpu / source dtype preserved │
│ pass               —                            │
│ オフラインでは未確定                            │
│   ? hardware/runtime execution                  │
│   ? artifact reload                             │
│   ? model quality                               │
╰─────────────────────────────────────────────────╯

大事なのは最後の3行です。計画からは、結果が動くか・読み込み直せるか・品質がよいかは分かりません。分かるのは測定したときだけです。

3. 三値に量子化する

既定ではテンソルごとに1つのscaleを使います。グループごとのscaleのほうが重みに忠実なので、64個ずつのグループにします。まず --dry-run で確認します。何も作られません:

openternary quantize tiny-llama --scale-granularity per_group --group-size 64 --device cpu --dry-run
Planned: scale_granularity=per_group group_size=64
grouping_scheme=last-dim-rowwise-v1 method=naive
No run directory was created (--dry-run).

実際に実行します:

openternary quantize tiny-llama --scale-granularity per_group --group-size 64 --device cpu --output runs/tiny-ternary
                 Quantize — tiny-llama
┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Section               ┃ Value                       ┃
┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Scale granularity     │ per_group                   │
│ Group size / scheme   │ 64 / last-dim-rowwise-v1    │
│ Quantizable tensors   │ 14                          │
│ Total groups          │ 4608                        │
│ Shards                │ 1                           │
│ Tensor payload        │ 1283584                     │
│ Shard file bytes      │ 1285736                     │
│ Packed estimate bytes │ 196096                      │
│ Content FP            │ sha256:2a25a7bffeeb0d010... │
└───────────────────────┴─────────────────────────────┘
Quantize completed: /home/user/openternary-demo/runs/tiny-ternary
ternary artifact: /home/user/openternary-demo/runs/tiny-ternary/artifacts/snapshot

量子化の対象の重みは、グループごとに \(\{-s, 0, +s\}\) の3値のどれかになりました(\(s\) はそのグループの scale)。ただし、まだふつうの浮動小数点数として保存されているので、snapshotは元のモデルと同じ大きさ(1,285,736バイト)です。このfake-quantの形式は、どのTransformersのコードでも読み込めるので、そのまま評価できます。Packed estimate bytes は、同じ重みを1つ2bitで保存し、scaleと量子化しないテンソルを加えた大きさです。計算方法は三値量子化で説明しています。

runs/tiny-ternary は**実行フォルダ(run)**です:

runs/tiny-ternary/
├── config.yaml          # 実際に使った、すべて解決済みの設定
├── environment.json     # バージョン、gitのcommit、ハードウェア、シード
├── model.json           # モデルIDとrevision
├── metrics.json         # 状態と主な数値
├── quantization.json    # テンソルごとの詳細とfingerprint
├── logs/
└── artifacts/snapshot/  # 量子化したモデル(ふつうのHFモデルとして読める)
    ├── model-00001-of-00001.safetensors
    ├── openternary-manifest.json   # 形式、来歴、全ファイルのSHA-256
    └── config.json、トークナイザのファイルなど

各ファイルの説明は実行フォルダのリファレンスにあります。

4. packed化・復元・検査

三値の重みを1つ2bitに詰めます:

openternary export runs/tiny-ternary --format ternary-packed --output runs/tiny-packed
項目                  値
────────────────────────────────────────
artifact              /home/user/openternary-demo/runs/tiny-packed
manifest              schema_version=1, format=ternary-packed,
                      execution=completed, artifact_validation=file_integrity,
                      model_reload=not_run, quality_acceptance=not_run,
                      manifest_fingerprint=5e15f72a740a53eb777c57da17528fde3c52…
artifact_validation   file_integrity

テンソルのデータは1,285,736バイトから198,600バイトへ、約6.5分の1になります。残りの大部分は、元の精度のままの埋め込みと出力層です。packedの成果物はまだ直接実行できないので、safetensorsに戻します:

openternary export runs/tiny-packed --format safetensors --output runs/tiny-restored

そして念入りに検査します。--level tensors はすべてのテンソルを開き、形・dtype・有限値かどうかをmanifestと照合します:

openternary artifacts validate runs/tiny-restored --level tensors
項目                   値
────────────────────────────────────────
artifact               /home/user/openternary-demo/runs/tiny-restored
artifact_validation    tensor_integrity
manifest_fingerprint   62adeab647febbb4eef8c923bfdee60b41504c38ded7db75aed1aaa9…
tensor_count           21
model_reload           ○ not run
quality_acceptance     ○ not run

復元したテンソルは、fake-quantのsnapshotとバイト単位で一致します。三値の重みのpacked化は可逆です。結果が 主張していないことにも注目してください。model_reload と quality_acceptance は ○ not run です。ファイルの整合性から分かるのはファイルが壊れていないことだけで、モデルが動くことではありません。

5. ベンチマークで振る舞いを比べる

smokeベンチマークは、固定の5つのプロンプトをgreedyで生成し、生成されたトークンをすべて記録します:

openternary benchmark tiny-llama --device cpu --dtype fp32 --output runs/bench-original
openternary benchmark runs/tiny-ternary/artifacts/snapshot --device cpu --dtype fp32 --output runs/bench-ternary
openternary compare runs/bench-original runs/bench-ternary --output runs/compare-bench
     Compare — behavioral delta (observational)
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Metric              ┃ Value                       ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Protocol match      │ True                        │
│ Result observation  │ different                   │
│ Result same?        │ False                       │
│ Content match       │ None                        │
│ Baseline result FP  │ sha256:6c4af247d0a4f7029... │
│ Quantized result FP │ sha256:c5b2dce616da98b50... │
└─────────────────────┴─────────────────────────────┘
      Per-prompt token diff
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┓
┃ Prompt                 ┃ Same? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━┩
│ smoke.code.python      │ False │
│ smoke.en.short         │ False │
│ smoke.ja.short         │ False │
│ smoke.ja.summary       │ False │
│ smoke.reasoning.simple │ False │
└────────────────────────┴───────┘
Result fingerprint equality is observational, NOT pass/fail.

Protocol match: True は、2つのrunがまったく同じプロンプトと生成設定を使ったことを意味します。つまり公平な比較です。出力はすべてのプロンプトで異なりました。これは観測であって判定ではありません。違う文章が悪い文章とは限らないからです。品質を判断するには、次のステップが必要です。

6. 品質を測って比較する

quality は、固定したデータセットのファイルを使って、英語と日本語のパープレキシティを測り、指示ケースを実行します。わざと小さくした次の例を quality-mini.json として保存してください。実際のデータセットにはもっと多くのレコードが必要です。形式は評価と比較で説明しています:

quality-mini.json
{
  "schema_version": 1,
  "dataset": {
    "id": "example/quality-mini",
    "revision": "v1",
    "license": "CC0-1.0",
    "source": [{"id": "handwritten", "revision": "v1"}]
  },
  "splits": {
    "calibration": [
      {"id": "cal-1", "text": "The river carried small boats past the old mill every morning.", "language": "en"}
    ],
    "validation": [
      {"id": "val-en-1", "text": "Tea was first grown in the hills, where the air stays cool and wet all year.", "language": "en"},
      {"id": "val-ja-1", "text": "駅の近くには小さな本屋があり、週末になると多くの人が集まります。", "language": "ja"}
    ],
    "test": [
      {"id": "test-en-1", "text": "A lighthouse keeper wrote down the weather twice a day for forty years.", "language": "en"},
      {"id": "test-ja-1", "text": "冬の朝は空気が澄んでいて、遠くの山まではっきりと見えます。", "language": "ja"}
    ]
  },
  "instruction_cases": {
    "validation": [
      {"id": "val-case-1", "prompt": "Reply with only the word: blue", "expected": "blue"}
    ],
    "test": [
      {"id": "test-case-1", "prompt": "Reply with only the number: 7", "expected": "7"}
    ]
  }
}
openternary quality tiny-llama --data quality-mini.json --device cpu --dtype fp32 --output runs/quality-original
openternary quality runs/tiny-ternary/artifacts/snapshot --data quality-mini.json --device cpu --dtype fp32 --output runs/quality-ternary
openternary compare runs/quality-original runs/quality-ternary --require-acceptance --output runs/compare-gate

主な数値(各runの quality.json より):

英語のパープレキシティ 日本語のパープレキシティ 指示スコア
元のモデル 111.90 81.60 0
三値 105.36 78.13 0

三値モデルのほうがパープレキシティが低くなっています。実際の重みでこうなれば驚くべきことですが、ここでは意味がありません。元のモデルがランダムなノイズで、それを量子化したものもノイズだからです。そのため compare はこれを受け入れません:

╭─ ✖ 受入条件を満たしていない ─────────────────────────────────────────────────────────╮
│ required quality acceptance failed or evidence is incomplete                         │
╰─────────────────────────────────────────────────────────────────────── 終了コード 6 ─╯

理由は runs/compare-gate/acceptance.json に書かれています:

{
  "accepted": false,
  "status": "insufficient_evidence",
  "reason": "schema 3 with matched source/interface/runtime and non-synthetic model evidence required"
}

品質Gateが受け入れるのは、人工のものではないモデルを、同じ元モデル・トークナイザ・デバイス・dtype・ソフトウェアで測った場合だけです。--require-acceptance を付けると、Gateを通らなければ終了コード6で終わるので、スクリプトやCIはそこで止まります。なぜここまで厳しくしているかは 実行結果と証拠で説明しています。

7. TUIで見る

同じフォルダで、引数なしの openternary を実行します。実行結果タブに、ここまでに作ったものが、各runのファイルと一緒に一覧されます:

実行結果タブ: チュートリアルで作ったrunの一覧と、選択中のrunの詳細とファイル

/quantize と入力して Enter を押すと、同じ量子化をフォームで指定し、計画を確認して、バックグラウンドのジョブとして実行できます。やり方は全画面インターフェースで説明しています。

片付け

作ったものはすべて最初に作ったフォルダの中(tiny-llama/ と runs/)にあります。終わったらフォルダごと削除してください。

次に読むもの