From f0ae194f33077691f85db62c6c80e68bb99e26de Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sun, 18 Oct 2026 10:02:05 +0300 Subject: [PATCH 1/2] feat(export): write the SHA-256 sidecar that verify-gguf and the documentation rely on `forgelm export` computed the digest of the GGUF but never wrote `.sha256`; the only writer in the package was the checkpoint-tarball helper. `forgelm verify-gguf`, the user manual and the GGUF reference all describe the sidecar as something the exporter produces, so the sidecar check had nothing to compare against unless the operator created the file by hand. The exporter now writes `.sha256` in `sha256sum` binary-mode format (` *`) through a temporary name and a rename. A sidecar that cannot be written fails the export (exit 2, `error_kind="runtime"`) rather than leaving a GGUF the documentation says is covered; the digest is still reported. For a K-quant request the file written is the intermediate `*.f16.gguf`, so the sidecar belongs to that file. A `model_integrity.json` that cannot be updated is now logged as a warning instead of at debug level. The sidecar detects corruption and accidental change; it is not tamper evidence, and the pages now say so. The same change rewrites the parts of the GGUF pages that described things the code does not do: the invented manifest and `verify-gguf` output (vocabulary and tokenizer checks that do not exist), K-quants shown as one-step, "always merges LoRA", and a clean `verify-gguf` exit presented as more than it proves. English and Turkish. Refs #83, #84, #85 Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 9 ++ docs/reference/verify_gguf_subcommand-tr.md | 4 +- docs/reference/verify_gguf_subcommand.md | 4 +- docs/usermanuals/en/deployment/gguf-export.md | 72 +++++++----- docs/usermanuals/en/deployment/verify-gguf.md | 8 +- docs/usermanuals/tr/deployment/gguf-export.md | 72 +++++++----- docs/usermanuals/tr/deployment/verify-gguf.md | 8 +- forgelm/export.py | 48 +++++++- forgelm/verify/_gguf.py | 3 +- tests/test_export.py | 106 ++++++++++++++++++ 10 files changed, 263 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d44dd02..ecfc068c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ All notable changes to ForgeLM are documented here. ### Added +- `forgelm export` now writes `.sha256` next to the GGUF (`sha256sum` format), the file `forgelm verify-gguf` + and the documentation always expected. It had never been written, so a sidecar-based check had nothing to compare. + A sidecar that cannot be written fails the export (exit 2) instead of leaving a GGUF without it; for a K-quant + request it belongs to the intermediate `*.f16.gguf`. A `model_integrity.json` that cannot be updated is now logged + as a warning, not at debug level (#84). - `CITATION.cff`, so the repository can be cited through GitHub's "Cite this repository" panel (#964). - A contributor can claim a `good first issue` by commenting `/assign` and give it @@ -68,6 +73,10 @@ All notable changes to ForgeLM are documented here. ### Fixed +- Correct the GGUF pages (English and Turkish): the export example shows the real output, K-quants are described as + the two-step operation they are, the invented "manifest" and `verify-gguf` output (vocabulary and tokenizer checks that + do not exist) are replaced by the real ones, a clean `verify-gguf` exit is said to prove only what ran, and the sidecar + is described as corruption detection, not tamper evidence (#83, #84, #85). - Fix the KTO bullet on the Alignment overview user-manual page (English and Turkish): it named the fields `(prompt, response, good?)`, but the KTO loader reads `prompt`, `completion` and a boolean `label` (#965). diff --git a/docs/reference/verify_gguf_subcommand-tr.md b/docs/reference/verify_gguf_subcommand-tr.md index 0b092908..f53fe5dd 100644 --- a/docs/reference/verify_gguf_subcommand-tr.md +++ b/docs/reference/verify_gguf_subcommand-tr.md @@ -43,7 +43,7 @@ Kodlar `forgelm/cli/subcommands/_verify_gguf.py::_run_verify_gguf_cmd` tarafınd | **Meta veri bloğu** | İsteğe bağlı `gguf` paketi yüklüyse. Üst kaynak okuyucu ile meta veri + tensor tanımlarını ayrıştırır. | Okuyucu ayrıştırma sırasında istisna fırlatır → çıkış kodu sidecar'ın ne kanıtladığına bağlıdır; çünkü ayrıştırma hatası tek başına bozuk bir dosyayı, bu dosyanın format revizyonunu okuyamayacak kadar eski bir `gguf` paketinden ayırt edemez. **Sidecar yok** → çıkış `6` (bozulmayı eleyecek hiçbir şey yok; dosya kesilmiş / hasarlı sayılmalı). **Eşleşen sidecar** → çıkış `1` (dosya export edilenle bayt-bayt aynı — `gguf`'u yükseltip yeniden çalıştırın; artefakt sahibini çağırmayın). **Uyuşmayan sidecar** → çıkış `6`, sidecar uyuşmazlığı olarak raporlanır (checksum daha güçlü kanıttır ve baskın gelir). Paket yoksa → kontrol atlanır (magic + sidecar kontrolleri yük taşır). | | **SHA-256 sidecar** | `.sha256` mevcutsa. Dosyanın SHA-256'sını yeniden hesaplar ve sidecar'ın ilk boşluk-ayrılı token'ı ile karşılaştırır (sha256sum formatı ` *` desteklenir). | İyi-biçimli ama uyuşmayan digest → çıkış `6` (dosya export sonrası değişmiş). Sidecar mevcut ama içeriği 64 karakterlik hex digest değilse → çıkış `1` (bozuk-sidecar maskelenmesine karşı kapalı başarısızlık — hiçbir şey karşılaştırılmadı). Sidecar yoksa → kontrol sessizce atlanır. | -Exporter sidecar'ı varsayılan olarak yazar (bkz. [`docs/usermanuals/tr/deployment/gguf-export.md`](../usermanuals/tr/deployment/gguf-export.md)); GGUF dosyalarını üçüncü taraflardan alan operatörler sidecar'ı da talep etmelidir. +Exporter sidecar'ı varsayılan olarak yazar (K-quant isteğinde sidecar ara f16 dosyasına aittir; bkz. [`docs/usermanuals/tr/deployment/gguf-export.md`](../usermanuals/tr/deployment/gguf-export.md)); GGUF dosyalarını üçüncü taraflardan alan operatörler sidecar'ı da talep etmelidir. ## Emit edilen audit event'leri @@ -142,7 +142,7 @@ Aynı dosyanın yanında **hiç** sidecar yoksa karar bunun yerine `6` olur — ### İsteğe bağlı bağımlılık yok -`gguf` paketi yüklü değilse meta veri ayrıştırma katmanı sessizce atlanır — magic + sidecar kontrolleri yük taşımaya devam eder: +`gguf` paketi yüklü değilse meta veri ayrıştırma katmanı atlanır (`metadata_parsed: False`). O zaman denetlenen yalnızca magic ve sidecar'dır; sidecar da yoksa temiz bir çıkış yalnızca magic header'ı kanıtlar: ```shell $ pip uninstall -y gguf diff --git a/docs/reference/verify_gguf_subcommand.md b/docs/reference/verify_gguf_subcommand.md index c91506ca..55a970cf 100644 --- a/docs/reference/verify_gguf_subcommand.md +++ b/docs/reference/verify_gguf_subcommand.md @@ -43,7 +43,7 @@ The codes are emitted by `forgelm/cli/subcommands/_verify_gguf.py::_run_verify_g | **Metadata block** | When the optional `gguf` package is installed. Parses the metadata + tensor descriptors via the upstream reader. | Reader raises mid-parse → the exit code depends on what the sidecar can prove, because the parse error alone cannot tell a corrupted file apart from a `gguf` package too old for this file's format revision. **No sidecar** → exit `6` (nothing available to rule out corruption; treat the file as truncated / damaged). **Matching sidecar** → exit `1` (the file is byte-identical to what was exported — upgrade `gguf` and re-run; do not page the artifact owner). **Mismatching sidecar** → exit `6`, reported as the sidecar mismatch (the checksum is the stronger evidence and dominates). Package absent → check is skipped (the magic + sidecar checks remain load-bearing). | | **SHA-256 sidecar** | When `.sha256` exists. Recomputes file SHA-256 and compares against the sidecar's first whitespace-separated token (sha256sum format ` *` is supported). | Well-formed digest that disagrees → exit `6` (file changed after export). Sidecar present but contents are not a 64-character hex digest → exit `1` (fail closed against malformed-sidecar masquerade — nothing was compared). Sidecar absent → check is skipped silently. | -The exporter writes the sidecar by default (see [`docs/usermanuals/en/deployment/gguf-export.md`](../usermanuals/en/deployment/gguf-export.md)); operators receiving GGUF files from third parties should request the sidecar alongside. +The exporter writes the sidecar by default (for a K-quant request it belongs to the intermediate f16 file; see [`docs/usermanuals/en/deployment/gguf-export.md`](../usermanuals/en/deployment/gguf-export.md)); operators receiving GGUF files from third parties should request the sidecar alongside. ## Audit events emitted @@ -142,7 +142,7 @@ With **no** sidecar beside the same file the verdict is `6` instead — nothing ### Optional dependency absent -When the `gguf` package is not installed, the metadata-parse layer is skipped silently — the magic + sidecar checks remain load-bearing: +When the `gguf` package is not installed, the metadata-parse layer is skipped (`metadata_parsed: False`). Magic and sidecar are then all that is checked, and with no sidecar either, a clean exit proves only the magic header: ```shell $ pip uninstall -y gguf diff --git a/docs/usermanuals/en/deployment/gguf-export.md b/docs/usermanuals/en/deployment/gguf-export.md index 0303839f..2d898d7b 100644 --- a/docs/usermanuals/en/deployment/gguf-export.md +++ b/docs/usermanuals/en/deployment/gguf-export.md @@ -5,21 +5,20 @@ description: Quantise to GGUF for local inference with llama.cpp, Ollama, or LM # GGUF Export -GGUF (GPT-Generated Unified Format) is the de facto file format for local CPU/GPU inference via `llama.cpp`. ForgeLM exports any fine-tuned checkpoint to GGUF with six quantisation levels and a SHA-256 manifest. +GGUF (GPT-Generated Unified Format) is the de facto file format for local CPU/GPU inference via `llama.cpp`. ForgeLM exports a fine-tuned checkpoint to GGUF and writes a SHA-256 sidecar next to the file. `f16` and `q8_0` come out of a single command; the K-quant levels (`q2_k`, `q3_k_m`, `q4_k_m`, `q5_k_m`) need one more `llama-quantize` step, described below. ## Quick example ```shell $ forgelm export ./checkpoints/customer-support \ --output model.gguf \ - --quant q4_k_m -✓ merged LoRA into base -✓ converted to GGUF -✓ quantised to q4_k_m (4.1 GB → 4.1 GB) -✓ wrote model.gguf and model.gguf.sha256 + --quant q8_0 +Export complete: model.gguf (quant=q8_0, sha256=3f2a91c0b4d7…) ``` -The output is a single `.gguf` file plus a `.sha256` manifest. +(Log output, abridged.) The result is `model.gguf` plus `model.gguf.sha256` next to it. The sidecar is in `sha256sum` format (` *model.gguf`), so both `sha256sum -c model.gguf.sha256` and [`forgelm verify-gguf`](#/deployment/verify-gguf) accept it. When the checkpoint directory already holds a `model_integrity.json`, the digest is also recorded there under `exported_artifacts` (`--no-integrity-update` skips this). + +The sidecar detects corruption and accidental change. It sits beside the file, so it is **not** tamper evidence: whoever can replace the GGUF can replace the sidecar too. If you need that, sign the file or keep the digest where the file's writer cannot reach it. ## Quantisation levels @@ -34,51 +33,68 @@ The output is a single `.gguf` file plus a `.sha256` manifest. `q4_k_m` is the sweet spot — fits easily on consumer hardware, minimal quality loss versus full precision. +### K-quants take one more step + +The converter ForgeLM calls writes `f16` and `q8_0` directly. For a K-quant (`q2_k`, `q3_k_m`, `q4_k_m` — the default — and `q5_k_m`) `forgelm export` writes an intermediate `f16` GGUF named `.f16.gguf`, with its own `.sha256`. It reports the substitution (`requested_quant`, `manual_step_required` and the exact `followup_command` in `--output-format json`) and logs the `llama-quantize` command that finishes the job: + +```shell +$ forgelm export ./checkpoints/customer-support --output model.gguf --quant q4_k_m +Export complete: model.f16.gguf (quant=f16, sha256=9c1e47ab02d5…) +Manual quantization step required — run: llama-quantize model.f16.gguf model.gguf Q4_K_M +$ llama-quantize model.f16.gguf model.gguf Q4_K_M +$ sha256sum model.gguf > model.gguf.sha256 +``` + +`llama-quantize` comes from llama.cpp; ForgeLM does not install it. `forgelm export` writes no sidecar for the quantised file, so the last command creates one and `forgelm verify-gguf model.gguf` has a digest to compare. + GGUF export is always a separate, explicit `forgelm export` invocation — there is no config-driven auto-export toggle. `forgelm/config.py` has no `output:` block and no `gguf` field anywhere in the schema, so a training YAML cannot trigger export as a side effect of a passing eval gate; run `forgelm export` yourself once training finishes. ## Multi-quant export `forgelm export --quant` accepts a single value per invocation (one of `{q2_k, q3_k_m, q4_k_m, q5_k_m, q8_0, f16}`). To produce several -quantisations from one checkpoint, run the command once per quant: +quantisations from one checkpoint, run the command once per quant. For +the two levels the converter writes directly: ```shell -$ for q in q4_k_m q5_k_m q8_0; do +$ for q in q8_0 f16; do forgelm export ./checkpoints/run \ --output "./gguf/model.${q}.gguf" \ --quant "${q}" done -✓ wrote gguf/model.q4_k_m.gguf (4.1 GB) -✓ wrote gguf/model.q5_k_m.gguf (4.8 GB) -✓ wrote gguf/model.q8_0.gguf (7.2 GB) +Export complete: ./gguf/model.q8_0.gguf (quant=q8_0, sha256=3f2a91c0b4d7…) +Export complete: ./gguf/model.f16.gguf (quant=f16, sha256=b81d05e6c392…) ``` -The manifest: +Each run writes its own sidecar (`model.q8_0.gguf.sha256`, `model.f16.gguf.sha256`). For K-quants, run the loop with the K-quant names and finish each intermediate file with `llama-quantize`, as above. + +When the checkpoint directory holds a `model_integrity.json`, each run appends an entry to its `exported_artifacts` list: ```json { - "base_model": "Qwen/Qwen2.5-7B-Instruct", - "fine_tune_run_id": "abc123", - "quants": { - "q4_k_m": {"size_bytes": 4100000000, "sha256": "..."}, - "q5_k_m": {"size_bytes": 4800000000, "sha256": "..."}, - "q8_0": {"size_bytes": 7200000000, "sha256": "..."} - }, - "exported_at": "2026-04-29T15:00:00Z" + "exported_artifacts": [ + {"file": "model.q8_0.gguf", "format": "gguf", "quant": "q8_0", "sha256": "…", "size_bytes": 7200000000}, + {"file": "model.f16.gguf", "format": "gguf", "quant": "f16", "sha256": "…", "size_bytes": 13000000000} + ] } ``` ## Verifying GGUF integrity ```shell -$ forgelm verify-gguf model.q4_k_m.gguf -✓ valid GGUF magic -✓ vocab size matches base -✓ sha256 matches manifest -✓ tokenizer round-trip OK +$ forgelm verify-gguf model.q8_0.gguf +OK: model.q8_0.gguf + GGUF magic OK, metadata parsed, SHA-256 sidecar match + magic_ok: True + metadata_parsed: True + sidecar_present: True + sidecar_match: True + tensor_count: 291 + sha256_actual: a4c1f2… + sha256_expected: a4c1f2… ``` -This catches corruption from bad transfers, silent disk errors, and the occasional `llama.cpp` upstream incompatibility. +This checks the GGUF magic header, parses the metadata block when the optional `gguf` package is installed, and compares the `.sha256` sidecar when it exists. It does not compare the vocabulary or the tokenizer with the base model. It catches corruption from bad transfers, silent disk errors and a file edited after export; it does not show who produced the file. [Verify GGUF](#/deployment/verify-gguf) says what each layer proves. ## Loading GGUF in popular tools @@ -117,7 +133,7 @@ The result is full-precision GGUF, ~13 GB for a 7B model. ## Common pitfalls :::warn -**Exporting LoRA without merging.** `forgelm export` always merges LoRA adapters into the base before quantising. If you want adapter-only inference, you don't want GGUF — use `forgelm chat` against the adapter directly or load with PEFT. +**Forgetting `--adapter`.** `forgelm export` merges a LoRA adapter into the base model only when you pass `--adapter `; without it, the model path must already be a full model directory. If you want adapter-only inference, you don't want GGUF — use `forgelm chat` against the adapter directly or load with PEFT. ::: :::warn diff --git a/docs/usermanuals/en/deployment/verify-gguf.md b/docs/usermanuals/en/deployment/verify-gguf.md index 8fecc66a..ddbf4fc7 100644 --- a/docs/usermanuals/en/deployment/verify-gguf.md +++ b/docs/usermanuals/en/deployment/verify-gguf.md @@ -5,11 +5,11 @@ description: Three-layer integrity check for exported GGUF model files before se # Verify GGUF -`forgelm verify-gguf` is the read-only verifier paired with the GGUF export step. It runs a three-layer integrity check on a GGUF model file: the 4-byte `GGUF` magic header, the metadata block (when the optional `gguf` Python package is installed), and a SHA-256 comparison against the `.sha256` sidecar (when present). Use it as the final pre-serve gate so a corrupted or tampered GGUF never reaches `llama.cpp`, Ollama, vLLM, or LM Studio. +`forgelm verify-gguf` is the read-only verifier paired with the GGUF export step. It runs a three-layer integrity check on a GGUF model file: the 4-byte `GGUF` magic header, the metadata block (when the optional `gguf` Python package is installed), and a SHA-256 comparison against the `.sha256` sidecar (when present). Use it as the final pre-serve gate so a corrupted or accidentally changed GGUF does not reach `llama.cpp`, Ollama, vLLM, or LM Studio. The sidecar sits next to the file, so this is not tamper evidence (see the pitfalls below). ## When to use it -- **Before loading a GGUF into a serving runtime.** A clean `verify-gguf` exit is the minimum signal the file is what the exporter wrote. +- **Before loading a GGUF into a serving runtime.** A clean `verify-gguf` exit proves only what ran: with neither the `gguf` package nor a `.sha256` sidecar it proves that the first four bytes are `GGUF` and nothing else, not that the file is complete or unmodified. - **In CI/CD release gates after GGUF export.** Run after every `forgelm export` step and **fail the release on any non-zero exit code** (the public contract is the `0/1/2/3/4/5/6` set; see the in-manual [Exit Codes](#/reference/exit-codes) reference. `1` is the most common gate trip for a bad path or malformed sidecar — nothing was compared, or what was compared came out clean — while `6` means the file *is* a GGUF and a comparison genuinely disagreed; `2` covers genuine I/O failures on existing files, and other non-zero codes propagate from upstream pipelines). - **When receiving a GGUF from a third-party trainer or model hub.** Recompute the magic + metadata + SHA-256 layers to detect drift between what they sent and what they signed. - **After moving a GGUF between machines.** Any byte-level corruption in transit shows up as a SHA-256 mismatch. @@ -100,7 +100,7 @@ Pipe to `jq` to filter on the `valid` flag without parsing the human-readable te | **Metadata block** | When the optional `gguf` Python package is installed. | The reader raised mid-parse. That alone cannot tell a truncated file apart from a `gguf` package too old for this file's format revision, so the verdict is decided by what the sidecar can prove — see [the three metadata outcomes](#metadata-parse-failure-depends-on-the-sidecar) below. | `6` (no sidecar) / `1` (matching sidecar) / `6` (mismatching sidecar) | | **SHA-256 sidecar** | When `.sha256` exists alongside the file. | Either the file was modified after export (a well-formed digest that disagrees — integrity failure), OR the sidecar itself is malformed (non-hex / wrong length / `TODO` placeholder — nothing was compared). The verifier fails closed on a malformed sidecar so a corrupted manifest can never silently masquerade as "verified". | `6` (modified) / `1` (malformed sidecar) | -The exporter writes the sidecar by default. When you receive a GGUF from a third party, request the sidecar alongside it; without one, only the magic + metadata layers run. +The exporter writes the sidecar by default (for a K-quant request it belongs to the intermediate f16 file; create one for the quantised file with `sha256sum model.gguf > model.gguf.sha256`). When you receive a GGUF from a third party, request the sidecar alongside it; without one, only the magic + metadata layers run. ### Metadata parse failure depends on the sidecar @@ -116,7 +116,7 @@ The middle row is the one to internalise in CI: exit `6` means "page whoever own ### Optional dependency: the `gguf` package -The metadata parse uses the upstream `gguf` Python package. When the package is not installed, the layer is skipped silently — the magic + sidecar checks remain load-bearing: +The metadata parse uses the upstream `gguf` Python package. When the package is not installed, the layer is skipped (the text output shows `metadata_parsed: False`). The magic and the sidecar are then all that is checked, and with no sidecar either, a clean exit means only that the file starts with `GGUF`: ```shell $ pip uninstall -y gguf diff --git a/docs/usermanuals/tr/deployment/gguf-export.md b/docs/usermanuals/tr/deployment/gguf-export.md index 5832e786..789ff769 100644 --- a/docs/usermanuals/tr/deployment/gguf-export.md +++ b/docs/usermanuals/tr/deployment/gguf-export.md @@ -5,21 +5,20 @@ description: llama.cpp, Ollama veya LM Studio ile yerel inference için GGUF'a k # GGUF Export -GGUF (GPT-Generated Unified Format), `llama.cpp` üzerinden yerel CPU/GPU inference için fiili dosya formatıdır. ForgeLM herhangi bir fine-tuned checkpoint'i altı kuantizasyon seviyesi ve SHA-256 manifest ile GGUF'a export eder. +GGUF (GPT-Generated Unified Format), `llama.cpp` üzerinden yerel CPU/GPU inference için fiili dosya formatıdır. ForgeLM fine-tuned bir checkpoint'i GGUF'a export eder ve dosyanın yanına bir SHA-256 sidecar yazar. `f16` ve `q8_0` tek komutla çıkar; K-quant seviyeleri (`q2_k`, `q3_k_m`, `q4_k_m`, `q5_k_m`) aşağıda anlatılan bir `llama-quantize` adımı daha ister. ## Hızlı örnek ```shell $ forgelm export ./checkpoints/customer-support \ --output model.gguf \ - --quant q4_k_m -✓ LoRA base'e merge edildi -✓ GGUF'a dönüştürüldü -✓ q4_k_m'a kuantize edildi (4.1 GB → 4.1 GB) -✓ model.gguf ve model.gguf.sha256 yazıldı + --quant q8_0 +Export complete: model.gguf (quant=q8_0, sha256=3f2a91c0b4d7…) ``` -Çıktı tek `.gguf` dosyası artı `.sha256` manifest. +(Log çıktısı, kısaltılmış.) Sonuç `model.gguf` ve yanında `model.gguf.sha256`'dır. Sidecar `sha256sum` formatındadır (` *model.gguf`); bu yüzden hem `sha256sum -c model.gguf.sha256` hem de [`forgelm verify-gguf`](#/deployment/verify-gguf) onu kabul eder. Checkpoint dizininde zaten bir `model_integrity.json` varsa digest oraya da `exported_artifacts` altında yazılır (`--no-integrity-update` bunu atlar). + +Sidecar bozulmayı ve kazara değişikliği yakalar. Dosyanın yanında durduğu için kurcalama kanıtı **değildir**: GGUF'u değiştirebilen sidecar'ı da değiştirebilir. Buna ihtiyacınız varsa dosyayı imzalayın ya da digest'i dosyanın yazarının erişemediği bir yerde tutun. ## Kuantizasyon seviyeleri @@ -34,6 +33,20 @@ $ forgelm export ./checkpoints/customer-support \ `q4_k_m` tatlı nokta — consumer donanıma rahat sığar, full precision'a karşı minimum kalite kaybı. +### K-quant'lar bir adım daha ister + +ForgeLM'in çağırdığı dönüştürücü `f16` ve `q8_0`'ı doğrudan yazar. Bir K-quant için (`q2_k`, `q3_k_m`, `q4_k_m` — varsayılan — ve `q5_k_m`) `forgelm export`, kendi `.sha256`'sıyla birlikte `.f16.gguf` adında ara bir `f16` GGUF yazar. Değişikliği bildirir (`--output-format json`'da `requested_quant`, `manual_step_required` ve tam `followup_command`) ve işi bitiren `llama-quantize` komutunu loglar: + +```shell +$ forgelm export ./checkpoints/customer-support --output model.gguf --quant q4_k_m +Export complete: model.f16.gguf (quant=f16, sha256=9c1e47ab02d5…) +Manual quantization step required — run: llama-quantize model.f16.gguf model.gguf Q4_K_M +$ llama-quantize model.f16.gguf model.gguf Q4_K_M +$ sha256sum model.gguf > model.gguf.sha256 +``` + +`llama-quantize` llama.cpp'den gelir; ForgeLM onu kurmaz. `forgelm export` kuantize edilmiş dosya için sidecar yazmaz; son komut onu oluşturur ve `forgelm verify-gguf model.gguf` karşılaştıracak bir digest bulur. + GGUF export her zaman ayrı, açık bir `forgelm export` çağrısıdır — config-driven bir otomatik export toggle'ı yoktur. `forgelm/config.py`'de bir `output:` bloğu ve şemada hiçbir yerde `gguf` alanı yoktur, dolayısıyla bir eğitim YAML'ı eval kapısının geçmesinin yan etkisi olarak export'u tetikleyemez; eğitim bittiğinde `forgelm export`'u kendiniz çalıştırın. ## Çoklu-quant export @@ -41,45 +54,48 @@ GGUF export her zaman ayrı, açık bir `forgelm export` çağrısıdır — con `forgelm export --quant` invocation başına tek değer kabul eder (seçenekler: `{q2_k, q3_k_m, q4_k_m, q5_k_m, q8_0, f16}`). Tek bir checkpoint'ten birden çok quantisation üretmek için komutu her -quant için bir kez çalıştırın: +quant için bir kez çalıştırın. Dönüştürücünün doğrudan yazdığı iki +seviye için: ```shell -$ for q in q4_k_m q5_k_m q8_0; do +$ for q in q8_0 f16; do forgelm export ./checkpoints/run \ --output "./gguf/model.${q}.gguf" \ --quant "${q}" done -✓ gguf/model.q4_k_m.gguf yazıldı (4.1 GB) -✓ gguf/model.q5_k_m.gguf yazıldı (4.8 GB) -✓ gguf/model.q8_0.gguf yazıldı (7.2 GB) +Export complete: ./gguf/model.q8_0.gguf (quant=q8_0, sha256=3f2a91c0b4d7…) +Export complete: ./gguf/model.f16.gguf (quant=f16, sha256=b81d05e6c392…) ``` -Manifest: +Her çalıştırma kendi sidecar'ını yazar (`model.q8_0.gguf.sha256`, `model.f16.gguf.sha256`). K-quant'lar için döngüyü K-quant adlarıyla çalıştırın ve her ara dosyayı yukarıdaki gibi `llama-quantize` ile tamamlayın. + +Checkpoint dizininde bir `model_integrity.json` varsa her çalıştırma onun `exported_artifacts` listesine bir kayıt ekler: ```json { - "base_model": "Qwen/Qwen2.5-7B-Instruct", - "fine_tune_run_id": "abc123", - "quants": { - "q4_k_m": {"size_bytes": 4100000000, "sha256": "..."}, - "q5_k_m": {"size_bytes": 4800000000, "sha256": "..."}, - "q8_0": {"size_bytes": 7200000000, "sha256": "..."} - }, - "exported_at": "2026-04-29T15:00:00Z" + "exported_artifacts": [ + {"file": "model.q8_0.gguf", "format": "gguf", "quant": "q8_0", "sha256": "…", "size_bytes": 7200000000}, + {"file": "model.f16.gguf", "format": "gguf", "quant": "f16", "sha256": "…", "size_bytes": 13000000000} + ] } ``` ## GGUF bütünlük doğrulama ```shell -$ forgelm verify-gguf model.q4_k_m.gguf -✓ valid GGUF magic -✓ vocab boyutu base ile eşleşiyor -✓ sha256 manifest ile eşleşiyor -✓ tokenizer round-trip OK +$ forgelm verify-gguf model.q8_0.gguf +OK: model.q8_0.gguf + GGUF magic OK, metadata parsed, SHA-256 sidecar match + magic_ok: True + metadata_parsed: True + sidecar_present: True + sidecar_match: True + tensor_count: 291 + sha256_actual: a4c1f2… + sha256_expected: a4c1f2… ``` -Bu kötü transferden bozulmaları, sessiz disk hatalarını ve ara sıra `llama.cpp` upstream uyumsuzluklarını yakalar. +Bu komut GGUF magic header'ını denetler, isteğe bağlı `gguf` paketi kuruluysa meta veri bloğunu ayrıştırır ve varsa `.sha256` sidecar'ını karşılaştırır. Kelime dağarcığını ya da tokenizer'ı base modelle karşılaştırmaz. Kötü transferden bozulmaları, sessiz disk hatalarını ve export sonrası düzenlenmiş bir dosyayı yakalar; dosyayı kimin ürettiğini göstermez. Her katmanın neyi kanıtladığı için [Verify GGUF](#/deployment/verify-gguf) sayfasına bakın. ## Yaygın araçlarda GGUF yükleme @@ -118,7 +134,7 @@ Sonuç full-precision GGUF, 7B model için ~13 GB. ## Sık hatalar :::warn -**LoRA'yı merge etmeden export.** `forgelm export` her zaman LoRA adapter'ı kuantizasyondan önce base'e merge eder. Adapter-only inference istiyorsanız GGUF istemiyorsunuz — adapter'a karşı doğrudan `forgelm chat` kullanın veya PEFT ile yükleyin. +**`--adapter`'ı unutmak.** `forgelm export` bir LoRA adapter'ı base modele yalnızca `--adapter ` verdiğinizde merge eder; vermezseniz model yolu zaten tam bir model dizini olmalıdır. Adapter-only inference istiyorsanız GGUF istemiyorsunuz — adapter'a karşı doğrudan `forgelm chat` kullanın veya PEFT ile yükleyin. ::: :::warn diff --git a/docs/usermanuals/tr/deployment/verify-gguf.md b/docs/usermanuals/tr/deployment/verify-gguf.md index de55e2a3..37383a20 100644 --- a/docs/usermanuals/tr/deployment/verify-gguf.md +++ b/docs/usermanuals/tr/deployment/verify-gguf.md @@ -5,11 +5,11 @@ description: llama.cpp, Ollama, vLLM ya da LM Studio üzerinden servis etmeden # GGUF Doğrulama -`forgelm verify-gguf`, GGUF export adımıyla eşleşen salt-okunur doğrulayıcıdır. Bir GGUF model dosyası üzerinde üç katmanlı bütünlük kontrolü yapar: 4-baytlık `GGUF` magic header, isteğe bağlı `gguf` Python paketi yüklüyse meta veri bloğu ve mevcutsa `.sha256` sidecar'ına karşı SHA-256 karşılaştırması. Bozulmuş veya tahrif edilmiş bir GGUF'un asla `llama.cpp`, Ollama, vLLM ya da LM Studio'ya ulaşmaması için son ön-servis kapısı olarak kullanın. +`forgelm verify-gguf`, GGUF export adımıyla eşleşen salt-okunur doğrulayıcıdır. Bir GGUF model dosyası üzerinde üç katmanlı bütünlük kontrolü yapar: 4-baytlık `GGUF` magic header, isteğe bağlı `gguf` Python paketi yüklüyse meta veri bloğu ve mevcutsa `.sha256` sidecar'ına karşı SHA-256 karşılaştırması. Bozulmuş veya kazara değişmiş bir GGUF'un `llama.cpp`, Ollama, vLLM ya da LM Studio'ya ulaşmaması için son ön-servis kapısı olarak kullanın. Sidecar dosyanın yanında durduğu için bu bir kurcalama kanıtı değildir (aşağıdaki tuzaklara bakın). ## Ne zaman kullanılır -- **Bir GGUF'u servis runtime'ına yüklemeden önce.** Temiz bir `verify-gguf` çıkışı, dosyanın exporter'ın yazdığı şey olduğuna dair minimum sinyaldir. +- **Bir GGUF'u servis runtime'ına yüklemeden önce.** Temiz bir `verify-gguf` çıkışı yalnızca çalışan kontrolü kanıtlar: ne `gguf` paketi ne de `.sha256` sidecar'ı varsa ilk dört baytın `GGUF` olduğunu kanıtlar, başka hiçbir şeyi değil; dosyanın eksiksiz ya da değiştirilmemiş olduğunu değil. - **GGUF export sonrası CI/CD yayın kapılarında.** Her `forgelm export` adımından sonra çalıştırın ve **sıfırdan farklı herhangi bir çıkış kodunda yayını başarısız sayın** (kamuya açık sözleşme `0/1/2/3/4/5/6` setidir; manuel içindeki [Çıkış Kodları](#/reference/exit-codes) referansına bakın. Bozuk bir yol veya bozuk sidecar için `1` en sık kapı tripi'dir — hiçbir şey karşılaştırılmadı ya da karşılaştırılan şey temiz çıktı — `6` ise dosyanın *gerçekten* bir GGUF olduğu ve bir karşılaştırmanın gerçekten uyuşmadığı anlamına gelir; `2` mevcut dosyalardaki gerçek I/O hatalarını kapsar, üst pipeline'lardan diğer sıfırdan farklı kodlar da iletilebilir). - **Üçüncü-taraf bir trainer'dan veya model hub'ından GGUF alındığında.** Gönderilen ile imzalanan arasındaki sapmayı tespit etmek için magic + meta veri + SHA-256 katmanlarını yeniden hesaplayın. - **Bir GGUF'u makineler arasında taşıdıktan sonra.** Aktarımda oluşan herhangi bir bayt-seviyesi bozulma SHA-256 uyuşmazlığı olarak ortaya çıkar. @@ -100,7 +100,7 @@ $ forgelm verify-gguf --output-format json \ | **Meta veri bloğu** | İsteğe bağlı `gguf` Python paketi yüklüyse. | Okuyucu ayrıştırma sırasında istisna fırlattı. Bu tek başına, kesilmiş bir dosyayı bu dosyanın format revizyonunu okuyamayacak kadar eski bir `gguf` paketinden ayırt edemez; dolayısıyla kararı sidecar'ın kanıtladığı şey belirler — aşağıdaki [üç meta veri sonucuna](#meta-veri-ayrıştırma-hatası-sidecara-bağlıdır) bakın. | `6` (sidecar yok) / `1` (eşleşen sidecar) / `6` (uyuşmayan sidecar) | | **SHA-256 sidecar** | Dosyanın yanında `.sha256` mevcutsa. | Ya dosya export sonrası değiştirildi (uyuşmayan iyi-biçimli bir digest — bütünlük arızası) YA DA sidecar'ın kendisi bozuk (hex değil / yanlış uzunluk / `TODO` placeholder — hiçbir şey karşılaştırılmadı). Doğrulayıcı bozuk sidecar'da kapalı başarısız olur; böylece bozuk bir manifest asla "doğrulanmış" gibi sessizce maskelenemez. | `6` (değiştirilmiş) / `1` (bozuk sidecar) | -Exporter sidecar'ı varsayılan olarak yazar. Üçüncü taraftan bir GGUF aldığınızda yanında sidecar'ı da talep edin; sidecar olmazsa yalnızca magic + meta veri katmanları çalışır. +Exporter sidecar'ı varsayılan olarak yazar (K-quant isteğinde sidecar ara f16 dosyasına aittir; kuantize edilmiş dosya için `sha256sum model.gguf > model.gguf.sha256` ile bir tane oluşturun). Üçüncü taraftan bir GGUF aldığınızda yanında sidecar'ı da talep edin; sidecar olmazsa yalnızca magic + meta veri katmanları çalışır. ### Meta veri ayrıştırma hatası sidecar'a bağlıdır @@ -116,7 +116,7 @@ CI'da içselleştirilmesi gereken satır ortadaki: çıkış `6` "artefakt sahib ### İsteğe bağlı bağımlılık: `gguf` paketi -Meta veri ayrıştırması üst kaynak `gguf` Python paketini kullanır. Paket yüklü değilse katman sessizce atlanır — magic + sidecar kontrolleri yük taşımaya devam eder: +Meta veri ayrıştırması üst kaynak `gguf` Python paketini kullanır. Paket yüklü değilse katman atlanır (metin çıktısı `metadata_parsed: False` gösterir). O zaman denetlenen yalnızca magic ve sidecar'dır; sidecar da yoksa temiz bir çıkış yalnızca dosyanın `GGUF` ile başladığı anlamına gelir: ```shell $ pip uninstall -y gguf diff --git a/forgelm/export.py b/forgelm/export.py index 595a6c78..6e97a447 100644 --- a/forgelm/export.py +++ b/forgelm/export.py @@ -17,6 +17,7 @@ from __future__ import annotations +import contextlib import hashlib import logging import os @@ -189,6 +190,10 @@ def _merge_adapter(model_path: str, adapter_path: str, merged_dir: str) -> None: # SHA-256 helper # --------------------------------------------------------------------------- +# Suffix of the digest file written next to the GGUF. ``forgelm verify-gguf`` looks for +# ``.sha256`` (``forgelm.verify._gguf._SIDECAR_SUFFIX``); tests/test_export.py keeps the two equal. +GGUF_SIDECAR_SUFFIX = ".sha256" + def _sha256_file(path: str) -> str: """Compute the SHA-256 digest of a file.""" @@ -199,6 +204,29 @@ def _sha256_file(path: str) -> str: return h.hexdigest() +def _write_sha256_sidecar(gguf_path: str, digest: str) -> str: + """Write ``.sha256`` in ``sha256sum`` binary-mode format (`` *``). + + The file is written next to the GGUF through a temporary name and renamed into place, so a reader never + sees a half-written digest. ``sha256sum -c`` accepts the result, and so does ``forgelm verify-gguf``. + The sidecar detects corruption and accidental change; it is not tamper evidence, because whoever can + replace the GGUF can replace the sidecar next to it. Returns the sidecar path. + """ + sidecar_path = gguf_path + GGUF_SIDECAR_SUFFIX + temporary_path = sidecar_path + ".tmp" + try: + with open(temporary_path, "w", encoding="utf-8") as fh: + fh.write(f"{digest} *{os.path.basename(gguf_path)}\n") + os.replace(temporary_path, sidecar_path) + except OSError: + with contextlib.suppress( + OSError + ): # best-effort tidy-up of our own temporary file; the original error is re-raised + os.remove(temporary_path) + raise + return sidecar_path + + # --------------------------------------------------------------------------- # Compliance integration # --------------------------------------------------------------------------- @@ -241,7 +269,7 @@ def _update_integrity_manifest(model_dir: str, export_result: ExportResult) -> N # ``json.JSONDecodeError`` since it subclasses ``ValueError``). # Updating the manifest is non-fatal: the artefact itself was already # exported successfully and its on-disk SHA-256 is recoverable. - logger.debug("Could not update model_integrity.json: %s", e) + logger.warning("Could not update model_integrity.json: %s", e) # --------------------------------------------------------------------------- @@ -407,7 +435,9 @@ def export_model( converter can exceed one hour; raise this for large models. Returns: - :class:`ExportResult` with SHA-256, file size, and output path. + :class:`ExportResult` with SHA-256, file size, and output path. The digest is also written next to the + GGUF as ``.sha256`` (``sha256sum`` format), which ``forgelm verify-gguf`` compares against. For + a K-quant request the file written is the intermediate f16 GGUF and the sidecar belongs to it. """ fmt = output_format.lower() quant = quant.lower() @@ -466,6 +496,20 @@ def export_model( digest = _sha256_file(actual_output_path) size_bytes = os.path.getsize(actual_output_path) + try: + _write_sha256_sidecar(actual_output_path, digest) + except OSError as e: + return ExportResult( + success=False, + output_path=actual_output_path, + format=fmt, + quant=actual_quant, + sha256=digest, + size_bytes=size_bytes, + error=f"GGUF written to {actual_output_path} but its SHA-256 sidecar could not be written: {e}", + error_kind="runtime", + ) + # Structured substitution signal: a K-quant request is served as an # intermediate f16 GGUF and needs a manual llama-quantize follow-up. Expose # the requested quant + the exact follow-up command so JSON/CI consumers can diff --git a/forgelm/verify/_gguf.py b/forgelm/verify/_gguf.py index 04c1f2de..68bb0811 100644 --- a/forgelm/verify/_gguf.py +++ b/forgelm/verify/_gguf.py @@ -192,7 +192,8 @@ def _maybe_parse_metadata(path: str) -> Dict[str, Any]: artefacts on a minimal install legitimately do not have it. Absent ``gguf`` package = ``parsed=False``, ``error=None`` and the caller treats this as "metadata check skipped" (the magic-header - + SHA-256-sidecar checks are the load-bearing integrity surface). + and SHA-256-sidecar checks then remain; with no sidecar either, a + clean result proves only the magic header). Raising ``ImportError`` here would break the subcommand for the optional-extra-not-installed path and contradict the project standard. Genuine corruption (file present but reader crashes) diff --git a/tests/test_export.py b/tests/test_export.py index 319874bb..25071f9e 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -10,11 +10,13 @@ from unittest.mock import MagicMock, patch from forgelm.export import ( + GGUF_SIDECAR_SUFFIX, SUPPORTED_FORMATS, SUPPORTED_QUANTS, ExportResult, _sha256_file, _update_integrity_manifest, + _write_sha256_sidecar, export_model, ) @@ -69,6 +71,110 @@ def test_large_file_consistent(self, tmp_path): # --------------------------------------------------------------------------- +class TestSha256Sidecar: + """`forgelm export` writes `.sha256`, the file `forgelm verify-gguf` looks for (#84).""" + + @staticmethod + def _gguf(tmp_path, name="model.gguf", body=b"GGUF" + b"\x00" * 64): + path = tmp_path / name + path.write_bytes(body) + return str(path) + + def test_suffix_matches_what_verify_gguf_looks_for(self): + from forgelm.verify._gguf import _SIDECAR_SUFFIX + + assert GGUF_SIDECAR_SUFFIX == _SIDECAR_SUFFIX + + def test_sidecar_is_in_sha256sum_binary_format(self, tmp_path): + gguf = self._gguf(tmp_path) + digest = _sha256_file(gguf) + sidecar = _write_sha256_sidecar(gguf, digest) + assert sidecar == gguf + ".sha256" + assert open(sidecar, encoding="utf-8").read() == f"{digest} *model.gguf\n" + + # The fake file is not a parseable GGUF, so the metadata layer is switched off: these tests are about the sidecar, + # and must give the same answer whether or not the optional `gguf` package is installed. + _NO_METADATA = {"parsed": False, "error": None, "tensor_count": None} + + def test_verify_gguf_accepts_what_the_exporter_wrote(self, tmp_path): + from forgelm.verify._gguf import verify_gguf + + gguf = self._gguf(tmp_path) + _write_sha256_sidecar(gguf, _sha256_file(gguf)) + with patch("forgelm.verify._gguf._maybe_parse_metadata", return_value=self._NO_METADATA): + result = verify_gguf(gguf) + assert result.valid is True + assert result.checks["sidecar_present"] is True and result.checks["sidecar_match"] is True + + def test_verify_gguf_catches_a_change_after_export(self, tmp_path): + from forgelm.verify._gguf import verify_gguf + + gguf = self._gguf(tmp_path) + _write_sha256_sidecar(gguf, _sha256_file(gguf)) + with open(gguf, "ab") as fh: + fh.write(b"tampered") + with patch("forgelm.verify._gguf._maybe_parse_metadata", return_value=self._NO_METADATA): + result = verify_gguf(gguf) + assert result.valid is False and result.checks["sidecar_match"] is False + + def test_a_failed_write_leaves_no_temporary_file(self, tmp_path): + gguf = self._gguf(tmp_path) + with patch("forgelm.export.os.replace", side_effect=OSError("disk full")): + try: + _write_sha256_sidecar(gguf, "0" * 64) + except OSError: + pass + else: # pragma: no cover - the patched replace must raise + raise AssertionError("expected OSError") + assert not os.path.exists(gguf + ".sha256") and not os.path.exists(gguf + ".sha256.tmp") + + +class TestExportWritesTheSidecar: + def _run_export(self, tmp_path, quant, content=b"GGUF-mock-bytes"): + output_path = str(tmp_path / "model.gguf") + + def fake_run(cmd, **kwargs): + actual = cmd[cmd.index("--outfile") + 1] if "--outfile" in cmd else output_path + with open(actual, "wb") as f: + f.write(content) + result = MagicMock() + result.returncode, result.stderr, result.stdout = 0, "", "" + return result + + stub = MagicMock() + os.makedirs(str(tmp_path / "llama_cpp"), exist_ok=True) + stub.__file__ = str(tmp_path / "llama_cpp" / "__init__.py") + open(str(tmp_path / "llama_cpp" / "convert_hf_to_gguf.py"), "w").close() + with patch.dict(sys.modules, {"llama_cpp": stub}), patch("subprocess.run", side_effect=fake_run): + return output_path, export_model(str(tmp_path / "model"), output_path, quant=quant, update_integrity=False) + + def test_sidecar_sits_next_to_the_file_with_the_reported_digest(self, tmp_path): + output_path, result = self._run_export(tmp_path, "q8_0") + assert result.success is True and result.output_path == output_path + text = open(output_path + ".sha256", encoding="utf-8").read() + assert text == f"{result.sha256} *model.gguf\n" + + def test_k_quant_sidecar_belongs_to_the_intermediate_f16_file(self, tmp_path): + output_path, result = self._run_export(tmp_path, "q4_k_m") + assert result.success is True and result.output_path.endswith(".f16.gguf") + assert os.path.isfile(result.output_path + ".sha256") + assert not os.path.exists(output_path + ".sha256") # the requested q4_k_m file does not exist yet + + def test_a_sidecar_that_cannot_be_written_fails_the_export_loudly(self, tmp_path): + with patch("forgelm.export._write_sha256_sidecar", side_effect=OSError("read-only file system")): + _, result = self._run_export(tmp_path, "q8_0") + assert result.success is False and result.error_kind == "runtime" + assert "SHA-256 sidecar could not be written" in result.error and "read-only" in result.error + assert result.sha256 is not None # the file exists and its digest is still reported + + def test_a_manifest_that_cannot_be_updated_is_logged_as_a_warning(self, tmp_path, caplog): + (tmp_path / "model_integrity.json").write_text("{not json") + result = ExportResult(success=True, output_path=str(tmp_path / "m.gguf"), sha256="abc") + with caplog.at_level(logging.WARNING, logger="forgelm.export"): + _update_integrity_manifest(str(tmp_path), result) + assert any("Could not update model_integrity.json" in r.message for r in caplog.records) + + class TestUpdateIntegrityManifest: def test_updates_existing_manifest(self, tmp_path): integrity_path = tmp_path / "model_integrity.json" From 5f0ae2065bcff5da000544e9665bcdc8c0143923 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Sun, 18 Oct 2026 10:59:53 +0300 Subject: [PATCH 2/2] docs(gguf): say how the intermediate file is named and that the manifest entry is best effort Review findings on the export page: the K-quant intermediate is the output name with its .gguf suffix replaced by .f16.gguf (model.gguf becomes model.f16.gguf), not .f16.gguf appended; and the exported_artifacts entry is best effort, because _update_integrity_manifest logs a warning and the export succeeds when model_integrity.json is not valid JSON or cannot be written. English and Turkish. Refs #84 Co-Authored-By: Claude Sonnet 5.5 --- docs/usermanuals/en/deployment/gguf-export.md | 6 +++--- docs/usermanuals/tr/deployment/gguf-export.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/usermanuals/en/deployment/gguf-export.md b/docs/usermanuals/en/deployment/gguf-export.md index 2d898d7b..a18f8c2f 100644 --- a/docs/usermanuals/en/deployment/gguf-export.md +++ b/docs/usermanuals/en/deployment/gguf-export.md @@ -16,7 +16,7 @@ $ forgelm export ./checkpoints/customer-support \ Export complete: model.gguf (quant=q8_0, sha256=3f2a91c0b4d7…) ``` -(Log output, abridged.) The result is `model.gguf` plus `model.gguf.sha256` next to it. The sidecar is in `sha256sum` format (` *model.gguf`), so both `sha256sum -c model.gguf.sha256` and [`forgelm verify-gguf`](#/deployment/verify-gguf) accept it. When the checkpoint directory already holds a `model_integrity.json`, the digest is also recorded there under `exported_artifacts` (`--no-integrity-update` skips this). +(Log output, abridged.) The result is `model.gguf` plus `model.gguf.sha256` next to it. The sidecar is in `sha256sum` format (` *model.gguf`), so both `sha256sum -c model.gguf.sha256` and [`forgelm verify-gguf`](#/deployment/verify-gguf) accept it. When the checkpoint directory already holds a `model_integrity.json`, ForgeLM also tries to record the digest there under `exported_artifacts` (best effort: if the file is not valid JSON or cannot be written, a warning is logged and the export still succeeds; `--no-integrity-update` skips this). The sidecar detects corruption and accidental change. It sits beside the file, so it is **not** tamper evidence: whoever can replace the GGUF can replace the sidecar too. If you need that, sign the file or keep the digest where the file's writer cannot reach it. @@ -35,7 +35,7 @@ The sidecar detects corruption and accidental change. It sits beside the file, s ### K-quants take one more step -The converter ForgeLM calls writes `f16` and `q8_0` directly. For a K-quant (`q2_k`, `q3_k_m`, `q4_k_m` — the default — and `q5_k_m`) `forgelm export` writes an intermediate `f16` GGUF named `.f16.gguf`, with its own `.sha256`. It reports the substitution (`requested_quant`, `manual_step_required` and the exact `followup_command` in `--output-format json`) and logs the `llama-quantize` command that finishes the job: +The converter ForgeLM calls writes `f16` and `q8_0` directly. For a K-quant (`q2_k`, `q3_k_m`, `q4_k_m` — the default — and `q5_k_m`) `forgelm export` writes an intermediate `f16` GGUF, named like the output with its `.gguf` suffix replaced by `.f16.gguf` (`model.gguf` becomes `model.f16.gguf`; an output without the suffix gets `.f16.gguf` appended), with its own `.sha256`. It reports the substitution (`requested_quant`, `manual_step_required` and the exact `followup_command` in `--output-format json`) and logs the `llama-quantize` command that finishes the job: ```shell $ forgelm export ./checkpoints/customer-support --output model.gguf --quant q4_k_m @@ -68,7 +68,7 @@ Export complete: ./gguf/model.f16.gguf (quant=f16, sha256=b81d05e6c392…) Each run writes its own sidecar (`model.q8_0.gguf.sha256`, `model.f16.gguf.sha256`). For K-quants, run the loop with the K-quant names and finish each intermediate file with `llama-quantize`, as above. -When the checkpoint directory holds a `model_integrity.json`, each run appends an entry to its `exported_artifacts` list: +When the checkpoint directory holds a `model_integrity.json`, each run tries to append an entry to its `exported_artifacts` list (best effort, as above): ```json { diff --git a/docs/usermanuals/tr/deployment/gguf-export.md b/docs/usermanuals/tr/deployment/gguf-export.md index 789ff769..24c77d40 100644 --- a/docs/usermanuals/tr/deployment/gguf-export.md +++ b/docs/usermanuals/tr/deployment/gguf-export.md @@ -16,7 +16,7 @@ $ forgelm export ./checkpoints/customer-support \ Export complete: model.gguf (quant=q8_0, sha256=3f2a91c0b4d7…) ``` -(Log çıktısı, kısaltılmış.) Sonuç `model.gguf` ve yanında `model.gguf.sha256`'dır. Sidecar `sha256sum` formatındadır (` *model.gguf`); bu yüzden hem `sha256sum -c model.gguf.sha256` hem de [`forgelm verify-gguf`](#/deployment/verify-gguf) onu kabul eder. Checkpoint dizininde zaten bir `model_integrity.json` varsa digest oraya da `exported_artifacts` altında yazılır (`--no-integrity-update` bunu atlar). +(Log çıktısı, kısaltılmış.) Sonuç `model.gguf` ve yanında `model.gguf.sha256`'dır. Sidecar `sha256sum` formatındadır (` *model.gguf`); bu yüzden hem `sha256sum -c model.gguf.sha256` hem de [`forgelm verify-gguf`](#/deployment/verify-gguf) onu kabul eder. Checkpoint dizininde zaten bir `model_integrity.json` varsa ForgeLM digest'i oraya da `exported_artifacts` altında yazmayı dener (best-effort: dosya geçerli JSON değilse ya da yazılamıyorsa bir uyarı loglanır ve export yine de başarılı olur; `--no-integrity-update` bunu atlar). Sidecar bozulmayı ve kazara değişikliği yakalar. Dosyanın yanında durduğu için kurcalama kanıtı **değildir**: GGUF'u değiştirebilen sidecar'ı da değiştirebilir. Buna ihtiyacınız varsa dosyayı imzalayın ya da digest'i dosyanın yazarının erişemediği bir yerde tutun. @@ -35,7 +35,7 @@ Sidecar bozulmayı ve kazara değişikliği yakalar. Dosyanın yanında durduğu ### K-quant'lar bir adım daha ister -ForgeLM'in çağırdığı dönüştürücü `f16` ve `q8_0`'ı doğrudan yazar. Bir K-quant için (`q2_k`, `q3_k_m`, `q4_k_m` — varsayılan — ve `q5_k_m`) `forgelm export`, kendi `.sha256`'sıyla birlikte `.f16.gguf` adında ara bir `f16` GGUF yazar. Değişikliği bildirir (`--output-format json`'da `requested_quant`, `manual_step_required` ve tam `followup_command`) ve işi bitiren `llama-quantize` komutunu loglar: +ForgeLM'in çağırdığı dönüştürücü `f16` ve `q8_0`'ı doğrudan yazar. Bir K-quant için (`q2_k`, `q3_k_m`, `q4_k_m` — varsayılan — ve `q5_k_m`) `forgelm export`, kendi `.sha256`'sıyla birlikte, çıktı adındaki `.gguf` uzantısı `.f16.gguf` ile değiştirilerek adlandırılan (`model.gguf` → `model.f16.gguf`; bu uzantıyla bitmeyen bir çıktıya `.f16.gguf` eklenir) ara bir `f16` GGUF yazar. Değişikliği bildirir (`--output-format json`'da `requested_quant`, `manual_step_required` ve tam `followup_command`) ve işi bitiren `llama-quantize` komutunu loglar: ```shell $ forgelm export ./checkpoints/customer-support --output model.gguf --quant q4_k_m @@ -69,7 +69,7 @@ Export complete: ./gguf/model.f16.gguf (quant=f16, sha256=b81d05e6c392…) Her çalıştırma kendi sidecar'ını yazar (`model.q8_0.gguf.sha256`, `model.f16.gguf.sha256`). K-quant'lar için döngüyü K-quant adlarıyla çalıştırın ve her ara dosyayı yukarıdaki gibi `llama-quantize` ile tamamlayın. -Checkpoint dizininde bir `model_integrity.json` varsa her çalıştırma onun `exported_artifacts` listesine bir kayıt ekler: +Checkpoint dizininde bir `model_integrity.json` varsa her çalıştırma onun `exported_artifacts` listesine (yukarıdaki gibi best-effort) bir kayıt eklemeyi dener: ```json {