Flaps

軽量・爆速モデル LFM2 を 爆速言語 Rust で翻訳タスクを試す。

CPUでも動作する軽量モデル LFM2 をRustで動かしてみました。

記事作成日:2026-01-10, 著者: Hi6

LFM2はLiquidが発表した次世代基盤モデルで、ハイブリッドLiquidアーキテクチャを使用。CPU上でQwen3の2倍以上の推論速度、3倍高速トレーニングを実現。16ブロック構成(短距離畳み込み10ブロック+GQA 6ブロック)により、同サイズ比較でMMLU・IFEval・GSM8Kなど主要ベンチマークで上回る性能を示す。CPU・GPU・NPUすべてで最適化し、ExecuTorchやllama.cppでミリ秒級低遅延・オフライン推論を可能にする。 エッジAI市場の2035年1兆ドル規模を見据え、スマートフォン・自動車・ロボットなどに展開し、企業向け商用ライセンスも提供する。

CPUでしかもローカルで実行が可能・・・これをLM Studio のリリースノートで見かけ気になってGeminiさんに聞いたら・・すごそうなモデルだったので、これをRustで動かしてみたくなったので試してみた。もちろんLM Studioから使用することも可能です。

今回使用するモデルは

翻訳タスクをこなすRustコード

use llama_cpp_2::model::params::LlamaModelParams;
use llama_cpp_2::model::LlamaModel;
use llama_cpp_2::context::params::LlamaContextParams;
use llama_cpp_2::llama_backend::LlamaBackend;
use std::io::Write;
use llama_cpp_2::model::AddBos;
use llama_cpp_2::model::Special;
use std::time::Instant;

const MODEL_PATH: &str = "LFM2-350M-ENJP-MT-Q4_K_M.gguf";
// const MODEL_PATH: &str = "LFM2.5-1.2B-JP-Q4_K_M.gguf";

fn main() -> anyhow::Result<()> {
    let start = Instant::now();//時間計測用
    let prompt = r#"<|im_start|>system
Translate to Japanese.
<|im_end|>
<|im_start|>user
Emphasis on human-AI collaboration. Instead of focusing solely on making fully autonomous AI systems, we are excited to build multimodal systems that work with people collaboratively.
<|im_end|>
<|im_start|>assistant
"#;
    // LlamaBackend は「LLM用の土台(OSのようなもの)」であり、
    // その上で LFM2 という「アプリ(モデル)」を動かしている、というイメージ。
    let mut backend = LlamaBackend::init()?;
    backend.void_logs(); // モデル読み込み時のログを無効化
    let model_params = LlamaModelParams::default();
    let model = LlamaModel::load_from_file(\
    &backend, MODEL_PATH, &model_params)?;
    let mut ctx = model.new_context(&backend, LlamaContextParams::default())?;

    // プロンプトのバッチ化とトークン化
    // 512は一度に扱える最大トークン数、1はバッチに追加できるシーケンスの最大数
    let mut batch = llama_cpp_2::llama_batch::LlamaBatch::new(512, 1);
    // AddBos は トークンの先頭に BOS(Beginning‑of‑Stream)
    // トークンを付加するかどうか」を制御するフラグです。
    // AddBos::Never BOS トークンを一切付加しません。
    let tokens = model.str_to_token(prompt, AddBos::Never)?;
    for (i, token) in tokens.iter().enumerate() {
        // 引数: (トークン, 位置, シーケンスIDの配列, 最後のトークンかどうか)
        // 最後の引数は、システムプロンプトなどは計算不要なので false で無効にし、
        // 最後のトークンでTrue に設定するとそれ以降確率取得計算をする
        batch.add(*token, i as i32, &[0], i == tokens.len() - 1)?;
    }

    // デコードと生成ループ
    ctx.decode(&mut batch)?;
    let mut n_cur = batch.n_tokens();

    // サンプラーの作成
    let mut sampler = llama_cpp_2::sampling::LlamaSampler::chain_simple([
        // 重複を抑える(同じ言葉を繰り返すとペナルティ)
        llama_cpp_2::sampling::LlamaSampler::penalties(64, 1.1, 0.1, 0.1),
        llama_cpp_2::sampling::LlamaSampler::greedy(), 
        // グリーディデコーディング:最も確率の高いトークンを逐次選択。
    ]);

    let mut output_buffer = Vec::new();
    let max_generate_token = 1024;

    while n_cur <= max_generate_token {
        let token = sampler.sample(&ctx, batch.n_tokens() - 1);
        if model.is_eog_token(token) { break; } // 終了トークン処理

        //Plaintext : 特別なトークンが表示されないように無視・適切に処理します
        match model.token_to_bytes(token, Special::Plaintext) {
            Ok(bytes) => {
                output_buffer.extend_from_slice(&bytes);
                //バイト列をバッファーに流す

                // 有効な部分だけを抽出して表示
                let valid_up_to = match std::str::from_utf8(&output_buffer) {
                    Ok(s) => {
                        print!("{}", s);
                        output_buffer.len()
                    }
                    Err(e) => {
                        // 珍しい漢字や絵文字: トークナイザーが対応していない
                        // 複雑な文字を出そうとして、
                        // バイトを細かく分けて出力してきたときの対応。
                        let valid_len = e.valid_up_to();
                        //指定された文字列内の有効なインデックスを返します。
                        if valid_len > 0 {
                            let valid_str = std::str::from_utf8(\
                            &output_buffer[..valid_len]).unwrap();
                            print!("{}", valid_str);
                        }
                        valid_len
                    }
                };

                // 今までの print!() などで溜まっていた『出力待ちの列』を、
                // 強制的に画面(標準出力)へ押し出す」という物理的なプッシュ操作
                std::io::stdout().flush()?;
                output_buffer.drain(..valid_up_to); // 出力した分だけ削る
            }
            Err(e) => eprintln!("Error: {:?}", e),
        }
        batch.clear();//毎サイクル、新しい1トークンだけを処理
        // 「直前に決まった1つのトークン」を、次に計算するためにセット
        batch.add(token, n_cur, &[0], true)?;
        // これまでの文脈と、上記 add した新しいトークンを踏まえて、
        // 次に来る確率が高いトークンを推論
        ctx.decode(&mut batch)?;
        n_cur += 1;// next step
    }

    // ループ終了後、万が一バッファに残っていれば出す
    // (不完全な文字でも lossy で出力する)
    if !output_buffer.is_empty() {
        print!("{}", String::from_utf8_lossy(&output_buffer));
        std::io::stdout().flush()?;
    }
    let duration = start.elapsed();
    println!("\n\n翻訳にかかった時間: {:?}", duration);
    println!("Token毎の生成時間: {:?}", duration / tokens.len() as u32);
    Ok(())
}
出力結果:
人とAIの協働に重点を置く。完全自律型AIシステムを作ることにのみ集中するのではなく、人と協働して機能するマルチモーダルシステムを構築することに興奮している。

翻訳にかかった時間: 632.1836ms
Token毎の生成時間: 12.157376ms

ストレスなく・・・結果が返ってくる・・・しかもこれがCPUだけで実行できているところがさらにすごい!ただ、小さなモデルなので日本語の固有名詞などの知識に乏しいと感じた・・・その場合は、サイズは大きくなるがLFM2.5-1.2B-JP-Q4_K_M.ggufを使用すると速度を犠牲にして、少し良い結果が得られる・・・。さらに精度を求めるならgemma-transrate@q4_k_mがおすすめ。

古いPC(intel i7 1.8GHz メモリ16G)でも動作するか確認してみたがLFM2.5-1.2B-JP-Q4_K_M.ggufのモデルでも速度は落ちるが動作するのを確認できた。


翻訳アプリケーション

上記のコードにUiを実装してアプリケーションにしたもの(インストーラ付き)を作成しましたので試したい方は各自の責任においてご自由にお試しください。こちらのモデルは、非営利・研究目的、または Qualified Non‑Profit Organization が利用する分は制限はありませんが、商用利用は 年収1,000万ドル以下 のみ許可され、超過すると利用不可となるようです。

ご利用について

免責条項

本ソフトウェアの利用により生じた一切の損害について、配布者は一切の責任を負わないものとします。利用者は自らの判断と責任で本ソフトウェアを使用し、必要に応じてバックアップやセキュリティ対策を講じるものとします。


[!NOTE] 参照サイト

Liquid

Liquid Ai の Hugging Face