
Nemotron 3.5 Lightning を LoRA 事後学習で LLM ルーターの判定役に仕立ててみた
はじめに
こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。
先日の first-touch 記事で測った Nemotron 3.5 Lightning、あのあと実務にどう組み込めていけるのかが気になっている方もいるのではないでしょうか。1M コンテキストの 30B MoE というスペックもさることながら、このモデルは事後学習(学習済みモデルに追加の学習を重ねて、特定の用途に仕立て直すこと)のベースとして使われることを強く意識した設計になっています。今回はその看板を実測で確かめるべく、自分が普段使っている LLM ルーターの「判定役(capability judge)」に LoRA SFT で仕立ててみました。
判定役というのは、コーディングエージェントからのリクエストを読んで「このタスクは安いモデル(weak)で足りるか、高いモデル(strong)が要るか」を毎回判定するモデルのことです。自分は NeMo Switchyard というルーターでこの構成を運用していて、判定役には DGX Spark 2 台で動かしている deepseek-v4-flash-0731 を使ってきました(2 台構成は下記記事で扱っています。2026-08 時点の記事です)。ただ、このモデルは 284B あります。毎リクエスト走る判定のために 2 台がかりの大物を据えておくのは、席として明らかに重すぎます。判定専任の軽いモデルに置き換えられたら、と前から思っていました。
先に結論を書いておくと、87 件の実機ルーティング評価で蒸留元の flash-0731 を上回り(89.7% → 94.3%)、284B が務めていた判定の席を 21GB の 30B に置き換えられました。教師モデルには構造的に判定できなかった難問の振り分けも部分的にできるようになっています。ただしそこに至る途中で、学習したモデルが本番経路でだけ暴走するという不可解な事件がありました。原因は JSON のキー順です。この顛末が今回いちばん共有価値のある学びだと思っています。
過去記事の続きにあたる内容なので、Lightning そのものの素性は first-touch 記事を、ルーター側の仕組みは Switchyard の記事をご覧ください(いずれも 2026-08 時点の記事です)。
この記事では、教師データの設計から LoRA SFT、NVFP4 量子化、実機ルーティング評価までの一連の流れと、途中で踏んだ落とし穴を紹介します。ローカル LLM を「測る」の次の段階、業務ワークロードに「仕立てる」を考えている人に刺さるといいなと思っています。
素の Lightning は judge としてそのままでは使えない
まず全体の構成です。ルーターの中で judge がどこにいるかだけ掴んでもらえれば十分です。
judge はリクエストごとに「このタスクを weak モデルが一発で完遂する確率(p_solve)」を 4 フィールドの JSON で返し、ルーターがしきい値と比べて行き先を決めます。判定は毎リクエスト走るので、速さと安さと形式の堅牢さがすべて要求される地味にシビアな部分です。
ここに素の Lightning をそのまま入れるとどうなるか。学習には使わず検証用に取り分けたホールドアウト 237 件で、現行 judge の flash-0731 と比べた結果が以下です。
| 指標 | flash-0731(現行) | 素の Lightning |
|---|---|---|
| JSON の妥当性検査通過 | 237/237 | 212/237 |
| 判定カテゴリの一致 | —(これが正解基準) | 17.3% |
| weak/strong 判定の一致 | — | 72.2% |
数字以上に深刻だったのは中身で、237 件中 234 件を「unmatched(判定不能)」カテゴリに振ってしまい、実運用形状のリクエスト 10 件では全件が strong 行きになりました。全部高いモデルに流れるなら、ルーターの意味がありません。フォーマットにはだいたい従えるのに、judge としての語彙と確率感覚がまるごと欠けている状態です。ここが事後学習の出番ですね。
教師データは実測の正解率から作る
教師データというのは、モデルに真似させたい入力と出力のお手本ペアのことです。今回は 2 系統用意しました。設計の核は、お手本側の p_solve の値(ラベル)に weak モデルの実測の正解率をそのまま使うことです。
以前 NVIDIA LLM Router v3 を再訓練したとき、LLM に正解判定させたラベルのバグとバイアスで痛い目を見ました(2026-07 時点の記事です)。その反省から、今回はコーディングベンチマーク 195 問を実際に weak モデルへ 2 回ずつ解かせ、通った頻度をラベルにしています。判定の語彙(判定カテゴリと根拠の一文)は、現行 judge の flash-0731 の判定をお手本として写し取りつつ(これがいわゆる蒸留です)、肝心の確率だけは実測に差し替える構成です。
| 系統 | 件数 | ラベルの出どころ |
|---|---|---|
| コーディングベンチ実測 | 195 問(教師の応答が壊れた 1 問を除き、学習 167 + 検証 27) | weak の実測正解率 + 難度帯の平滑化 |
| 合成タスク | 2,085 問(学習 1,875 + 検証 210) | flash-0731 の判定を蒸留 |
合成側は「壁打ち相談」「曖昧な整形依頼」「画像頼みの依頼」など 8 アーキタイプを誘導して生成し、実測側だけでは全件 supported になってしまう判定カテゴリの偏りを埋めています。生成と蒸留のコールは合計 4,000 回ほどで、現行 judge の flash-0731 にそのまま流して作っています。
できあがった教師データ 1 件はこんな見た目です。入力にはタスクの指示文と、weak モデルの得意不得意を整理した capability カードが入り、出力のお手本として 4 フィールドの JSON を与えます。
{
"crux": "Sort each row descending, then for each column take the maximum value across rows and sum those maxima.",
"primary_rule": "SUP-3",
"capability_boundary": "supported",
"p_solve": 0.88
}
crux は解き筋の要約、primary_rule は判定根拠のルール ID、capability_boundary が判定カテゴリ、p_solve が weak の完遂確率です。モデルはこのお手本を数千件なぞって、judge の語彙と確率感覚を身につけていきます。そして、この 4 フィールドの並びが後半の事件の伏線になります。
もうひとつ地味に大事なのが評価セットの凍結です。weak が 2 回連続で落ち、strong なら通った「見極め 14 問」を含む評価用データは、ハッシュ照合で学習データから 1 件残らず除外しています。あとで「蒸留元を超えたか」を判定する試験問題なので、これが漏れたら台無しですからね。
公式 cookbook の LoRA SFT で二度学ぶ
学習は Nemotron リポジトリの公式 cookbook(Megatron-Bridge の LoRA レシピ)をベースに、データセット部分だけ judge 用に差し替えました。LoRA は 30B の重み全体を書き換える代わりに、小さな差分だけを追加で学習する方式です。そして SFT は、前章のお手本ペアをそのまま真似させる教師あり学習を指します。この組み合わせなら、大きなモデルでも短時間・低コストで仕立て直せます。ハードは NVIDIA Brev で借りた H100 2 枚、設定は rank 8 / alpha 32 / 2 epoch / シーケンス長 3072 で、1 走あたり学習 46 分 + 差分を元の重みへ合成するマージが 2 分です。GPU 代にすると 1 走 10 ドル前後で済みます。
最初の学習(v0)で、形式面は一発で治りました。JSON の妥当性検査は 212/237 から 237/237 になり、判定カテゴリの一致も 17.3% から 73.4% まで跳ねています。ところが検証用に取り分けたベンチ問題のうち、strong に振るべき難問 11 問を 1 問も strong に振れていませんでした。原因を追うと、教師の flash-0731 自身がこの形状の問題を全部「weak で解ける」と楽観判定する盲点を持っていて、蒸留はその盲点まで忠実に写していたのです。せっかく実測ラベルを用意したのに、合成データ側の楽観的な蒸留ラベルに数で押し負けていました。
そこで 2 走目(v1)は配合を修正します。実測ラベルのベンチ問題を 6 倍に複製し、楽観バイアスの強い合成カテゴリを 1 つ除外しました。結果は次のとおりです。
| 指標 | 素の Lightning | v0(初期配合) | v1(配合修正) |
|---|---|---|---|
| JSON の妥当性検査通過 | 212/237 | 237/237 | 237/237 |
| weak/strong 判定の一致 | 72.2% | 78.1% | 80.2% |
| p_solve の平均絶対誤差 | 0.225 | 0.151 | 0.139 |
| 検証データの難問を strong に振れた数 | 0/11 | 0/11 | 2/11 |
データの配合だけで、難問を見極める力が動き始めました。学習レシピは一切触っていません。事後学習の成否はレシピよりデータ設計、というよく聞く話をそのまま体験した形です。
学習環境まわりの壁物語(nemo コンテナ の 2 つの壁)
壁 1: cookbook 指定のコンテナが外部から取得できない
cookbook が指定する nvcr.io/nvidian/nemo:26.08 は NVIDIA 社内レジストリのイメージで、外部からは pull できません。public の nvcr.io/nvidia/nemo:26.06 に切り替えたうえで、Lightning 用の学習レシピ 4 ファイルを Megatron-Bridge の未マージブランチから checkout して差し込みました。レシピ内にハードコードされた社内ストレージのパスも手元の checkpoint パスへ書き換えが必要です。公式 cookbook でも「そのまま動く」とは限らず、コンテナとレシピの出どころ確認が先というのが学びです。
壁 2: 26.06 と cookbook の API 差分
cookbook の train スクリプトは 26.08 系の API 前提で、26.06 ではデータセット設定まわりが 3 箇所ほど変わっています。既成の training.jsonl を読ませるためにダミーの dataset 指定で前処理をスキップさせる小改造をしました。
JSON のキー順が judge を暴走させていた
ここからが本題です。v1 を実際のルーター経由で動かすと、直叩きでは無傷なのに、実運用形状のリクエストの約 18% で応答が壊れる現象が出ました。壊れ方は毎回同じで、JSON の最終フィールドまで出力したあと、空白文字だけを上限の 4096 トークンまで延々と吐き続けます。判定は失敗扱いとなり、フェイルセーフでリクエストは strong 行きになるので実害は静かですが、判定 1 回に 52 秒かかっていては、判定席を移す意味がありません。
ルーターを通したときだけ起きる。そこで vLLM に届いたリクエストをルーター経由と直叩きで diff したところ、差分は response_format 内の JSON スキーマのキー順だけでした。ルーター(Rust 実装)は設定を再直列化するときにオブジェクトのキーをアルファベット順に並べ替えます。一方、vLLM 側で structured output(出力を指定スキーマどおりの JSON に強制する仕組み)を担う xgrammar は、スキーマの properties の並び順をそのまま生成順として強制します。そして v1 の学習データ 2,610 件は、ラベル JSON が全件同一のキー順でした。
つまり、モデルは「crux から始まり p_solve で終わる」トークン列として判定 JSON を暗記していた。そこへ文法エンジンがアルファベット順(p_solve が途中に来る並び)を強制すると、モデルは自分の知っている出口を失って空白を吐き続ける。アルファベット順のスキーマを直叩きで再現すると 10 回中 4 回暴走し、学習時と同じ自然順なら 53 回連続で無傷でした。
対処は両輪で入れました。ルーター側にはキー順を保存する 1 行修正の PR を上流に提出。そしてモデル側は、学習データのラベル JSON のキー順だけをサンプルごとにシャッフルして再学習します(v2)。意味内容は 1 ビットも変えず、4 フィールドの並び 24 通りをまんべんなく踏ませるだけのデータ拡張(augmentation)です。再学習のコストは v1 と同じ 1 走 10 ドル前後でした。
効果は明快で、キー順を 5 通りに振った 200 コールのプローブで暴走は 0 件、ルーター直結の実機 100 コールでも 0 件です。判定品質は v1 と同水準(weak/strong 一致 80.2% → 81.4%)を保っています。
| 経路 | v1 | v2(キー順シャッフル学習) |
|---|---|---|
| アルファベット順スキーマ直叩き | 4/10 で暴走 | 0/40 |
| ルーター直結の実機コール | 約 18% で暴走 | 0/100 |
structured output を前提に JSON を出力させる fine-tune では、キー順は「意味を持たないはずの実装詳細」ではなく契約の一部です。しかも OpenAI 互換のプロキシやルーターは JSON を再直列化するものが多く、キー順はいつでも変わりえます。ラベルのキー順を固定したまま学習する限り、この地雷はどの構成でも踏みうるので、出力 JSON を学習させるならキー順シャッフルを最初から入れておくのが安全だと思います。
NVFP4 に量子化して DGX Spark に載せる
学習済みモデルは BF16 で 62GB あります。DGX Spark の 128GB メモリに載ることは載るのですが、実運用の長いコンテキストを判定させると、入力を読み込む前処理(prefill)だけで 6.8〜89.3 秒かかり、毎リクエスト走る judge としては使いものになりません。そこで量子化、つまり重みをより粗い数値表現に置き換えてサイズと計算量を圧縮する処理に進みます。学習をやり直す必要のない PTQ(post-training quantization)方式で、Model-Optimizer に同梱されている Lightning 公式の NVFP4 レシピ(4/6 手法)を使いました。GB10 1 台で 1 時間弱、62GB が 21GB になります。
量子化で judge の確率感覚が壊れないかが心配どころですが、ホールドアウト 237 件で p_solve の平均絶対誤差は 0.131 と BF16 の 0.137 から悪化せず、判定レイテンシは中央値 1.27 秒に収まりました。実運用の長文コンテキストでも 1.8〜2.6 秒です。現行 judge の flash-0731 が 2 秒強なので、判定を 30B に任せても速さは同等以上です。
一点、量子化後のモデルを vLLM がロードできないトラブルを踏みました。export されたテンソル名の全件に内部用のサフィックスが混入していて、safetensors のヘッダを書き換えて剥がすと解消します。同じ手順(Model-Optimizer の main ブランチ + hf_ptq.py)を踏む方は、ロードエラーが出たらテンソル名を疑ってみてください。
蒸留元の flash-0731 をどこまで超えたか
仕上げに、現行 judge・素の Lightning・学習後(v2 の NVFP4)の 3 者を同一条件で比べました。評価はラベル付き 47 問のプローブと、実際のルーター経由で 87 件を流す実機評価の 2 本立てです。ここでの期待判定は教師の出力ではなく、weak が実際に解けるかどうかの実測とケース設計から独立に決めた正解です。教師の flash-0731 も同じ試験を受ける側に回るので、蒸留元超えをこの軸で測れます。
| 指標 | flash-0731(現行) | 素の Lightning | 学習後 v2-NVFP4 |
|---|---|---|---|
| ラベル付き 47 問の一致 | 38/47 | 32/47 | 45/47 |
| 実機 87 件の期待判定一致 | 78/87(89.7%) | —(導入不可) | 82/87(94.3%) |
| 見極め 14 問を strong に振れた数 | 0/14 | — | 5/14 |
| 判定レイテンシ(中央値) | 2.2 秒 | — | 1.27 秒(長文実測 1.8〜2.6 秒) |
| 判定席の専有リソース | 284B・DGX Spark 2 台 | — | 21GB・1 台に収まる |
蒸留元超えを実測で確認できました。とくに見極め 14 問は、教師の flash-0731 が構造的に 1 問も strong に振れない問題群です。実測ラベルを混ぜた効果がここに出ていて、0 と 5 の差は見た目より大きいと感じています。しきい値も学習後の確率分布で引き直し、0.75 から 0.80 に更新しました(実機 87 件で 80/87 → 82/87)。
この判定役は、チームで運用しているオープンウェイトモデル 2 つ + 自動ルーティングの環境(下記記事)に挿さる部品です。284B に 2 台がかりで任せていた判定が NVFP4 の 21GB・1 台に収まり、空いた 2 台は実行側のモデルに専念させられます。判定も実行もローカルに寄せていく構成が、これでまた一歩進みました。
そして実運用の第 1 号は、コーディングエージェントではなく常駐エージェントの側で始まっています。DGX Spark に NVFP4 版の judge と Switchyard をもう 1 系統立てて、常駐で動かしている NemoHermes のリクエストを自動ルーティングに載せ替えました。初日 16 時間の運用で暴走はゼロ、判定も weak 92 件と strong 24 件にきちんと割れています。コーディングエージェント向けに仕立てた判定役が、リクエストの中身だけを見て別のハーネスにもそのまま挿さるのは、事後学習した部品の再利用性として地味にうれしいところですね。NemoHermes そのものは以下の記事で紹介しています(2026-06 時点の記事です)。
予測型ルーターと生成型 judge の 2 系譜
今回の judge は判定文を生成するモデルですが、以前試した NVIDIA LLM Router v3 は、文章を生成せず入力を読み込んだ時点の内部状態から直接スコアを出す予測型でした。両方を自分で再訓練してみて、対比がだいぶ立体的になったので短くまとめておきます。
| 観点 | 予測型(LLM Router v3) | 生成型(今回の judge) |
|---|---|---|
| 判定の出力 | 分類ヘッドのスコア | 根拠つきの JSON |
| 適応コスト | 480 問・11 ドル・学習 5 分 | 2,610 件・10 ドル・学習 50 分 |
| ラベル設計の急所 | LLM 判定ラベルのバイアス | 蒸留ラベルが教師の盲点まで写す |
| 運用の急所 | プールを変えるたび再訓練 | 出力契約(キー順まで)の固定 |
どちらも「ラベルの出どころ」で品質が決まり、「判定モデル自身の推論コスト」が固定費になる点は同じです。個人的には、判定根拠が JSON で残る生成型のほうがデバッグはしやすく、一方で暴走のような生成ならではの故障モードを抱える、という手触りでした。長期的には、判定役は独立したモデルではなく weak 実行層の prefill に折り畳まれていく方向性が示唆されていて、そうなるとこの 2 系譜は合流するのかもしれません。
まとめ
first-touch で測った Nemotron 3.5 Lightning を、教師データ設計 → LoRA SFT → NVFP4 量子化 → 実機評価まで通して、ルーターの judge として蒸留元超えのところまで仕立てられました。この judge は DGX Spark 上の Switchyard の判定役として、すでに NemoHermes の実運用トラフィックを振り分け始めています。学習自体は公式 cookbook ベースで 1 走 10 ドル前後・1 時間弱と手軽で、成否を分けたのはデータでした。実測の正解率をラベルにすること、教師の盲点が蒸留で写ること、そして JSON のキー順まで学習データの契約に含まれること。この 3 つが今回の持ち帰りです。
できていないことも書いておくと、見極め 14 問は 5/14 で道半ばですし、量子化後のモデルには特定入力で決定論的に暴走する箇所が 1 件残っています(フェイルセーフで安全側に倒れるため運用受容としました)。判定カテゴリの語彙一致も、キー順の丸暗記を奪った代償でわずかに下がっています。
次は同じデータセットを使って、DGX Spark 単体での QLoRA 学習にも挑戦してみたいところです。H100 を借りずに手元だけで学習まで完結するのか、品質はどこまで並ぶのかは続編で扱う予定です。
参考リンク
- NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16(Hugging Face)
- NVIDIA-NeMo/Nemotron — 今回使った LoRA SFT の usage-cookbook が入っているリポジトリ
- NVIDIA-NeMo/Switchyard
- NVIDIA-NeMo/Megatron-Bridge
- NVIDIA/Model-Optimizer — NVFP4 の 4/6 量子化レシピはこちらに同梱
- vLLM v0.27.1 リリース(学習後モデルの serve に使用)






