ggerganov

Stars and mentions

Last 30 days, Oct 5 – Nov 3

GitHub stars36
mentions28
01234Oct 5Oct 19Nov 3
UTC days · repositories ggerganov owns5 repositories

Package downloads

Cumulative recorded downloads · since 2025-10-21

npm17.1K
04.3K8.6K12.9K17.1KOct 5Oct 19Nov 3
Oct 19npm: 1/1 packages observed

Across 1 known packages in repositories ggerganov owns. Cumulative totals include recorded downloads before the selected range. Missing observations leave gaps; totals may be incomplete.

Top repositories

ggerganov’s repositories ranked by new stars, as on trending.

Loading

Contributes to

Repositories outside ggerganov’s account where ggerganov opened or reviewed pull requests in the past 365 days.

Who noticed ggerganov

Everyone who starred or forked ggerganov’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

28posts
Nov 40 posts
  1. You're doing #podcasts/content or alike? whisper.cpp is really worth a look, it is fast and works well, fully free/OSS! ggerganov/whisper.cpp

  2. Inflearnが自動で字幕を生成するまで(AI/インフラ編) ! この記事は2023年10月31日に執筆されたものです。 こんにちは。InflabのDevOpsエンジニア、SunBです。 2023年9月、Inflearnに字幕とスクリプトの機能が追加されました。皆さん、活用されていますか? 今回追加された字幕機能は、動画の音声データをもとに自動で字幕を生成する部分と、生成された字幕を受講画面に表示する部分の、大きく2つに分けられます。 今日は、このうち私が実装を担当した字幕生成について、どのような技術をもとに、どのような過程を経て実装したのかをお話しします。 2日で字幕機能を作ってデモする まず、「DevOpsエンジニアが字幕機能を実装したの?」と思われる方もいらっしゃると思うので、今回の字幕機能の実装プロジェクトがどのように始まったのかをご紹介します。 Inflabでは今年から、Infthonという名前で社内ハッカソンを開催しています。応募者をランダムに4人1組のチームに分け、約2日間でアイデアの議論とMVPの実装を行ったあとに発表する、という形式です。 ところが偶然(?)、Inflearnの講座プレイヤーやVODインフラを多く手がけていたFE開発者のPapicoと私が同じチームになり、自然と講座の再生に関するアイデアを議論することになりました。そして、第1回Infthonのテーマだった「チームの生産性向上、またはAIを活用したサービス改善」に最も合うと考えた講座のAI字幕機能をアイデアとして選びました。 まず、AIベースの字幕生成機能を実装するにあたって、最低限の条件として次のことを考えました。 字幕生成がオフラインで動作すること(データの外部送信を防ぐ) 字幕生成にコンピューティングリソース以外の追加費用がかからないこと(オープンソースモデルを自前で実行) 字幕生成にかかる時間が動画の長さを超えないこと(ライブデモができるくらい速いこと) これらの条件を満たす技術を探してみたところ、OpenAIのWhisperというモデルが目に留まりました。Whisper以外に検討した技術は以下のとおりです。 KoSpeech: Archived OpenSpeech + KsponSpeech: 自分たちで学習させてモデルを作るには、技術への理解度もまだ低く、時間もかなりかかると予想された KakaoのPORORO: Archived MetaのSeamlessM4T: テスト用の講座を使ったPoCの過程で、SeamlessM4T largeモデルはWhisper large-v2モデルに比べて精度が劣ることを確認 NaverのCLOVA SpeechなどのAPIベースのサービス: データの外部送信とAPI料金が発生 Infthon以前にも、MacWhisperなどのツールで、OpenAIのSTTモデルであるWhisperの存在とその精度の高さは知っていましたが、深く調べたことはありませんでした。実装期間が2日と短かったため、仕組みを深く理解することよりも、素早く適用してデモできるかどうかを重視して作業を進めました。その結果、すべての処理を私のMac上でオフラインで動かす構成になりました。その際、Pythonで実装されたOpenAIのWhisperは遅すぎたため、代わりにC++に移植されたwhisper.cppを使いました。 こうして、次の流れを短期間で実装できました。 動画をアップロードする ffmpegで16-bit WAVの音声データを抽出する whisper.cpp + large-v2モデルで字幕を書き起こし、srtファイルとして保存する 受講画面でこのsrtファイルを再生する さらに、FE開発者のPapicoの活躍で、スクリプトの表示と再生位置の移動、字幕の修正依頼と修正依頼の管理機能までデモでき、第1回Infthonで1位という栄誉を手にしました。 こうして楽しい経験として幕を閉じたと思っていたのですが…… 正式プロジェクトとして採用 ある意味当然のことながら、字幕機能はすぐに正式プロジェクトとして採用され、新たなスタートを切りました。 Infthonですでにデモまで終えていたので大きな心配はありませんでしたが、引っかかる点が3つありました。 Shaka Playerでm3u8のHLSプレイリストとsrt字幕ファイルを同時に表示すると、字幕の同期がずれる問題 AWSのインフラでWhisperをまだ動かしたことがなかった点 韓国語字幕の1行あたりの長さと改行位置が適切でなく、読みにくい問題 1つ目は、FE開発者のPapicoが字幕の表示処理をShaka Playerの機能を使わずにReactで新たに実装し、きれいに解決してくれました。一方、2つ目と3つ目は、そのまま現実の問題として降りかかってきます。 インフラ上でWhisperを動かす Infthonのときとは違い、今回は実際のユーザーにサービスを提供するため、まずは私たちのインフラ上で機能を動かす必要がありました。 エンコーディングインフラの改善記録 字幕生成インフラについてお話しする前に、まず動画エンコーディングインフラを改善した過程をご紹介します。 私たちは2022年10月まで、動画のエンコーディングにAWS MediaConvertを使っていましたが、コスト削減とエンコーディングオプションのカスタマイズのために、EC2 c6i.xlarge Spot InstanceベースのAWS Batch構成に移行しました。MediaConvertのReserved Queueなど、さまざまな代替案も試しましたが、予約したエンコーディングの開始が遅すぎたり、4Kなど一部のオプションが使えないといった制約があったりして、要件を満たすのは難しいと判断し、自分たちで構成する方向を選びました。 結果として、インフラ構成の変更は効果がありました。長さ28分の1080p動画を540p、720p、1080pにエンコードする場合、AWS Batchを使ったときとMediaConvertを使ったときにかかる時間と費用は、以下のとおりでした。 AWS Batch (ECS EC2 c6i.xlarge Spot) MediaConvert (On-Demand Pro) 差 時間 約10分 約7分 約3分 (x0.7) 費用 約$0.021 約$1.895 (SD x 1, HD x 2) 約$1.874 (x90.2) 動画の長さと比べれば、エンコーディング時間の差はわずかですが、費用はなんと90倍以上の差があることを確認しました。 AWS Batch構成で、EC2インスタンスタイプごとの費用と性能を分析した結果は以下のとおりです。(長さ28分の1080p動画を540p、720p、1080pにエンコードする場合、Spot Instance未適用) インスタンスタイプ オンデマンドの1時間あたりの費用 (A) エンコーディングの実行時間 (B) 実際の費用 (A x B) c6i.xlarge $0.192 10分 $0.032 c6g.xlarge $0.154 12分47秒 $0.033 c6i.2xlarge $0.384 5分57秒 $0.038 c6i.4xlarge $0.768 4分23秒 $0.056 g5g.xlarge $0.5166 5分1秒 $0.043 g5.xlarge $1.237 3分15秒 $0.067 エンコーディングインフラと同じ構成でwhisper.cppを試す 上記の改善したエンコーディングインフラをもとに、字幕インフラも同じ構造で構成してみました。AWS Batch上のc6i.xlargeインスタンスで28分の動画に対してwhisper.cppを実行したところ、結果はなんと69分3秒と、予想よりもはるかに遅いものでした。 Infthonのデモ用にMac Studio(M1 Ultra)で同じ動画をwhisper.cppで処理したときは、3分30秒でした。これを見て、すぐにGPUインスタンスのPoCに取りかかりました。 GPUインスタンスのPoC 最初は、g5インスタンスより安いARMベースのg5g.xlargeインスタンスを使ってみようとしました。ところが、NVIDIAドライバーやCUDAのバージョンをいくら変えても、PyTorchからCUDAを認識できませんでした。CUDA 11のPython cuDNNライブラリはARMアーキテクチャをサポートしておらず、PyTorchの正式版はCUDA 12をサポートしていなかったため、PyTorch Nightly + CUDA 12 cuDNNの構成を試しましたが、torch.cuda.is_available() が False を返す状態が続きました。そんな中、AWS BatchではまだARMアーキテクチャベースのGPUインスタンスをサポートしていないというドキュメントを見つけ、g5gインスタンスの利用はひとまず諦めました。 その後、g5.xlargeインスタンスでOpenAIのPythonベースのWhisperを実行してみたところ、7分17秒かかりました。動画の長さに比べれば許容範囲でしたが、Mac Studioでwhisper.cppを使ったときは3分30秒だったことを考えると、ほかに方法はないかと考えるようになりました。そして、SeamlessM4Tについて調べていたとき、偶然Hacker NewsでWhisperXに言及しているコメントを見かけました。 WhisperXへの移行 WhisperXを使って、g5.xlargeインスタンスで28分の動画の字幕生成を試してみたところ、59秒で完了し、最も速い結果となりました。その出力結果も、Whisperやwhisper.cppと比べて精度が劣るということはなく、それぞれ一長一短がありました。 以下は、出力結果を比較した当時に書いたメモの一部です。 Whisper、WhisperXはg5.xlargeで、whisper.cppはMac Studioで抽出したバージョン 3つともかなり違いがあるが、どれか1つが最も優れているというわけではなく、それぞれにうまく書き起こせている部分がある。 WhisperXが正確に抽出した部分 뽑았는데 (Whisper: 꼽았는데) 등허리 (Whisper: 등어리) 복부 (Whisper: 복구) whisper.cppが正確に抽出した部分 Eye Reflect (WhisperX: I Reflect) Whisperが正確に抽出した部分 Z-Remesh (WhisperX: gremesher) 아이리플렉트 (WhisperX: I Reflect) WhisperXはほかの2つよりはるかに速く実行でき、音節単位でalignするので行の再構成に有利であり、字幕の修正依頼機能の有効性も比較的高くなると推定されることを考えると、WhisperXを使うのが最も良いと思われる。 ただし、ときどき複数の文を早口で話す部分で抜け落ちる文があるが、精度が低いと判断して意図的に除外しているように見える。 まとめ 最初に試したwhisper.cpp + c6i.xlargeを基準に、試した組み合わせとの差をまとめると次のとおりです。(large-v2モデルを使って28分の音声データを書き起こした場合) ツール マシン 実行時間 差(速度) whisper.cpp c6i.xlarge 69分3秒 - (x1) Whisper c6i.xlarge 時間がかかりすぎて計測を中断 - WhisperX c6i.xlarge 実行中にエラーが発生 - whisper.cpp Mac Studio 3分30秒 65分33秒 (x19.7) Whisper Mac Studio 時間がかかりすぎて計測を中断 - WhisperX Mac Studio 15分26秒 53分37秒 (x4.5) whisper.cpp(cuBLASビルド) g5.xlarge 17分25秒 51分38秒 (x4) Whisper g5.xlarge 7分17秒 61分46秒 (x9.5) WhisperX g5.xlarge 59秒 68分4秒 (x70.2) 最終的に、WhisperX + g5.xlargeの組み合わせを選びました。 こうして最初の絶望を乗り越え、速度を70倍以上改善し、インフラ上で字幕生成機能を正常に動かせるようになりました。その過程でGPUインスタンスや動画エンコーディングに関する分析も行い、エンコーディング関連のプロセスも改善できました。 (試行錯誤の跡) 字幕の長さと読みやすさの改善 字幕をどう生成するかという課題は、音声データからテキストを抽出するだけでは終わりませんでした。 whisper.cpp、Whisper、WhisperXのいずれにも、--max_line_width のような1行の長さを制限するオプションがあったのですが、問題はこのオプションが動作しないことでした。(……)おそらく韓国語だから動作しないのだろうと推測しています。 そのため、モデルが出力した字幕データは行ごとに長さがバラバラで、文の終わりで改行されることもほとんどなく、読みやすさは最悪でした。 これを改善するために、ピリオド/疑問符/感嘆符があれば改行する案、一定の文字数以上なら単純に空白を基準に半分に分割する案、形態素解析によって行を再構成する案など、さまざまな方法を模索しました。 そんな中、WhisperXに移行して音節単位で時間を追跡できるようになったことで、いっそすべての行を一から作り直そうと考えました。そこで以下の基準を定め、WhisperXの実行から行の構成までを一度に行うPythonスクリプトを実装しました。 WhisperXが時間の追跡に失敗した音節の時間は、時間の追跡に成功した最も近い前後の音節の時間、または文の開始・終了時間から線形補間する。(wav2vecの韓国語モデルベースだったため、ハングル以外は音節の時間を追跡できないという問題がありました。) 直前の単語までの行の長さが0.5秒未満であれば、ほかのどの条件を満たしていても改行しない。 直前の単語までの行の長さが10秒を超えたら、現在の単語から新しい行にする。 現在の単語を加えると行が10文字を超え、かつ形態素解析の結果で文の数が変わる場合は、現在の単語から新しい行にする。 現在の単語を加えると行が50文字を超える場合は、現在の単語から新しい行にする。 文の開始が直前の文の終わりから0.5秒以内の場合は、文の開始時間として直前の文の終了時間を代わりに使う。 実は、このうち形態素解析で文の数を計算し、それをもとに改行する基準は最後に作ったものです。この基準を決めるまでは、WhisperXをCLIで実行したあと、JSONで保存されたデータを読み込んで行を構成する処理をすべてGoで実装していました。しかし、形態素解析と文の数の計算にKiwiを使うことになったため、WhisperXをPython内で直接実行するようにし、行の構成もPythonで実装し直しました。そしてGoは、このPythonスクリプトを実行し、最終的に構成された字幕データをストレージに保存するという、AWS Batch内の1つのジョブのライフサイクル管理だけを担う構造にしました。 こうして、現在提供している字幕のように、読みやすさが大きく改善された字幕データを生成できるようになりました。 GPUインスタンス不足の問題を回避する 問題はすべて解決したと思い、これまでにアップロードされた講座を対象に、実際に字幕を一括生成しようとしました。ところが、AWS Batchにジョブを投入していくら待っても、EC2インスタンスが起動しませんでした。Spot Requestを確認してみると、ソウルリージョンにg5.xlargeインスタンスのSpot Capacityがなく、起動できていなかったのです。 Spot Instanceとは? Spot Instanceは、AWSで使われていないコンピューティングリソースを最大90%割引した価格で提供する機能です。実行中のSpot Instanceがときどき中断されることもありますが、AWS Batchなどを使ってワークフローの耐障害性を確保していれば、Spot Instanceを適用するだけで60%以上のコスト削減効果を得られます。 実際に、動画エンコーディングインフラにSpot Instanceを適用することで平均70%のコストを削減できていたので、そもそも費用のかさむGPUインスタンスを使ううえで、Spot Instanceの適用は必須でした。 Spot Capacityを求めて GPUインスタンスが足りないという話はよく耳にしていましたが、実際に経験したのは初めてでした。3日待っても起動しなかったので、別の方法を探し始めました。 最初に、Spot Placement Scoreという機能を使ってみました。米国のバージニア、オハイオ、オレゴンリージョンはg5.xlargeインスタンスが最も安く、Placement Scoreも高く出ていたので、まずはバージニアリージョンで試してみました。 初めて使うリージョンで何もなかったため、VPC、Subnet、EIP、NAT Gatewayをはじめ、AWS Batch関連の各種リソースをすべて新しく作成しました。ところが実行してみると、同じようにSpot Capacityがないというエラーが発生しました。しまった!と思い、すぐ隣のオハイオリージョンにも同じ構成を作ってみましたが、やはりSpot Capacityがないというエラーが発生しました。(……) Placement Scoreは当てにならないなと思って調べてみたところ、スポットインスタンスアドバイザーというものを見つけました。g5.xlargeスポットインスタンスの中断頻度が低いリージョンを探してみると、オレゴンリージョンが該当することが分かりました。 バージニア、オハイオリージョンに作ったものはすべて削除し、オレゴンリージョンに構成して実行してみると、ようやく字幕生成ジョブが動き始めました。一括処理のために2,000台のg5.xlargeインスタンスを同時に起動する計画でしたが、オレゴンリージョンでさえインスタンス不足で最大500台ほどしか起動できませんでした。それでも、約4時間で字幕の一括生成を完了できました。 一括処理のコスト 基盤 単価 費用 差 AWS Batch (g5.xlarge Spot) - $807.22 - (x1) AWS Batch (g5.xlarge On-Demand) - 約$2,690 約$1,883 (x3.3) OpenAI Whisper API $0.006 / minute 約$8,700 約$7,893 (x10.8) Naver CLOVA CSR API $0.012 / minute 約$17,400 約$16,593 (x21.6) 字幕の一括生成作業にかかった費用は $807.22 で、約24,000時間に及ぶ動画の音声データをAPIベースのサービスで処理する場合と比べて、10分の1以下のコストで字幕を生成できる構成を実現できました。 結論 以上のような過程を経て、約2か月で字幕生成機能を無事に実装できました。Infthonをきっかけに、2日間でアイデアの選定から技術調査、デモ用の実装までを終え、その後の2か月間で、それをユーザーにリリースできるレベルまで引き上げるためにさまざまな試みをしました。貴重な経験でした。 字幕生成に関する技術的な部分も注目に値しますが、特に強調したいのは、1人が短期間でこなしたにしては、インフラ関連の作業量がかなり多かったことです。何のリソースもないリージョンを3回も渡り歩き、VPCからAWS Batch環境までを一晩で構成できたのは、何かが準備されていなければ不可能なことです。特に、受講権限を持つユーザーにのみ字幕データを提供する対応や、字幕のストレージをS3からMongoDBに移行する作業など、本文には書いていない数多くのインフラ作業がありました。それらに触れなかったのは、ほとんど悩むことなく素早く対応できたからです。 これが可能だったのは、これまで懸命に構築してきたIaCのおかげだと思います。私たちがどのようにIaCを構築したかについては、(今度こそ本当に)近日公開予定の「InflabのIaC構築記(Part 1)」の記事でご覧いただけます。AWS CDKからTerraformへの記事でTerragruntとTerratestについての記事を予告しておきながら、1年以上新しい記事を投稿できなかった理由も、その記事で詳しく書いていますので、IaCに興味のある方はぜひ読んでみてください。 ありがとうございました。

  3. VIGIL: The Cloud Is Not the Only Place a Memory Can Live VIGIL: Keep Your Family Stories Off the Cloud — An Offline AI Memory Archive > *"Every time we ask a cloud AI to remember something for us, we make a quiet trade: convenience for control. We surrender our memories, rituals, and unwritten life details to corporate servers. I built VIGIL to explore a different contract: an air-gapped sanctuary where human lore remains sovereign on the keeper’s machine, while AI serves as a replaceable local instrument rather than a permanent owner." This is my submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine. --- What I Built VIGIL is a local-first archive for preserving the stories, rituals, names, recipes, and small details that make someone impossible to forget. A user can: - Preserve a memory about a person, place, date, and sensory detail. - Search the archive locally with instant client-side querying. - Open the original memory untouched and intact. - Ask for a short AI-generated "echo" (reflection). - Export and import the archive as portable, sovereign JSON. The original memory is always kept strictly separate from the generated reflection. VIGIL never pretends that an AI-generated paragraph is the voice of someone who has passed away. I interpreted Ghost in the Machine as a question about what remains when a machine reflects something deeply human back to us. The ghost in VIGIL is not a simulated person. It is the signal that remains in a recipe, a song, a kitchen, or a phrase someone left behind. --- Demo & Video Walkthrough {% embed youtu.be %} Open VIGIL → The demo works immediately without an account, backend server, or model installation. Try it in one minute: 1. Open "The orange radio". 2. Click "Ask for an echo". 3. Read the offline reflection. 4. Add a memory of your own. 5. Search for radio or music. 6. Export the archive as JSON. > For the full local-AI experience, launch Ollama and select Local open model. --- How I Built It VIGIL offers two reflection paths designed around resilience and privacy: 1. Offline Whisperer The default path is deterministic and requires no network connection, AI model, or API key. This guarantees the archive still functions when the internet is completely unavailable. Capturing, searching, exporting, and reflecting on memories never depend on a remote service. 2. Local Open-Weight AI When the user selects Local open model, VIGIL communicates directly from the browser to Ollama running on localhost: ollama pull llama3.2:3b ollama serve The rest of the stack is intentionally minimal, zero-dependency, and inspectable: - Browser localStorage for sovereign, on-device data persistence. - Local full-text search executed client-side. - JSON export & import for complete data ownership and portable backups. - Static GitHub Pages deployment with no telemetry, tracking, or analytics. - Zero hosted databases and no API keys required. Architecture Overview Selected Memory │ ├── Offline Whisperer │ └── Deterministic local reflection (Zero network / No model) │ └── Ollama on localhost └── Open-weight model reflection (llama3.2:3b) Browser localStorage └── Capture · Search · Export · Import There is no intermediary VIGIL server. When the local open model is selected, requests go directly from the client's browser to the user's local Ollama process. --- Why Does Open Innovation Matter? A closed cloud AI API turns privacy into a mere promise: > "Trust us. We will keep your family stories safe." VIGIL makes privacy a structural guarantee visible in the architecture. With open models and open-source software: - Keeper Autonomy: The archive keeper can inspect every line of code, select their own local model, modify prompt templates, run smaller quantized models, fine-tune family-specific LoRA adapters, or remove the AI layer entirely while retaining their memories. - Failure-as-Design: If Ollama is not running, VIGIL does not silently forward data to a remote cloud fallback or hallucinate fake status. It cleanly falls back to a clearly-labeled offline reflection. - Permanent Availability: If internet infrastructure goes down, the core archival experience remains 100% operational. The model is never the owner of the memory. It is simply an instrument the keeper can put down at any time. --- The Important Design Decision VIGIL strictly demarcates: 1. The original memory (what the human actually experienced and recorded). 2. The generated interpretation (the machine's echo). 3. The source of that interpretation (deterministic whisperer vs. local LLM). This distinction is vital. A memory archive must preserve what a person actually wrote, never gradually supplanting it with synthetic, confident machine prose. Every echo is explicitly labeled as an interpretation; the original memory remains the inviolable source of truth. --- Honest Limitations - Browser Storage Lifecycle: VIGIL is an exploration of privacy-first architecture, not an encrypted cold-storage vault. Because data lives in browser localStorage, clearing browser cache/data will remove local records. Important archives should regularly be exported to JSON backups. - Sample Data: The public GitHub Pages demo loads sample starter memories so visitors can experience the archive immediately without entering sensitive personal data. --- The Spirit of It The moment I wanted VIGIL to evoke was deliberately quiet: Someone opens a memory about an orange radio, reads the sensory details of a kitchen and a fading song, and receives an echo that does not attempt to impersonate anyone. It simply notices that the detail was kept. > A memory does not need a cloud-sized model to matter. It needs a safe place to remain. --- What I Would Build Next - On-device speech-to-text with Whisper.cpp for spoken oral histories. - Local vector embeddings for semantic and emotional memory search. - Client-side AES-GCM encrypted backup archive files. - Consent-based, family-specific LoRA adapters. - Encrypted peer-to-peer sync between household devices. --- Code & Links - Live Demo: sharan-s-dev.github.io - Source Code: sharan-s-dev/VIGIL - License: MIT

    5 reactions
  4. Reddit · · r/Vicharak · u/DarkSilverDeveloper · ggerganov/whisper.cpp

    Running Whisper Speech Recognition Locally on Vicharak Axon via whisper.cpp Yes the RK3588 can run Whisper for local speech-to-text either via CPU threads (whisper.cpp on Cortex-A76 cores) or NPU-accelerated. Vicharak's own benchmarks show Whisper-base running at 0.215 RTF (real-time factor) on the NPU, meaning transcription runs roughly 4-5x faster than real-time audio playback. CPU Path (whisper.cpp): Bash: git clone https://github.com/ggerganov/whisper.cpp cd whisper.cpp make -j8 bash ./models/download-ggml-model.sh base.en ./main -m models/ggml-base.en.bin -f input_audio.wav For NPU-accelerated inference, see the RKNN model zoo's Whisper example or Vicharak'sAxon-NPU-Guide benchmark table. Access Linux audio routing guides at docs.vicharak.in.

    1 point
  5. Руководство по сборке локального AI-стека. Часть 6. Конвертация моделей Hugging Face в GGUF Руководство по сборке локального AI-стека Конвертация моделей Hugging Face -> GGUF Не все модели существуют в готовом .gguf-формате. Иногда нужна модель, которую сообщество ещё не квантовало, или хочется получить конкретный квант (Q4_K_M, Q5_K_M, Q8_0), которого нет в готовых сборках. В таких случаях её нужно сконвертировать самостоятельно: из «сырых» весов Hugging Face (.safetensors) в GGUF, а затем — в нужный квант. Скрипт convert_to_gguf.sh автоматизирует весь процесс: от установки зависимостей до финального файла .gguf, с очисткой промежуточных данных. Что нужно понять до начала Конвертация — это всегда Python-скрипт. В отличие от квантования (llama-quantize — C++ бинарник), конвертер convert_hf_to_gguf.py требует transformers, safetensors, sentencepiece и torch (CPU-версию). Полностью отказаться от Python не получится — но можно изолировать его в venv, чтобы не засорять систему. Процесс состоит из двух шагов: Конвертация .safetensors -> .gguf (F16). Это «перевод» весов из формата Hugging Face в формат llama.cpp. Размер файла примерно равен сумме .safetensors (может быть чуть больше из-за метаданных). Квантование F16 -> Q4_K_M. Это «сжатие» с потерей точности. Размер уменьшается в 3–4 раза, качество падает минимально. Зачем нужен промежуточный F16. llama-quantize работает только с GGUF — он не умеет читать .safetensors. Поэтому F16-файл — обязательный промежуточный шаг. После квантования его можно удалить (если не нужны другие кванты из того же F16). Что делает скрипт Скрипт convert_to_gguf.sh — это «одна команда, весь пайплайн»: Проверяет системные пакеты (cmake, build-essential, python3-venv, git, curl, jq) через dpkg -s. Ставит только если отсутствуют. Собирает llama.cpp (если llama-quantize и llama-cli ещё нет). Создаёт venv (если нет) и ставит зависимости из llama.cpp/requirements/requirements-convert_hf_to_gguf.txt. Это официальный файл от llama.cpp, который тянет CPU-версию torch (через --extra-index-url download.pytorch.org) — никаких CUDA-драйверов. Скачивает модель через hf download --local-dir hf_model. Sanity check: проверяет, что .safetensors — реальные веса, а не LFS-указатели (маленькие текстовые файлы). После hf download это всегда так, но проверка дешёвая. Конвертирует в models/<модель>-f16.gguf через convert_hf_to_gguf.py. Квантует в models/<модель>-Q4_K_M.gguf через llama-quantize. Проверяет размер Q4: если файл меньше 100 МБ — явно битый, выходит с ошибкой. Очищает hf_model/ и промежуточный F16 после успешной проверки. Всё, что попало в CLEANUP_PATHS, удаляется при выходе (успех / ошибка / Ctrl+C) через trap cleanup EXIT INT TERM. Это гарантирует, что при сбое не останутся «полу-скачанные» гигабайты. Флаг KEEP_INTERMEDIATE=1 отключает очистку — для отладки. Ключевые моменты CPU-версия torch — критично В requirements-convert_hf_to_gguf.txt уже указан --extra-index-url download.pytorch.org и torch==2.11.0. Это гарантирует, что: Не притянутся nvidia-* пакеты (~2 ГБ). Не потребуются CUDA-драйверы. Конвертация пойдёт на CPU (что для convert_hf_to_gguf.py — норма, он не использует GPU). Если ставить torch вручную без --index-url, pip возьмёт CUDA-версию с PyPI. Это не сломает конвертацию (CPU-путь всё равно используется), но займёт лишние 2 ГБ и может потребовать libcuda.so (которого нет). hf download vs wget vs git clone Скрипт использует hf download — официальный CLI от Hugging Face. Он: Не требует git-lfs (качает через HTTPS). Поддерживает gated-модели (с токеном). Умеет выбирать конкретные файлы (если нужно только часть репозитория). Альтернативы: wget/curl — только для одного файла по прямой ссылке. Для полного репозитория (с config.json, tokenizer.json, несколькими .safetensors-шардами) — неудобно. git clone с git-lfs — если хочется именно git-историю. Но для конвертации это избыточно: git-lfs нужно ставить, а .safetensors всё равно скачаются как «большие файлы». Sanity check: LFS-указатели hf download качает реальные веса, но проверка на LFS-указатели — дешёвая страховка: if grep -ql "git-lfs.github.com/spec" "$HF_DIR"/*.safetensors 2>/dev/null; then echo "[!] В $HF_DIR обнаружены LFS-указатели вместо весов." >&2 exit 1 fi sourcecraft.dev Что это ловит. LFS-указатели — это маленькие (130 байт) текстовые файлы, которые git создаёт вместо реальных весов, если git-lfs не установлен. convert_hf_to_gguf.py на них упадёт с невнятной ошибкой («cannot load safetensors»), а проверка даёт понятное сообщение. Проверка размера Q4 Q4_SIZE=$(stat -c%s "$Q4_GGUF") if [ "$Q4_SIZE" -lt $((100 * 1024 * 1024)) ]; then echo "[!] Подозрительно маленький файл: $(du -h "$Q4_GGUF" | cut -f1)" >&2 exit 1 fi sourcecraft.dev Порог 100 МБ — с большим запасом. Даже крошечная модель 1.5B в Q4_K_M весит ~1 ГБ. Если файл меньше — что-то пошло не так на этапе квантования (например, llama-quantize упал, но создал пустой файл). Это дешёвая проверка, которая ловит 99% проблем. Очистка: CLEANUP_PATHS + trap CLEANUP_PATHS=() ... CLEANUP_PATHS+=("$HF_DIR") CLEANUP_PATHS+=("$F16_GGUF") cleanup() { local ec=$? trap - EXIT INT TERM set +e ... for p in "${CLEANUP_PATHS[@]}"; do [ -e "$p" ] && rm -rf "$p" done exit "$ec" } trap cleanup EXIT INT TERM sourcecraft.dev Почему это важно. hf_model для большой модели — это 10–20 ГБ .safetensors + F16 GGUF ещё столько же. Если скрипт упадёт на середине (например, OOM при конвертации), без очистки останется 20–40 ГБ «мусора». trap гарантирует, что при любом исходе (успех, ошибка, Ctrl+C) промежуточные файлы удаляются. Q4_K_M в CLEANUP_PATHS не добавляется — это итоговый продукт, его нужно сохранить. KEEP_INTERMEDIATE=1 — для отладки. Если конвертация падает, полезно посмотреть, что в hf_model и -f16.gguf: KEEP_INTERMEDIATE=1 ./convert_to_gguf.sh Qwen/Qwen3.5-2B sourcecraft.dev Архитектуры, которые «не идут» Не все модели конвертируются «из коробки». Реальные случаи: KDA / Lissa — convert_hf_to_gguf.py не знает архитектуру kda. Даже установка fla (flash-linear-attention) не помогает: в скрипте нет класса-обработчика для неё. Решение: ждать, пока поддержка войдёт в llama.cpp master (PR #18381), или использовать Python-инференс напрямую через transformers. Совсем новые архитектуры (например, Mamba-гибриды) — могут требовать transformers из git (pip install git+huggingface/transformers), если в стабильном релизе их ещё нет. Модели с кастомным trust_remote_code — иногда convert_hf_to_gguf.py требует флаг --trust-remote-code, иначе не загрузит config.json. Как проверить заранее. Зайдите в llama.cpp/conversion/ и поищите файл с именем архитектуры: ls llama.cpp/conversion/ | grep -i <архитектура> sourcecraft.dev Например, grep -i qwen покажет qwen.py. Если для вашей модели нет файла — конвертация не пройдёт без патчей. Использование Базовый запуск: ./convert_to_gguf.sh Qwen/Qwen3.5-2B sourcecraft.dev Без аргумента — спросит имя интерактивно: ./convert_to_gguf.sh Введите имя модели на Hugging Face (например, Qwen/Qwen3.5-2B): _ sourcecraft.dev С сохранением промежуточных файлов: KEEP_INTERMEDIATE=1 ./convert_to_gguf.sh Qwen/Qwen3.5-2B sourcecraft.dev Что появится в models/: models/ ├── Qwen3.5-2B-Q4_K_M.gguf ← итоговый файл (готов к запуску) └── (после успеха: hf_model/ и Qwen3.5-2B-f16.gguf удалены) sourcecraft.dev Время работы: Модель Скачивание Конвертация Квантование Итого Qwen3.5-2B 2–5 мин 1–2 мин 30 сек ~5 мин Qwen3.5-9B 10–20 мин 5–10 мин 1–2 мин ~20 мин Qwen3.5-35B 30–60 мин 20–40 мин 3–5 мин ~1.5 часа На HDD — дольше в 2–3 раза (особенно скачивание и чтение при конвертации). Код convert_to_gguf.sh #!/usr/bin/env bash Конвертация модели с Hugging Face в GGUF (F16) и квантование в Q4_K_M. Использование: ./convert_to_gguf.sh [USER/REPO] Пример: ./convert_to_gguf.sh Qwen/Qwen3.5-2B # Скрипт сам: - доустанавливает системные пакеты (через sudo apt); - создаёт/активирует venv и ставит зависимости из llama.cpp/requirements; - скачивает модель через hf download, если папки hf_model нет; - конвертирует в F16 GGUF, квантует в Q4_K_M; - проверяет квант через llama-cli; - удаляет hf_model и промежуточный F16 только после успешной проверки. set -euo pipefail --- 0. Параметры --- MODEL_ID="${1:-}" if [ -z "$MODEL_ID" ]; then read -r -p "Введите имя модели на Hugging Face (например, Qwen/Qwen3.5-2B): " MODEL_ID fi [ -n "$MODEL_ID" ] || { echo "[!] Имя модели не задано." >&2; exit 1; } Короткое имя для файлов (последняя часть после /) MODEL_SHORT="${MODEL_ID##*/}" --- 1. Пути --- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LLAMA_DIR="$SCRIPT_DIR/llama.cpp" REQ_DIR="$LLAMA_DIR/requirements" HF_DIR="$SCRIPT_DIR/hf_model" OUT_DIR="$SCRIPT_DIR/models" VENV_DIR="$SCRIPT_DIR/venv" mkdir -p "$OUT_DIR" --- 1a. Очистка на выходе (trap) --- CLEANUP_PATHS — всё, что нужно удалить при выходе. Q4_K_M сюда НЕ добавляем. CLEANUP_PATHS=() cleanup() { local ec=$? trap - EXIT INT TERM # защита от рекурсии set +e # очистка не должна падать echo "" if [ "${KEEP_INTERMEDIATE:-0}" = "1" ]; then echo "[i] KEEP_INTERMEDIATE=1: промежуточные файлы сохранены." exit "$ec" fi if [ ${#CLEANUP_PATHS[@]} -gt 0 ]; then echo "[*] Очистка промежуточных файлов..." for p in "${CLEANUP_PATHS[@]}"; do if [ -e "$p" ]; then echo " rm -rf $p" rm -rf "$p" fi done fi exit "$ec" } trap cleanup EXIT INT TERM --- 2. Системные пакеты (ставим только отсутствующие) --- SYS_PKGS=(cmake build-essential libopenblas-dev python3 python3-venv python3-pip git curl wget jq) MISSING_SYS=() for pkg in "${SYS_PKGS[@]}"; do dpkg -s: код 0 = установлен, иначе — нет dpkg -s "$pkg" &>/dev/null || MISSING_SYS+=("$pkg") done if [ ${#MISSING_SYS[@]} -gt 0 ]; then echo "[] Установка системных пакетов: ${MISSING_SYS[]}" sudo apt update -qq sudo apt install -y -qq "${MISSING_SYS[@]}" else echo "[*] Все системные пакеты уже установлены." fi --- 3. Сборка llama.cpp (если нужно) --- LLAMA_QUANTIZE="$LLAMA_DIR/build/bin/llama-quantize" LLAMA_CLI="$LLAMA_DIR/build/bin/llama-cli" if [ ! -x "$LLAMA_QUANTIZE" ] || [ ! -x "$LLAMA_CLI" ]; then echo "[*] Сборка llama.cpp..." if [ ! -d "$LLAMA_DIR/.git" ]; then git clone --depth=1 ggerganov/llama.cpp "$LLAMA_DIR" fi cmake -S "$LLAMA_DIR" -B "$LLAMA_DIR/build" -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_UI=ON cmake --build "$LLAMA_DIR/build" --config Release -j"$(nproc)" fi --- 4. venv и зависимости конвертера --- Используем официальный requirements-файл: он тянет CPU-версию torch. if [ ! -d "$VENV_DIR" ]; then echo "[*] Создание venv..." python3 -m venv "$VENV_DIR" fi активируем окружение source "$VENV_DIR/bin/activate" REQ_FILE="$REQ_DIR/requirements-convert_hf_to_gguf.txt" [ -f "$REQ_FILE" ] || { echo "[!] Не найден $REQ_FILE"; exit 1; } echo "[*] Установка зависимостей конвертера (CPU-only torch)..." Проверяем «маркерные» пакеты из requirements-convert_hf_to_gguf.txt. Если хоть одного нет — ставим весь файл (pip сам проверит версии и доставит). Это быстрее, чем pip install -r при полном наборе, и надёжнее, чем ручной список версий (при обновлении requirements всё подхватится). MARKER_PY_PKGS=(torch numpy sentencepiece transformers gguf protobuf huggingface_hub) MISSING_PY=() for pkg in "${MARKER_PY_PKGS[@]}"; do pip show "$pkg" &>/dev/null || MISSING_PY+=("$pkg") done fla нужен для архитектур линейного внимания (KDA/Lissa и др.). if [ ${#MISSING_PY[@]} -gt 0 ]; then echo "[] Отсутствуют Python-пакеты: ${MISSING_PY[]}" echo "[*] Установка из $REQ_FILE (CPU-only torch)..." pip install --no-cache-dir -q -r "$REQ_FILE" pip install --no-cache-dir -q huggingface_hub else echo "[*] Зависимости конвертера уже установлены." fi --- 5. Загрузка модели --- CLEANUP_PATHS+=("$HF_DIR") if [ -d "$HF_DIR" ] && [ -n "$(ls -A "$HF_DIR" 2>/dev/null)" ]; then echo "[*] Папка $HF_DIR уже существует, пропускаем загрузку." else echo "[*] Загрузка $MODEL_ID в $HF_DIR..." --local-dir — без промежуточного кэша, сразу в папку. hf download "$MODEL_ID" --local-dir "$HF_DIR" fi --- 5a. Sanity check: веса не LFS-указатели --- После hf download это всегда так, но проверка дешёвая и не мешает. if grep -ql "git-lfs.github.com/spec" "$HF_DIR"/*.safetensors 2>/dev/null; then echo "[!] В $HF_DIR обнаружены LFS-указатели вместо весов." >&2 exit 1 fi --- 6. Конвертация в F16 GGUF --- F16_GGUF="$OUT_DIR/${MODEL_SHORT}-f16.gguf" CLEANUP_PATHS+=("$F16_GGUF") echo "[*] Конвертация в $F16_GGUF..." python3 "$LLAMA_DIR/convert_hf_to_gguf.py" "$HF_DIR" --outfile "$F16_GGUF" Проверка размера F16 (должен быть сопоставим с суммой safetensors) echo "[*] Размер F16: $(du -h "$F16_GGUF" | cut -f1)" --- 7. Квантование в Q4_K_M --- Q4_GGUF="$OUT_DIR/${MODEL_SHORT}-Q4_K_M.gguf" echo "[*] Квантование в $Q4_GGUF..." "$LLAMA_QUANTIZE" "$F16_GGUF" "$Q4_GGUF" Q4_K_M --- 8. Проверка квантованной модели --- Минимальный sanity check: файл существует и размер разумный. Полноценный тест — через llama-server вручную (или в отдельном скрипте). if [ ! -f "$Q4_GGUF" ]; then echo "[!] Квантованный файл не создан: $Q4_GGUF" >&2 exit 1 fi Проверка размера: Q4_K_M обычно 1/4..1/5 от F16. Если файл < 100 МБ — явно битый. Q4_SIZE=$(stat -c%s "$Q4_GGUF") if [ "$Q4_SIZE" -lt $((100 * 1024 * 1024)) ]; then echo "[!] Подозрительно маленький файл: $(du -h "$Q4_GGUF" | cut -f1)" >&2 CLEANUP_PATHS+=("$Q4_GGUF") exit 1 fi echo "[*] Файл создан: $Q4_GGUF ($(du -h "$Q4_GGUF" | cut -f1))" --- 9. Очистка --- Ничего не делаем: cleanup() через trap EXIT/INT/TERM удалит содержимое CLEANUP_PATHS (если KEEP_INTERMEDIATE != 1). echo "[*] Готово: $Q4_GGUF" exit 0 sourcecraft.dev Проверка результата 1. Файл создан: ls -lh models/Qwen3.5-2B-Q4_K_M.gguf sourcecraft.dev Должен быть ~1–1.5 ГБ (для 2B в Q4_K_M). 2. Модель загружается в llama-server: ./run_llama_mmap.sh -m Qwen3.5-2B-Q4_K_M sourcecraft.dev В логе — model loaded, а в UI — ответ на «Привет». 3. Промежуточные файлы удалены: ls -d hf_model 2>/dev/null # должно быть "No such file" ls models/*-f16.gguf 2>/dev/null # должно быть пусто sourcecraft.dev Если что-то осталось — KEEP_INTERMEDIATE был включён, или скрипт упал до очистки. Проверьте вывод. Подводные камни Не запускайте конвертацию и llama-server одновременно при малом объёме RAM. convert_hf_to_gguf.py грузит .safetensors в память (для 9B — ~18 ГБ), а llama-server держит модель через mmap. Вместе они могут не влезть. Сначала конвертация, потом — сервер. F16 GGUF может быть больше суммы .safetensors на 5–10% (метаданные, выравнивание). Не пугайтесь, если ls -lh покажет чуть больше ожидаемого. Квантование не всегда даёт «ровно» Q4_K_M. Для некоторых архитектур llama-quantize может выдать предупреждение «some tensors require higher precision» и оставить часть весов в F16. Это нормально: критичные слои (например, output.weight) иногда остаются в F16 для стабильности. Tokenizer для мультимодальных моделей. Некоторые модели (например, MOSS-Image) требуют отдельный tokenizer.json, который не входит в .safetensors и не подхватывается convert_hf_to_gguf.py автоматически. Его нужно скачать отдельно (hf download <repo> tokenizer.json --local-dir ...) и указать в MODEL_MAP через --tokenizer. Для обычных LLM (Qwen, Gemma) это не нужно — токенизатор встроен в GGUF. Gated-модели. Если модель требует принятия лицензии на Hugging Face, hf download попросит токен. Залогиньтесь один раз: hf auth login sourcecraft.dev Токен сохранится в ~/.cache/huggingface/, дальнейшие загрузки пройдут без запроса. Итог convert_to_gguf.sh — это «одна команда, весь пайплайн» для получения .gguf из «сырых» весов Hugging Face. Он: Ставит отсутствующие зависимости (быстро при повторных запусках). Использует CPU-версию torch (без CUDA-драйверов). Гарантирует очистку при любом исходе через trap. Даёт понятные сообщения об ошибках (LFS-указатели, слишком маленький Q4, отсутствующий requirements). Не пытается «спасти» неподдерживаемые архитектуры (KDA) — честно падает с Failed to detect model architecture. Когда использовать: Модель есть на Hugging Face, но нет готового .gguf (или нет нужного кванта). Хочется Q5_K_M или Q8_0, а сообщество сделало только Q4_K_M. Нужна конкретная ревизия модели. Когда НЕ использовать: Готовый .gguf уже есть — скачайте его (hf download или wget), это в 3–5 раз быстрее. Архитектура не поддерживается convert_hf_to_gguf.py (проверьте conversion/ заранее). Нет 30–60 ГБ свободного места (.safetensors + F16 + Q4 на пике).

  6. Руководство по сборке локального AI-стека. Часть 2. Запуск llama-cpp Руководство по сборке локального AI-стека Часть 2. Запуск llama.cpp на тестовой модели На этом шаге мы соберём llama.cpp, скачаем компактную модель для теста, разберёмся с ключевыми опциями (mmap, лимиты памяти, mmproj) и запустим сервер. После этого у вас будет работающий чат по адресу 127.0.0.1 — с родным веб-интерфейсом llama.cpp (пока без Open WebUI). Что нужно понять до начала llama.cpp — это C++ движок для инференса LLM. Он умеет: Работать на CPU (без GPU) — то, что нам нужно. Загружать модель через mmap — не грузить её целиком в RAM, а подгружать страницы по требованию. Это позволяет запускать модель больше доступной памяти. Ограничивать потребление памяти через cgroup (через systemd-run). Работать в двух режимах: одна модель (-m) или «роутер» моделей (--models-dir), когда в UI виден список всех .gguf, а в памяти держится одна. Тестовая модель — Qwen3.5-2B-Q4_K_M (~1.5 ГБ), чтобы проверить пайплайн быстро, не дожидаясь загрузки 9–10 ГБ модели. Она же потом пригодится для «сверхбыстрых» задач (черновики, проверка идей). Шаг 1. Системные зависимости sudo apt update sudo apt install -y \ cmake build-essential libopenblas-dev \ python3 python3-venv python3-pip \ git curl wget jq sourcecraft.dev libopenblas-dev — ускоряет матричные операции на CPU. build-essential — компилятор для сборки llama.cpp. python3-venv понадобится позже для Open WebUI, но поставим сразу. Шаг 2. Сборка llama.cpp Скрипт run_llama_mmap.sh собирает llama.cpp автоматически при первом запуске, если бинарника нет. Но полезно понимать, что происходит: git clone --depth=1 ggerganov/llama.cpp cd llama.cpp cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_UI=ON cmake --build build --config Release -j"$(nproc)" sourcecraft.dev -DLLAMA_BUILD_UI=ON — собирает родной веб-интерфейс llama.cpp (Svelte). Без него сервер отдаёт только API, а корень (/) пустой. -j$(nproc) — параллельная сборка на всех ядрах. На 4-ядерном CPU занимает 5–10 минут, на 8+ — 3–5 минут. Сборка требует Node.js для UI, но современные версии llama.cpp встраивают UI как статику, так что Node.js не нужен. Если сборка ругнётся на UI — -DLLAMA_BUILD_UI=OFF, но тогда родного интерфейса не будет (только API, а чат вы будете смотреть через Open WebUI). После сборки появляются бинарники в llama.cpp/build/bin/: llama-server — HTTP-сервер с API и UI (то, что использует наш скрипт). llama-cli — CLI для одной генерации (для отладки). llama-quantize — утилита квантования (пригодится в части про конвертацию моделей). Шаг 3. Скачивание тестовой модели Модель — Qwen3.5-2B-Q4_K_M. Квантование Q4_K_M — «золотая середина»: файл ~1.5 ГБ, качество близко к F16. Качаем через wget (без Python): mkdir -p models wget -c -O models/Qwen3.5-2B-Q4_K_M.gguf \ "huggingface.co" sourcecraft.dev -c — докачка при обрыве. Для больших файлов лучше wget -c, чем curl, потому что curl без -C - начинает заново. Правила раскладки: Обычные (текстовые) модели — можно плоско, прямо в models/. Имя файла — <модель>-<квант>.gguf. Мультимодальные модели — всегда в отдельной подпапке, вместе с mmproj-F16.gguf. Это требование режима роутера: он ищет mmproj только в той же папке, где лежит модель, и имя проектора должно начинаться с mmproj. Один mmproj на несколько квантований (например, Q4_K_M и Q8_0 одной модели, если одинаковые) — можно сделать симлинк: ln -s mmproj-F16.gguf mmproj-Q8_0.gguf sourcecraft.dev Но проще — один mmproj на папку, а в папке одно квантование. Почему mmproj — это отдельный файл, а не часть модели. mmproj (multimodal projector) — это «переходник» между визуальным энкодером и языковой моделью. В llama.cpp он подключается отдельным флагом --mmproj. Без него VL-модель работает как обычная текстовая (картинки не понимает), с ним — «видит». Подробнее — в шаге 5 ниже. Шаг 4. Ключевые опции llama-server Скрипт run_llama_mmap.sh — это обёртка над llama-server, которая добавляет умные дефолты. Разберём важные флаги, которые он использует. --load-mode mmap — главная опция --load-mode mmap sourcecraft.dev Заставляет llama.cpp использовать mmap для загрузки весов. Что это даёт: Модель не грузится в RAM целиком. Ядро отображает файл в адресное пространство процесса и подгружает страницы по требованию. Когда модель обращается к слою, страница подгружается с диска; когда слой больше не нужен — ядро может её вытеснить. Позволяет запускать модель больше RAM. Например, 20-гиговая модель на 10 ГБ RAM. Ядро держит в памяти «горячие» страницы, остальные — на диске. Нет износа SSD. В отличие от swap, mmap не пишет страницы обратно — данные уже на диске, их не нужно сохранять. Альтернативы: --load-mode none — грузит всю модель в RAM. Упадёт с OOM, если модель больше памяти. --load-mode mlock — грузит и блокирует в RAM (запрещает вытеснять). Быстро, но только если модель помещается. --load-mode dio — DirectIO, чтение в обход page cache. В некоторых версиях llama.cpp багован (создаёт временные буферы размером с модель). Не используйте без проверки. Что происходит на практике. Первый запрос к модели — самый медленный: ядро подгружает нужные страницы с диска. На HDD это 5–10 минут для 20-гиговой модели. Последующие запросы быстрее — часть страниц уже в page cache. Чем больше RAM свободно, тем больше «горячих» страниц, тем быстрее. --no-repack — экономия памяти на CPU --no-repack sourcecraft.dev llama.cpp при загрузке может создавать вторую копию весов, переупакованную для более быстрых вычислений (CPU_REPACK). Это почти удваивает пиковое потребление RAM. Для модели, которая и так не влезает, — фатально (был реальный случай: CPU model buffer 9.4 ГБ + CPU_REPACK 11.2 ГБ = 20.6 ГБ при лимите 10 ГБ -> OOM). --no-repack отключает переупаковку. Цена — ~10–15% скорости вычислений. Для нашего сценария «запустить хоть как-то» — правильный компромисс. -ctk q8_0 -ctv q8_0 — квантизация KV-cache --cache-type-k q8_0 --cache-type-v q8_0 sourcecraft.dev KV-cache — это память под контекст (история диалога). По умолчанию он в f16, что для длинного контекста съедает гигабайты. q8_0 сжимает его в 2 раза (~50% экономии) с минимальной потерей качества. Для моделей, которые «еле влезают» в лимит, это критично. --reasoning on | off | auto — «размышления» Qwen3.x --reasoning auto --reasoning-budget 768 sourcecraft.dev Новые Qwen-модели (3.x, 3.5) поддерживают режим «размышлений»: перед ответом модель генерирует цепочку рассуждений (reasoning_content), потом — финальный ответ. Это улучшает качество на сложных задачах (математика, RAG, поэзия), но: На тривиальных вопросах («Привет») модель может «думать» 400+ токенов, что на CPU занимает минуты. Отсюда --reasoning off для интерактивного чата. --reasoning-budget N — жёсткий лимит токенов на размышления. После N токенов движок принудительно закрывает блок размышлений и заставляет модель отвечать. Без него reasoning может «съесть» весь ctx-size и на ответ не останется места (реальный случай: модель «думала правильно», но ответа так и не дала — упёрлась в контекст). Рекомендация: auto (по умолчанию в скрипте) с --reasoning-budget 768. Для быстрых ответов — -rn (off). --ctx-size — размер контекста --ctx-size 3096 sourcecraft.dev Сколько токенов модель «помнит» (промпт + история + reasoning + ответ). Больше — лучше для длинных диалогов и RAG, но пропорционально больше памяти под KV-cache. Подбирайте под задачу: для «Привет/Как дела» хватит 2048, для саммари статьи — 4096+. --threads — число потоков CPU --threads $(( $(nproc) - 2 )) sourcecraft.dev Скрипт берёт nproc - 2: оставляет 2 ядра системе, чтобы она не подвисала, пока модель считает. Для 4-ядерного CPU — 2 потока, для 8-ядерного — 6. Больше потоков ≠ быстрее: на гипертрединге может быть медленнее из-за конкуренции за кэш. --models-dir и --models-max 1 — режим роутера --models-dir ./models --models-max 1 sourcecraft.dev Вместо -m <модель> скрипт по умолчанию запускает роутер: в UI виден дропдаун со всеми .gguf из models/, можно переключаться без перезапуска сервера. --models-max 1 гарантирует, что в памяти держится одна модель: при переключении старая выгружается, новая грузится. Для лимита 10 ГБ — обязательно, иначе роутер попытается держать несколько моделей и получит OOM. Альтернатива — режим одной модели: -m <имя> (или -s для интерактивного выбора). Тогда сервер грузит только эту модель, без роутера. Чуть быстрее стартует, чуть меньше памяти накладных расходов. Удобно, когда модель уже выбрана. Лимиты памяти через systemd-run systemd-run --user --unit=llama-server-$$ \ -p MemoryMax=10G \ -p MemoryHigh=9G \ -p MemorySwapMax=0 \ -- llama-server ... sourcecraft.dev Скрипт оборачивает запуск в cgroup с лимитами: MemoryMax=10G — жёсткий лимит. При превышении cgroup убивает процесс (OOM-kill). Система при этом остаётся жива. MemoryHigh=9G — мягкий лимит. При приближении к нему ядро начинает агрессивно вытеснять страницы mmap обратно на диск (не дожидаясь OOM). MemorySwapMax=0 — запрет swap. Без этого ядро могло бы сбрасывать анонимную память (KV-cache, буферы) в swap, что для mmap-модели — катастрофа по I/O. Значения MEM_MAX/MEM_HIGH можно переопределить через env: MEM_MAX=14G ./run_llama_mmap.sh. Шаг 5. mmproj — что это и как раскладывать mmproj (multimodal projector) — это небольшой файл (~300–600 МБ), который «переводит» выходы визуального энкодера (ViT) в пространство, понятное языковой модели. Без него VL-модель работает как обычная текстовая; с ним — «видит» картинки. Требования: Имя файла начинается с mmproj (например, mmproj-F16.gguf, mmproj-Qwen3VL-4B-F16.gguf). Лежит в той же папке, что и модель — особенно в режиме роутера, где llama.cpp ищет проектор по папке модели. Проверка, что модель «видит» картинки. После запуска сервера в логе должно появиться: clip_model_load: loaded meta data ... clip_model_load: model size = ... sourcecraft.dev Если этих строк нет — mmproj не подхватился, и при попытке отправить картинку сервер скажет Images require a vision-capable model. Что делать, если mmproj нет для вашей модели: Не страшно, если vision не нужен — модель работает как текстовая. Если vision нужен, но mmproj нет — ищите его в том же репозитории на Hugging Face (обычно mmproj-*.gguf лежит рядом с .gguf-моделью). Не путайте mmproj с LLM-энкодером для генерации изображений (там --llm в sd.cpp — это текстовый энкодер, не то же самое). Код запуска run_llama_mmap.sh #!/usr/bin/env bash Запуск llama-server с mmap для модели, превышающей RAM. Usage: ./run_llama_mmap.sh # Переопределяемые env: MEM_MAX, MEM_HIGH, PORT, HOST, CTX_SIZE MEM_MAX=10G — жёсткий лимит (OOM-kill при превышении) MEM_HIGH=9G — мягкий лимит (начинается активное вытеснение) CTX_SIZE=2048 — маленький контекст экономит KV-cache # Мониторинг в другом терминале: watch -n1 'free -h; echo; cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/*/memory.pressure 2>/dev/null' set -euo pipefail --- Конфигурация --- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LLAMA_DIR="$SCRIPT_DIR/llama.cpp" MODELS_DIR="$SCRIPT_DIR/models" --- API-ключ (аутентификация) --- По умолчанию llama-server работает БЕЗ аутентификации. Чтобы включить защиту, задайте ключ: - вручную: API_KEY="ваш-секретный-ключ" - или оставьте пустым: API_KEY="" # Если API_KEY пуст — флаг --api-key НЕ добавляется, сервер открыт. Если API_KEY задан — флаг --api-key добавляется, и тогда: 1. Все API-запросы требуют заголовок Authorization: Bearer <ключ>. 2. Web UI загружается свободно (страница чата открывается), но для отправки сообщений нужно вписать ключ в настройках: Settings (иконка шестерёнки) -> General -> поле "API Key". --- Чтение секретов --- SECRETS_FILE="$SCRIPT_DIR/.secrets" if [ -f "$SECRETS_FILE" ]; then shellcheck disable=SC1090 set -a; source "$SECRETS_FILE"; set +a fi LLAMA_API_KEY="${LLAMA_API_KEY:-}" LOAD_MODE="mmap" PORT="${PORT:-8080}" HOST="${HOST:-0.0.0.0}" CTX_SIZE="${CTX_SIZE:-3096}" REASONING_BUDGET="${REASONING_BUDGET:-768}" # по умолчанию 768 токенов на размышления MEM_MAX="${MEM_MAX:-10G}" MEM_HIGH="${MEM_HIGH:-9G}" NPROC="$(nproc)" THREADS=$(( NPROC - 2 )) [ "$THREADS" -lt 2 ] && THREADS=2 --- Разбор аргументов скрипта --- SINGLE_MODEL=0 REASONING="${REASONING:-auto}" while [ $# -gt 0 ]; do case "$1" in -s|--single) SINGLE_MODEL=1 ;; -m|--model) MODEL_NAME_ARG="$2"; SINGLE_MODEL=1; shift 2 ;; -r|--reasoning) REASONING="on" ;; -rn|--reasoning-off) REASONING="off" ;; -h|--help) cat <<EOF Usage: $(basename "$0") [-s|--single] [-r|--reasoning] [-rn|--reasoning-off] Без флагов router mode — все .gguf из ./models видны в UI, переключение через дропдаун, в памяти одна модель (--models-max 1), режим размышлений auto. -s, --single выбор одной модели из консоли и загрузка сервера только с этой моделью. -r, --reasoning включить режим размышлений -rn, --reasoning-off выключить режим размышлений EOF exit 0 ;; *) echo "Неизвестный флаг: $1 (см. --help)"; exit 1 ;; esac shift done --- 1. Сборка llama.cpp при первом запуске --- LLAMA_SERVER="$LLAMA_DIR/build/bin/llama-server" if [ ! -x "$LLAMA_SERVER" ]; then echo "[*] Собираю llama.cpp (первый запуск, может занять несколько минут)..." if [ ! -d "$LLAMA_DIR/.git" ]; then git clone --depth=1 ggerganov/llama.cpp "$LLAMA_DIR" fi cmake -S "$LLAMA_DIR" -B "$LLAMA_DIR/build" -DCMAKE_BUILD_TYPE=Release -DLLAMA_BUILD_UI=ON cmake --build "$LLAMA_DIR/build" --config Release -j "$NPROC" fi [ -x "$LLAMA_SERVER" ] || { echo "[!] Сборка не удалась: $LLAMA_SERVER не найден"; exit 1; } --- 2. Модели --- mkdir -p "$MODELS_DIR" Папка с медиа-моделями (исключаем её из списка текстовых) MEDIA_MODELS_DIR="$MODELS_DIR/media" mapfile -t MODELS < <( find "$MODELS_DIR" -maxdepth 2 -type f -name "*.gguf" \ ! -name "mmproj" \ ! -path "$MEDIA_MODELS_DIR/*" \ | sort ) if [ ${#MODELS[@]} -eq 0 ]; then echo "[!] В $MODELS_DIR нет .gguf файлов."; exit 1 fi if [ "$SINGLE_MODEL" -eq 1 ]; then --- Режим одной модели if [ -n "${MODEL_NAME_ARG:-}" ]; then Нормализация: добавить .gguf, если нет [[ "$MODEL_NAME_ARG" != *.gguf ]] && MODEL_NAME_ARG="${MODEL_NAME_ARG}.gguf" MODEL_PATH="" for m in "${MODELS[@]}"; do if [ "$(basename "$m")" = "$MODEL_NAME_ARG" ]; then MODEL_PATH="$m" break fi done [ -n "$MODEL_PATH" ] || { echo "[!] Модель '$MODEL_NAME_ARG' не найдена. Доступные:" for m in "${MODELS[@]}"; do echo " $(basename "$m")"; done exit 1 } else if [ ${#MODELS[@]} -eq 1 ]; then MODEL_PATH="${MODELS[0]}" else echo "[*] Найдено несколько моделей:" select MODEL_PATH in "${MODELS[@]}"; do [ -n "${MODEL_PATH:-}" ] && break done fi fi echo "[*] Режим одной модели: $MODEL_PATH ($(du -h "$MODEL_PATH" | cut -f1))" else --- Режим роутера моделей echo "[*] Router mode: каталог $MODELS_DIR (${#MODELS[@]} шт.), --models-max 1" fi --- 3. Проверка занятости порта --- for h in 127.0.0.1 0.0.0.0; do if (echo >/dev/tcp/$h/$PORT) 2>/dev/null; then echo "[!] Порт $PORT уже занят ($h). Остановите другой процесс или смените PORT." exit 1 fi done --- 4. Определение доступных флагов (на случай старых/новых версий) --- HELP_OUT="$("$LLAMA_SERVER" --help 2>&1 || true)" OPT_FLAGS=() echo "$HELP_OUT" | grep -q -- "--no-warmup" && OPT_FLAGS+=("--no-warmup") echo "$HELP_OUT" | grep -q -- "--flash-attn" && OPT_FLAGS+=("-fa" "on") echo "$HELP_OUT" | grep -q -- "--no-repack" && OPT_FLAGS+=("--no-repack") Отключение reasoning для Qwen3.x (если поддерживается) REASON_ARGS=() if echo "$HELP_OUT" | grep -q -- "--reasoning "; then case "$REASONING" in on) REASON_ARGS+=(--reasoning on) ;; auto) REASON_ARGS+=(--reasoning auto) ;; off) REASON_ARGS+=(--reasoning off) ;; esac Бюджет: 0 для off, REASONING_BUDGET для on/auto if echo "$HELP_OUT" | grep -q -- "--reasoning-budget"; then if [ "$REASONING" = "off" ]; then REASON_ARGS+=(--reasoning-budget 0) else REASON_ARGS+=(--reasoning-budget "$REASONING_BUDGET") fi fi fi KV-cache квантизация (экономит ~50% памяти под контекст) KV_ARGS=() if echo "$HELP_OUT" | grep -q -- "--cache-type-k"; then KV_ARGS+=("-ctk" "q8_0" "-ctv" "q8_0") fi echo "[*] Load mode: $LOAD_MODE" [ ${#OPT_FLAGS[@]} -gt 0 ] && echo "[] Флаги: ${OPT_FLAGS[]}" [ ${#KV_ARGS[@]} -gt 0 ] && echo "[] KV-квантизация: ${KV_ARGS[]}" [ ${#REASON_ARGS[@]} -gt 0 ] && echo "[] Reasoning: ${REASON_ARGS[]}" Ищет mmproj-файл, соответствующий модели только при запуске одной модели, роутер находит автоматически. $1 — полный путь к .gguf файлу модели. Возвращает путь к mmproj через echo или пустую строку, если не найден. find_mmproj_for_model() { local model_path="$1" local model_dir model_file model_base short_name model_dir="$(dirname "$model_path")" model_file="$(basename "$model_path")" Если это сам файл mmproj — не ищем для него проектор if [[ "$model_file" == mmproj ]]; then return 1 fi model_base="${model_file%.gguf}" short_name="$(echo "$model_base" | sed -E 's/[-_](Q[0-9]+(_[A-Z0-9]+)|F16|F32|BF16|UD-Q[0-9]+(_[A-Z0-9]+))$//')" Приоритет 1: точное совпадение "mmproj-<short_name>.gguf" local candidate="$model_dir/mmproj-${short_name}.gguf" if [ -f "$candidate" ]; then echo "$candidate" return 0 fi Приоритет 2: любой mmproj в папке, имя которого содержит short_name (плоская структура: mmproj-<имя-модели>.gguf рядом с моделью) for f in "$model_dir"/mmproj.gguf "$model_dir"/.mmproj.gguf; do [ -f "$f" ] || continue if [[ "$(basename "$f")" == "$short_name" ]]; then echo "$f" return 0 fi done Приоритет 3: любой mmproj в папке модели (структура роутера: model.gguf + mmproj-F16.gguf в одной подпапке) for f in "$model_dir"/mmproj.gguf; do [ -f "$f" ] && { echo "$f"; return 0; } done echo "" return 1 } --- 5. Сборка команды --- SERVER_CMD=( "$LLAMA_SERVER" --host "$HOST" --port "$PORT" --threads "$THREADS" --ctx-size "$CTX_SIZE" --load-mode "$LOAD_MODE" -lv 4 "${OPT_FLAGS[@]}" "${KV_ARGS[@]}" "${REASON_ARGS[@]}" ) if [ "$SINGLE_MODEL" -eq 1 ]; then Одна модель, заданная явно SERVER_CMD+=(-m "$MODEL_PATH") ищем mmproj для мультимодальных моделей MMPROJ_PATH="$(find_mmproj_for_model "$MODEL_PATH" || true)" if [ -n "$MMPROJ_PATH" ]; then echo "[*] Найден mmproj: $(basename "$MMPROJ_PATH")" else Не ошибка: модель может быть текстовой echo "[*] mmproj не найден (модель будет работать как текстовая)" fi [ -n "$MMPROJ_PATH" ] && SERVER_CMD+=(--mmproj "$MMPROJ_PATH") else Router mode — модель в памяти одна, но можно переключаться в UI SERVER_CMD+=(--models-dir "$MODELS_DIR" --models-max 1) fi if [ -n "$LLAMA_API_KEY" ]; then SERVER_CMD+=(--api-key "$LLAMA_API_KEY") echo "[*] Аутентификация включена (API-ключ задан)" else echo "[*] Аутентификация отключена (ключ не задан)" fi --- 6. Подготовка фона и trap --- LOG_FILE="$(mktemp -t llama-server.XXXXXX.log)" SERVER_PID="" TAIL_PID="" UNIT_NAME="llama-server-$$" USE_SYSTEMD=0 cleanup() { [ -t 1 ] && echo "" [ -t 1 ] && echo "[*] Завершение..." if [ "$USE_SYSTEMD" -eq 1 ]; then systemctl --user stop "$UNIT_NAME" 2>/dev/null || true fi if [ -n "$SERVER_PID" ]; then kill "$SERVER_PID" 2>/dev/null || true wait "$SERVER_PID" 2>/dev/null || true fi [ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null || true rm -f "$LOG_FILE" [ -t 1 ] && echo "[*] Готово." } trap cleanup EXIT INT TERM --- 7. Запуск с лимитом памяти через systemd-run --- if command -v systemd-run &>/dev/null; then USE_SYSTEMD=1 echo "[*] Запуск под systemd-run: MemoryMax=$MEM_MAX, MemoryHigh=$MEM_HIGH, swap=off" if ! systemd-run --user --unit="$UNIT_NAME" \ -p MemoryMax="$MEM_MAX" \ -p MemoryHigh="$MEM_HIGH" \ -p MemorySwapMax=0 \ -p StandardOutput=file:"$LOG_FILE" \ -p StandardError=file:"$LOG_FILE" \ -- "${SERVER_CMD[@]}" >/dev/null 2>&1; then echo "[!] systemd-run недоступен, откатываюсь на запуск без лимита." USE_SYSTEMD=0 fi fi if [ "$USE_SYSTEMD" -eq 0 ]; then echo "[*] Запуск в фоне без лимита памяти" "${SERVER_CMD[@]}" >"$LOG_FILE" 2>&1 & SERVER_PID=$! fi Пробрасываем лог сервера в терминал в реальном времени если скрипт привязан к терминалу [ -t 1 ] && tail -n +1 -f "$LOG_FILE" & TAIL_PID=$! --- 8. Ожидание готовности --- echo "[*] Жду готовности сервера (до 600 с)..." READY=0 for _ in $(seq 1 600); do if [ "$USE_SYSTEMD" -eq 1 ]; then systemctl --user is-active "$UNIT_NAME" &>/dev/null || { echo "[!] Сервер умер на старте. Последние строки лога:" tail -n 40 "$LOG_FILE"; exit 1; } else kill -0 "$SERVER_PID" 2>/dev/null || { echo "[!] Сервер умер на старте. Последние строки лога:" tail -n 40 "$LOG_FILE"; exit 1; } fi if curl -sf "http://127.0.0.1:$PORT/health" >/dev/null 2>&1; then READY=1; break fi sleep 1 done [ "$READY" -eq 1 ] || { echo "[!] Сервер не поднялся за 120 с."; exit 1; } --- 9. Плашка для пользователя --- echo "" echo "============================================================" echo " llama.cpp server is UP" echo "------------------------------------------------------------" echo " Web UI: http://127.0.0.1:$PORT" echo " OpenAI API: http://127.0.0.1:$PORT/v1" echo " Шпаргалка: file://$SCRIPT_DIR/web/models.html" if [ "$SINGLE_MODEL" -eq 1 ]; then echo " Режим: одна модель ($(basename "$MODEL_PATH"))" else echo " Режим: router (модели из ./models, max 1 в RAM)" fi echo " Threads: $THREADS (из $NPROC)" echo " Ctx size: $CTX_SIZE" if [ "$USE_SYSTEMD" -eq 1 ]; then echo " Mem cap: $MEM_MAX (systemd-run, swap=off)" else echo " Mem cap: БЕЗ ЛИМИТА (systemd-run недоступен)" fi echo "------------------------------------------------------------" echo " Ctrl+C — остановить." echo "============================================================" echo "" --- 10. Ожидание и мониторинг падений --- while true; do if [ "$USE_SYSTEMD" -eq 1 ]; then systemctl --user is-active "$UNIT_NAME" &>/dev/null || { echo "[!] Сервер остановлен."; break; } else kill -0 "$SERVER_PID" 2>/dev/null || { echo "[!] Сервер остановлен."; break; } fi sleep 10 done sourcecraft.dev Проверка после запуска 1. Сервер поднялся: ./run_llama_mmap.sh -m Qwen3.5-2B-Q4_K_M sourcecraft.dev В терминале должна появиться плашка: ============================================================ llama.cpp server is UP ------------------------------------------------------------ Web UI: 127.0.0.1 OpenAI API: 127.0.0.1 ...sourcecraft.dev 2. Родной UI открывается: Откройте 127.0.0.1 в браузере. Выберите модель в дропдауне (router mode) или она уже выбрана (single mode). Отправьте «Привет» — должен прийти ответ. 3. API отвечает: curl 127.0.0.1 \ -H "Content-Type: application/json" \ -d '{"messages":[{"role":"user","content":"Привет"}],"stream":false}'sourcecraft.dev 4. Проверить память: RSS процесса (сколько реально в RAM) ps -o pid,comm,rss,%mem -C llama-server cgroup: сколько занято и какое давление cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/llama-server-*.service/memory.current cat /sys/fs/cgroup/user.slice/user-$(id -u).slice/user@$(id -u).service/llama-server-*.service/memory.pressure sourcecraft.dev Если memory.current близко к MEM_MAX, а memory.pressure растёт — модель активно «подгружает» страницы с диска. Это нормально для mmap, просто медленно.

  7. On-Device LLM Inference on Android with Microsoft Decision-1 and Qwen3.5-9B --- title: "On-Device LLM Inference on Android: Running Qwen3.5-9B with NNAPI and Vulkan Delegate" published: true description: "A production guide to deploying Qwen3.5-9B on Snapdragon 8 Gen 3 — covering Q4_K_M vs Q5_K_S quantization tradeoffs, mmap weight loading, and achieving sub-400ms TTFT." tags: android, kotlin, mobile, architecture canonical_url: https://blog.mvpfactory.co/on-device-llm-android-qwen3-nnapi --- What You Will Build By the end of this tutorial you will have a working Android inference pipeline that runs Qwen3.5-9B on a Snapdragon 8 Gen 3 device, stays under 2.5 GB of LPDDR5 memory, and delivers time-to-first-token under 400ms. Let me show you the pattern I use in every production on-device LLM deployment. Open-weight 9B models are no longer a server-only story. The hardware caught up: Snapdragon 8 Gen 3 ships with ~77 GB/s LPDDR5 bandwidth and a Hexagon NPU rated at 73 TOPS INT8. The remaining question is purely about quantization and delegation strategy — and that is exactly what we are covering here. --- Prerequisites - Android device with Snapdragon 8 Gen 3 (or simulator for architecture work) - llama.cpp Android port with JNI bindings configured - Qwen3.5-9B GGUF weights downloaded (Q4_K_M format — more on why below) - Android GPU Inspector installed for delegation profiling - minSdkVersion 29 or above (required for NNAPI stable paths) --- Step 1 — Choose Q4_K_M, Not Q5 Here is the gotcha that will save you hours: the quantization format is not a quality tradeoff on Android. It is a deployment gate. Your app gets approximately 2.5 GB of LPDDR5 (the OS reserves ~1.5 GB). Here is what the numbers actually look like at runtime: | Format | GGUF File Size | Runtime RSS (2K ctx) | PPL Delta vs FP16 | Avg Token Latency | TTFT | |---|---|---|---|---|---| | Q4_K_M | ~5.3 GB | ~2.1 GB | +0.18 | ~28 ms/tok | ~340 ms | | Q5_K_S | ~6.2 GB | ~2.5 GB | +0.09 | ~34 ms/tok | ~410 ms | | Q5_K_M | ~6.7 GB | ~2.7 GB | +0.07 | ~38 ms/tok | ~460 ms | Q5_K_S consumes the full 2.5 GB budget with zero headroom for KV cache (which costs ~280 MB at 2K context, plus ~350 MB for activation buffers). Q4_K_M leaves ~150 MB of runtime margin — enough to survive background memory pressure without an OOM kill. Do not debate Q5 until you have a device with 12 GB RAM. --- Step 2 — Memory-Map Your Weights Do not load model weights into the heap. Here is the minimal setup to get this working: val weightFile = File(modelDir, "qwen3.5-9b-q4_k_m.gguf") val channel = FileInputStream(weightFile).channel val mappedBuffer = channel.map( FileChannel.MapMode.READ_ONLY, 0L, channel.size() ) // Pass mappedBuffer directly to llama.cpp JNI binding nativeLoadFromMappedBuffer(mappedBuffer) The OS page cache handles eviction gracefully under memory pressure. Cold-start latency drops from 3–4 seconds to under 800ms because the kernel only faults in pages touched during the actual forward pass. --- Step 3 — Split Delegation by Operator Type The docs do not mention this clearly, but llama.cpp's default "auto" mode often leaves performance on the table. Mixed delegation is the right call: - NNAPI — handles quantized INT8 matrix multiplications efficiently on the Hexagon DSP and manages power budget better during sustained sessions. Route MLP layers here. - Vulkan compute — lower driver overhead for attention operations and handles irregular memory access patterns more gracefully. Route the attention mechanism here. Profile with Android GPU Inspector before committing. SoC vendor implementations vary enough that assumptions will cost you latency. --- Step 4 — Chunk-Prefill Batching for Sub-400ms TTFT Time-to-first-token is dominated by prompt prefill. Process the prompt in 64-token batches with double-buffering on weight reads: val CHUNK_SIZE = 64 val promptTokens = tokenizer.encode(userPrompt) promptTokens.chunked(CHUNK_SIZE).forEach { chunk -> nativePrefillChunk(chunk.toIntArray()) } // First decode step now starts from warm KV cache val firstToken = nativeDecode() This trades roughly 10–20% higher total prefill time for dramatically lower perceived latency. Users experience responsiveness; wall-clock throughput is a secondary metric. --- Gotchas android:largeHeap="true" will silently kill your mmap. On 32-bit ABI targets, largeHeap expands the heap address space region and directly competes with your mmap virtual address range. The model mmap can fail with ENOMEM even when physical RAM is available. The default is false — verify yours explicitly rather than assuming. Even on 64-bit targets, the expanded heap increases GC pressure during inference. Thermal throttle window is real. The Snapdragon 8 Gen 3 sustains full performance for roughly 8–12 minutes before throttling. Design your session length expectations accordingly, especially for production chat interfaces. GGUF file size and runtime RSS are not the same. mmap loads pages lazily, but the kernel still maps the full virtual address range. Always measure RSS, not file size, when evaluating your memory budget. --- Conclusion Q4_K_M, mmap loading, mixed NNAPI/Vulkan delegation, and chunk-prefill batching — that is the complete stack for production Qwen3.5-9B on Android. Each decision has a concrete reason behind it rooted in the hardware constraints of Snapdragon 8 Gen 3. Start here, profile on your specific target device, and do not optimize further until the data tells you to. Resources: - llama.cpp Android port - Android GPU Inspector - GGUF format specification

    1 reaction
  8. Building a Llama.cpp Container Image Llama.cpp is a high-performance inference platform designed for Large Language Models (LLMs) like Llama, Falcon, and Mistral. It provides a streamlined development environment compatible with both CPU and GPU systems. This guide explains how to build Llama.cpp into a Docker container image for CPU and GPU systems and push the finished image to a container registry. By the end, you'll have CPU and GPU container images of Llama.cpp built, tagged, and pushed to a container registry. --- Prerequisites - A server with Docker installed (a GPU-enabled server with NVIDIA drivers and the NVIDIA Container Toolkit for the GPU build). - Access to the server using SSH. - Start the Docker service. ```console $ sudo systemctl start docker ``` - Add the non-root user to the Docker group. For example, linuxuser. ```console $ sudo usermod -aG docker linuxuser ``` - Switch to the user: ```console $ su - linuxuser ``` 1. Set Up the Project Directory 1. Create a new directory to store your Llama.cpp project files: $ mkdir llama-project 2. Switch to the directory: $ cd llama-project 3. Clone the Llama.cpp project repository using Git: $ git clone https://github.com/ggerganov/llama.cpp/ 4. List files and verify that a new llama.cpp directory is available: $ ls Output: llama.cpp 5. Switch to the llama.cpp project directory: $ cd llama.cpp 6. List all hidden files and verify that a new .devops directory is available: $ ls -a Output: .. cmake convert.py flake.lock ggml-common.h ggml-metal.h ggml-quantsh .git llama.cpp CMakeLists.txt .devops The .devops directory contains the following Dockerfile resources: - main.Dockerfile: Contains the build context for CPU systems. - main-cuda.Dockerfile: Contains the build context for GPU systems. Use these resources in the next sections to build a CPU or GPU system container image. 2. Build a Llama.cpp Container Image for CPU Systems Follow the steps below to build a new Llama.cpp container image using main.Dockerfile, which contains the required build context and installs all necessary dependency packages for CPU-based systems. 1. Copy the main.Dockerfile from the .devops directory to the main llama.cpp project directory: $ cp .devops/main.Dockerfile . 2. Build a new container image using main.Dockerfile with all files in the project directory. Replace llama-image with your desired image name: $ docker build -f main.Dockerfile -t llama-image . 3. View all Docker images on the server and verify that a new llama-image is available: $ docker images Output: REPOSITORY TAG IMAGE ID CREATED SIZE llama-image latest f4fcb571a2f7 About a minute ago 80.1MB 3. Build a Llama.cpp Container Image for GPU Systems The Llama.cpp main-cuda.Dockerfile resource contains the build context for NVIDIA GPU systems that run the latest CUDA driver packages. Follow the steps below to build a Llama container image compatible with GPU systems. 1. Copy main-cuda.Dockerfile to the Llama.cpp project directory: $ cp .devops/main-cuda.Dockerfile . 2. Build a new container image llama-gpu-image using the main-cuda.Dockerfile with all files in the working project directory: $ docker build -f main-cuda.Dockerfile -t llama-gpu-image . 3. View all Docker images on the server and verify that the new llama-gpu-image is available: $ docker images Output: REPOSITORY TAG IMAGE ID CREATED SIZE llama-gpu-image latest 40421f1469cf 2 minutes ago 2.08GB To run the Llama.cpp GPU container image, verify that your target host includes the minimum or higher CUDA version referenced by the ARG CUDA_VERSION= directive within main-cuda.Dockerfile. 4. Verify the target CUDA version: $ cat main-cuda.Dockerfile | grep CUDA_VERSION= Output: ARG CUDA_VERSION=12.3.1 4. Push the Llama.cpp Container Image to a Container Registry 1. Open your container registry's management panel and copy the registry URL, username, and API key (or password) to use when accessing the registry. 2. Switch to your server terminal session and log in to your container registry. Replace your-registry-url, exampleregistry, exampleuser, and registry-password with your actual registry details: $ docker login https://your-registry-url/exampleregistry -u exampleuser -p registry-password 3. Tag the Llama.cpp container image with your desired registry tag. For example, your-registry-url/exampleregistry/llama-gpu-image: $ docker tag llama-gpu-image your-registry-url/exampleregistry/llama-gpu-image 4. View all Docker images on the server and verify that the new tagged image is available: $ docker images Output: REPOSITORY TAG IMAGE ID CREATED SIZE your-registry-url/exampleregistry/llama-gpu-image latest 40421f1469cf 3 minutes ago 2.08GB llama-gpu-image latest 40421f1469cf 3 minutes ago 2.08GB 5. Push the tagged image to your container registry repository: $ docker push your-registry-url/exampleregistry/llama-gpu-image 6. Open your container registry's management panel and check the repositories list to verify that the new repository is available. Next Steps - Automate the CPU and GPU builds in a CI/CD pipeline so new images are rebuilt whenever the Llama.cpp repository updates. - Pull the GPU image onto a multi-GPU host and benchmark inference throughput across different quantization levels. - Pin the CUDA_VERSION in main-cuda.Dockerfile to match the driver version installed on your target hosts. - Explore Llama.cpp's server mode to expose an OpenAI-compatible API from the container. For the full guide with additional tips, visit the original article on Vultr Docs.

    5 reactions · 1 comment
  9. The call script (booking a table) in the reel is our own dramatisation. GibberLink: PennyroyalTea/gibberlink ggwave: ggerganov/ggwave

    3 views
  10. @luqman_talks here is the language they use BTW ggerganov/ggwave

    1 like · 153 views
Trending awards, every completed day and week
Gold
11st place finishes
Silver
02nd place finishes
Bronze
03rd place finishes
Awarded repositories
1Owned by ggerganov now

Repositories

Ordered by gold, then silver, then bronze.

  1. wave-shareBest: 1st by stars, 587 stars100

Every award

Top-three finishes of these repositories in completed trending days and weeks, newest first.

  1. #1 by starsDaily trending
    587 stars
    wave-share

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.