LLM-jp-4のtrust_remote_codeを固定して監査する
LLM-jp-4 33B を使うなら、重みを落とす前に trust_remote_code=True が何を読むのか固定しておいたほうがいい。ここを main のまま通すと、同じ起動コマンドでも翌週には別の実装を実行しうる。
8月18日に公開された LLM-jp-4 33B は、Dense 型で 64 layers、context length は 65,536。モデルカードの例でも trust_remote_code=True を指定している。モデルカード と Cookbook を追うと、これは飾りのフラグではない。LLM-jp-4 は独自 tokenizer と Harmony 形式まわりの実装を同梱している。
今日、Hub の tree を取りにいく小さいコマンドを書いていたら、手元の名前解決が先に落ちた。33B を取り始めてから気づく類の問題ではなかったので、ネットワークにつながらなくても通る自己テストを先に置いた。重みのダウンロードは、そのあとでいい。
固定するのは重みではなく、実行経路
trust_remote_code が有効なとき、まず見たいのは次の4種類だ。
| 対象 | 見る理由 |
|---|---|
*.py |
独自 tokenizer や model 実装が入る |
*.jinja |
chat template が入力トークン列を決める |
config.json |
auto_map など読み込むクラスを決める |
tokenizer_config.json |
template と tokenizer の設定を持つ |
ここで固定したいのは重みではなく、実行経路だ。
下のスクリプトは Hugging Face の tree API からこの4種類だけを抜き出し、ファイルごとの oid と commit SHA を JSON に残す。標準ライブラリだけで動く。main や tag は受け取らず、40桁の commit SHA だけを受け取るようにした。
#!/usr/bin/env python3
import argparse
import json
from urllib.parse import quote
from urllib.request import Request, urlopen
TARGET_NAMES = {"config.json", "generation_config.json", "tokenizer_config.json"}
TARGET_SUFFIXES = (".py", ".jinja")
def is_execution_relevant(path: str) -> bool:
return path.rsplit("/", 1)[-1] in TARGET_NAMES or path.endswith(TARGET_SUFFIXES)
def build_lock(model: str, revision: str, entries: list[dict]) -> dict:
files = []
for entry in entries:
path = entry.get("path", "")
if entry.get("type") != "file" or not is_execution_relevant(path):
continue
oid = entry.get("oid")
if not isinstance(oid, str) or not oid:
raise ValueError(f"oid がないため固定できません: {path}")
files.append({"path": path, "oid": oid})
if not files:
raise ValueError("実行に関係する .py / .jinja / 設定JSON が見つかりません")
return {
"model": model,
"revision": revision,
"files": sorted(files, key=lambda item: item["path"]),
}
def fetch_tree(model: str, revision: str) -> list[dict]:
safe_model = quote(model, safe="/")
safe_revision = quote(revision, safe="")
url = (
f"https://huggingface.co/api/models/{safe_model}"
f"/tree/{safe_revision}?recursive=true&expand=false"
)
request = Request(url, headers={"Accept": "application/json"})
with urlopen(request, timeout=20) as response:
payload = json.load(response)
if not isinstance(payload, list):
raise ValueError("Hub API の応答が配列ではありません")
return payload
def self_test() -> None:
sample = [
{"type": "file", "path": "config.json", "oid": "config-oid"},
{"type": "file", "path": "llmjp4_tokenizer.py", "oid": "tokenizer-oid"},
{"type": "file", "path": "chat_template.jinja", "oid": "template-oid"},
{"type": "file", "path": "model-00001.safetensors", "oid": "weight-oid"},
]
lock = build_lock("example/model", "a" * 40, sample)
assert [item["path"] for item in lock["files"]] == [
"chat_template.jinja", "config.json", "llmjp4_tokenizer.py"
]
print("self-test: 3 files selected")
print("self-test: OK")
parser = argparse.ArgumentParser()
parser.add_argument("--model")
parser.add_argument("--revision")
parser.add_argument("--output", default="remote_code_lock.json")
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
if args.self_test:
self_test()
elif not args.model or not args.revision:
parser.error("--model と --revision を指定してください")
elif len(args.revision) != 40 or any(c not in "0123456789abcdef" for c in args.revision.lower()):
parser.error("--revision には40桁のGit commit SHAを指定してください")
else:
lock = build_lock(args.model, args.revision, fetch_tree(args.model, args.revision))
with open(args.output, "w", encoding="utf-8") as f:
json.dump(lock, f, ensure_ascii=False, indent=2)
f.write("\n")
print(f"locked {len(lock['files'])} files -> {args.output}")
掲載コードは Python 3.14 で次の出力を確認した。
$ python audit_hf_remote_code.py --self-test
self-test: 3 files selected
self-test: OK
実行前に lock を作る
Hub の「Files and versions」で commit SHA を選び、その値を渡す。main を渡せないので、ブランチ更新をうっかり本番へ持ち込まない。
python audit_hf_remote_code.py \
--model llm-jp/llm-jp-4-33b-thinking \
--revision <Files_and_versionsで選んだ40桁のcommit_SHA>
git add remote_code_lock.json
remote_code_lock.json は重みのハッシュ一覧ではない。更新時には旧ファイルと diff を取り、llmjp4_tokenizer.py、template、auto_map のどれが変わったかをレビューする。この差分が空なら、少なくとも remote code の実行経路は前回と同じだと確認できる。
Cookbook は thinking と instruct で独自 tokenizer、template、token 単位の出力 parsing に注意が必要だとしている。つまり trust_remote_code を外せば済む話ではない。外すなら、同梱コードを自前で取り込み、その版も同じように固定する必要がある。
おわりに
LLM-jp-4 33B の価値は、重みだけでは動かないところにもある。カスタム実装を有効にするなら、許可の一回で終わらせず、どの commit のどのファイルを読んだかをリポジトリに残す。これならモデル更新時のレビューは数十GBの差分ではなく、数個のコードと設定に絞れる。推論サーバーを速く立てる前に、この lock をCIで比較するところから始めるのが実務では効いた。