Skip to content

feat: stream model conversion - #1581

Merged
leejet merged 9 commits into
leejet:masterfrom
shikaku2:feat/streaming-convert
Jul 5, 2026
Merged

leejet merged 9 commits into
leejet:masterfrom
shikaku2:feat/streaming-convert

Conversation

@shikaku2

Copy link
Copy Markdown
Contributor

Split out from draft PR #1573: #1573

Summary

Changes --convert to stream converted tensors instead of allocating the entire converted model in one ggml_context before writing the output file.

This PR intentionally only covers the regular conversion memory/threading path. RMSE-guided conversion is not included here and will be handled separately after this is reviewed.

What changed

  • Collect output tensor metadata first without loading tensor data.
  • Write GGUF or safetensors metadata/header up front.
  • Load, convert, and write tensors in batches instead of keeping every converted tensor resident until the end.
  • Parallelize tensor loading/conversion within each batch.
  • Cap each batch by output tensor bytes, so large tensors still stream with bounded peak memory while smaller tensors can use available CPU threads.
  • Reuse the existing convert(input_path, vae_path, output_path, output_type, tensor_type_rules, convert_name) API and CLI behavior.

What is not included

  • No RMSE option or RMSE type selection.
  • No AIO/separate text encoder/diffusion/VAE packaging changes.
  • No --lazy-load runtime behavior changes.

Validation

  • cmake --build build -j16
  • git diff --check
  • Tiny safetensors -> GGUF conversion: build/bin/sd-cli -M convert -m /tmp/sdcpp-convert-tiny.safetensors -o /tmp/sdcpp-convert-tiny-final.gguf --type f16
  • Full SD3.5 Medium conversion: time build/bin/sd-cli -M convert -m /home/aaron/models/sd3.5-medium/sd3.5_medium.safetensors -o /tmp/sd3.5_medium_streaming_convert.gguf
    • Output: /tmp/sd3.5_medium_streaming_convert.gguf, 4.8G
    • Completed successfully in about 3.7s wall time on my machine

Notes

This is a draft because the new streaming writer path should get review and broader testing across output formats and platforms before being marked ready.

@wbruna wbruna left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First, about the coding style: this is placing format-specific logic inside convert.cpp. The format-specific code should go to the appropriate files inside model_io/, likely with a separate "write tensor" per file type. Note you should also avoid opening and closing the model files for each tensor, so some kind of "opened model file" abstraction will probably be needed. A "read the tensor at the specified offset" abstraction would probably make sense, too.

I gave this a try for a .safetensors -> Q4_K .gguf. On my machine, it was never able to saturate all CPU cores, so it got much slower than the normal conversion (around 1/2 - 1/3 speed). I/O didn't seem to be the bottleneck: system and wait times remained low.

Looking at the code, my guess would be the batching calculation: it would explain this behavior if for some reason it consistently used only 1 or 2 threads (the number of threads should also respect the --threads parameter by the way). The batching division also looks sub-optimal: you split up work between threads, then stop everything, write everything, then open threads again. So you are not allowing an overlap between the conversion and the writing; plus, a thread could finish much sooner than the others, and would stay idle until the next batch.

I would avoid the fixed batching, and use a true pipeline instead: either n read+convert threads + 1 write thread, or n read+convert+write threads, controlling for the memory budget with a condition variable. I would bet on the second option: if writing is the bottleneck, you'd naturally parallelize it as well.

Note you are not forced to write sequentially, either: you have offsets for each tensor, so they could be written as soon as they are ready, with each thread using its own open file object (I'd recommend preallocating the file at the beginning, to give the filesystem a better chance to avoid fragmentation issues). An out-of-order approach could also help with models with huge tensors, since you can try to overlap them with smaller ones.

@shikaku2

shikaku2 commented May 31, 2026 •

Copy link
Copy Markdown
Contributor Author

Added a follow-up commit (504d5f8) for the review feedback:

  • moved streaming output format logic into model_io/ via GGUF/safetensors streaming writer classes
  • replaced fixed batches with a memory-budgeted worker pipeline
  • added per-worker open output handles and output preallocation
  • wired convert mode to respect --threads while preserving the existing convert() API

I also benchmarked against a fresh master clone at be65ac7, both built RelWithDebInfo with Vulkan enabled, converting safetensors to Q4_K GGUF with --threads 16.

Model Build Wall CPU Max RSS Internal timing
SD3.5 Medium master be65ac7 13.08s 1405% 3.39 GiB load/convert 12.40s
SD3.5 Medium streaming 504d5f8 12.81s 1437% 2.22 GiB streaming convert 12.60s
SD3.5 Large master be65ac7 20.17s 909% 14.66 GiB load/convert 12.88s
SD3.5 Large streaming 504d5f8 13.10s 1345% 2.63 GiB streaming convert 12.88s

So the revised pipeline is roughly neutral on SD3.5 Medium and about 35% faster wall-clock on SD3.5 Large in this environment, with substantially lower peak RSS in both cases.

@wbruna

wbruna commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

I gave 504d5f8 a try. Looks like it's performing much better now.

The job division still have a few code smells, though. First, you are using separate I/O backends for reading (ifstream) and writing (FILE). If there is a reason to use FILE for writing, it needs to be very clear in comments; otherwise, just use an ofstream. Also, the whole "keep a table of n writers" logic is duplicated between safetensors and gguf.

A better approach could be splitting the writing logic from the model format. Say, an interface similar to:

  • write_metadata(empty output file, tensor metadata)
  • write_tensor(output file, tensor, offset)

then you'd have:

  • gguf_write_metadata(empty output file, tensor metadata)
  • gguf_write_tensor(output file, tensor, offset)
  • safetensors_write_metadata(empty output file, tensor metadata)
  • safetensors_write_tensor(output file, tensor, offset)

(note this could either be a small class hierarchy, or two sets of callbacks using the same signatures. I believe the hierarchy approach would be cleaner; it'd also avoid the need for that function template)

Then you can decouple the whole multithread-writing logic from the output formats:

  • first, get either a base class pointer or appropriate callback for the requested format
  • open a single output stream, and use it to call the appropriate 'write_metadata' to write headers, preallocate the file, etc (and before I forget: you'll eventually want something like posix_fallocate to do the preallocation, but that can wait until the basic multi-platform logic is working)
  • open additional n-1 output streams, so each writing thread calls 'write_tensor' on its own file (and so you avoid the need for that file table)

By the way: with this change, at least the output gguf file isn't identical to the one generated on master. Although it doesn't need to be, it'd be a good safety check, to be sure the code is working as intended.

@shikaku2

Copy link
Copy Markdown
Contributor Author

Updated the draft with commit 5da10f4 to address the writer-division feedback.

Changes:

  • added a small StreamingModelWriter interface with write_metadata, write_tensor, and file_size
  • moved GGUF/safetensors metadata and tensor-offset logic behind that interface
  • removed the duplicated per-format table of n writer handles
  • changed the shared streaming pipeline to open one std::fstream per worker and call the selected writer through the common interface
  • switched safetensors streaming writes fully to C++ streams
  • kept FILE* only inside GGUF metadata writing, with a comment explaining that gguf_write_to_file_ptr currently exposes the GGUF metadata write through FILE*; tensor data writes now use std::fstream
  • moved output sizing/preallocation into the shared streaming path with a portable seek/write fallback for now

Validation:

  • cmake --build build -j16 passes
  • git diff --check passes
  • regenerated SD3.5 Medium safetensors -> Q4_K GGUF with the streaming path using --threads 16
  • byte-for-byte cmp against the master-generated GGUF still differs because GGUF tensor metadata ordering differs
  • semantic GGUF comparison using gguf 0.19.0 passes: same 3 metadata fields, same 909 tensors, no missing/extra tensors, no tensor metadata differences, and no tensor data hash differences by tensor name

So the output is not byte-identical, but the tensor payloads and metadata compare equal when keyed by tensor name.

@leejet
leejet marked this pull request as ready for review July 5, 2026 09:20
@leejet
leejet merged commit da6db07 into leejet:master Jul 5, 2026
11 checks passed
@leejet

leejet commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Thank you for your contribution.

fire added a commit to V-Sekai-fire/stable-diffusion-ggml that referenced this pull request Sep 15, 2026
04b55bba Merge pull request leejet#8 from V-Sekai-fire/sync/upstream-refresh
667804ef Sync ggml/ to upstream refresh.
52c7f4a8 Merge pull request leejet#7 from V-Sekai-fire/repoint-urls-at-v-sekai-fire
4eebc4a9 Point URLs at V-Sekai-fire
d4d15152 Merge pull request leejet#6 from v-sekai-fabric/magi/citation
47fe255e Add CITATION.cff for the canonical ggml tree
d646939a Merge pull request leejet#5 from v-sekai-fabric/gate-volk-behind-ifdef
bcccfc16 ggml-vulkan: gate volk include behind #ifdef GGML_VULKAN_VOLK
06d5dab5 Merge pull request #4 from v-sekai-fabric/drop-empty-gitmodules
1302b98d Drop empty .gitmodules (CLAUDE.md submodule blocklist doctrine)
eee2d470 Merge pull request #3 from v-sekai-fabric/revert-broken-dl-port
0d9079c4 Revert "Port turboquant-godot: guard ggml-backend-dl.h behind GGML_BACKEND_DL"
273a4fcf Merge pull request #2 from v-sekai-fabric/turboquant-godot-port
32567060 Port turboquant-godot: route ggml-vulkan through volk loader
e3e3ec26 Port turboquant-godot: guard ggml-backend-dl.h behind GGML_BACKEND_DL
89bf35b3 Merge pull request #1 from weftspun/sam3-flash-attn-dk16-dk56
0827e29a metal : flash_attn_ext head_dim=16 and head_dim=56
3404c951 vulkan : local perf-log naming for ADD, GROUP_NORM and CONT
c21b1a10 metal : implement diag_mask_inf
REVERT: e20c3a1 ggml-cuda : add native FP8 matmul with cuBLASLt
REVERT: 032b699 ggml : support FP8 casts across compute backends
REVERT: 8e800ce ggml : remove standalone regular Hadamard op
REVERT: 373c7f1 ggml-vulkan : add native INT8 convrot support
REVERT: 4b053d2 ggml : fix HIP build for INT8 convrot
REVERT: 86803f3 ggml : add native INT8 convrot support
REVERT: 8846b79 cmake : add config version support (leejet#1582)
REVERT: 30bf868 ggml : bump version to 0.19.0 (leejet#1581)
REVERT: 77be358 sync : llama.cpp
REVERT: 0f8a392 ggml : add aarch64 HWCAP fallbacks and fix fp16 variant detection (llama/25554)
REVERT: 8bb3846 sycl: fix UE4M3 parsing (llama/25608)
REVERT: f6515fc sycl: *glu flat path (llama/26354)
REVERT: ff7002b sycl : Support DSv4 OPs: LIGHTNING_INDEXER,DSV4_HC_COMB,DSV4_HC_POST,DSV4_HC_PRE (llama/26568)
REVERT: e271907 sycl : fix error Error OP FLASH_ATTN_EXT on arc770 (llama/26441)
REVERT: 3b4aa5b sycl : enhance OP set_rows to support all missed data types (llama/26515)
REVERT: c0011fe cuda: fix warnings for unused variable/function (llama/26688)
REVERT: 02e0bca metal : avoid `threadgroup` matrix array instantiation in kernel_lightning_indexer (llama/26646)
REVERT: c035d79 ci : onboard AMD ROCm CI with gfx1151 fixes (llama/26544)
REVERT: 0b0a78b vulkan: fix submission batching size, add debug tools for diagnosing causes of DeviceLost drivers errors (llama/26371)
REVERT: f10de3a mtmd/ggml: add ggml_build_forward_order (llama/26649)
REVERT: d0e4951 vulkan backend ops: implemented GATED_LINEAR_ATTN (llama/25601)
REVERT: 90951f9 ggml : bump version to 0.18.1 (leejet#1578)
REVERT: 46c86bd sync : llama.cpp
REVERT: e75fcb4 sycl: parallelize the non-contiguous concat kernel (llama/25852)
REVERT: dbe27be Extended SYCL oneDNN SDPA to non-FP16 KV caches (Q4_0–Q8_0 and FP32) (#25874)
REVERT: e8e7ea4 ggml: use dynamic allocation for split graph inputs (llama/22789)
REVERT: f80f881 opencl: route large q6_K lm_head to the flat GEMV (llama/26427)
REVERT: 3e12f44 CUDA: Fix data-races when reusing SMEM in block_reduce (llama/26385)
REVERT: 6521490 metal: implement DSv4 Lightning Indexer (llama/25893)
REVERT: 5899365 metal : add SILU_BACK (llama/25982)
REVERT: 6be1ee1 metal : add F16 support for bin ops (llama/26465)
REVERT: 09ebda8 opencl: limit local workgroup size for GLU operation (llama/26383)
REVERT: 91778e3 metal: implement DeepSeek V4 hyper-connections (llama/26459)
REVERT: af8c565 opencl: bugfix increment ref_count in ggml_backend_opencl_init() (llama/26162)
REVERT: 12fbc7a sycl: fix classification of iGPUs (llama/26105)
REVERT: b5c81eb ggml-webgpu: add support for f16 repeat (llama/26307)
REVERT: a8f4315 vulkan: extend topk_moe fusion to support sqrt(softplus) (llama/26124)
REVERT: c7e4593 vulkan: add POOL_1D op (llama/25431)
REVERT: f3022a1 vulkan: Introduce driver version check for Windows Intel GPU to mitigate crashing (llama/25192)
REVERT: 369bf0a cuda: extract Q2_0 elements via __byte_perm (llama/25603)
REVERT: ec1eb4f SYCL: add oneMKL GEMM flash attention for XMX-accelerated prompt proc… (#25025)
REVERT: 0635a0f support the missed types in cpy (llama/26005)
REVERT: e2a5c53 ggml-zendnn : group matmul direct API for mul_mat_id (llama/25918)
REVERT: 97d7638 sycl : support dev2dev memcpy by DEV2DEV_MEMCPY_FORWARD (llama/26234)
REVERT: 776124b Support q2 mul_mat (llama/26231)
REVERT: 2413b70 sycl: fuse RMS_NORM + MUL (llama/26015)
REVERT: 39f1fd3 ggml-webgpu: improve flash_attn_vec for quantized KV at long contexts (llama/25956)
REVERT: f04850c vulkan: Support quantized concat (llama/25684)
REVERT: 3d68e2b Test support for alternative conv layout (llama/25617)
REVERT: 49ed848 ggml-cuda: Allow transpose-free gemmv computation (llama/26171)
REVERT: 78de606 sync : whisper.cpp
REVERT: 06ca976 ggml : bump version to 0.18.0 (leejet#1576)
REVERT: dfeb865 typo: init_model does not create tensors (leejet#1572)
REVERT: 4429529 sync : llama.cpp
REVERT: b5cd818 CUDA: add Q2_0 support (llama/25707)
REVERT: 560511d metal: fix memory unwire if model is freed without any GPU operations (llama/26082)
REVERT: 99ac6e0 ggml : Fix issue with kleidiai ci and stringop overflow warning (llama/26277)
REVERT: 111adea enhance UT to show all real unsupported backends (llama/25234)
REVERT: dbc6bc2 ggml-cuda : disable MMQ on devices with less than 48 KiB shared memory (llama/26141)
REVERT: 44fd996 sycl: contiguous fast path + 32-bit index math for unary elementwise ops (llama/25946)
REVERT: 0e1747a RPC: add tensor_memset (llama/25912)
REVERT: dd628ca add rdna3.5, and 3 to mmq configs so they can be tuned independently. (llama/26199)
REVERT: be64c58 ggml-webgpu: Fix some binding alias issues to support all archs, fix recurrent-state-rollback test (llama/25931)
REVERT: 98d5e77 opencl: skip the Adreno KQ/KQV image kernels for multi-stream batches (llama/26189)
REVERT: d727fbb ggml : set output of view src (llama/25729)
REVERT: 4a17c1b vulkan: add iq4_nl support back to FA (llama/24585)
REVERT: 260279d ggml-cuda: add chunked SSD matmul for Mamba-2 prefill acceleration (llama/22675)
REVERT: 23b34d5 sycl: fix use-after-return of the SDPA scale in the oneDNN flash-attention path (llama/25880)
REVERT: 5265d8f ggml-metal: FWHT kernel for metal backend (llama/25924)
REVERT: 3fc55d4 Disable -ffast-math on HIP (llama/25495)
REVERT: 0929483 sycl(build): parallelize ocloc invocations (llama/25903)
REVERT: 27bbeaf ggml : adjust logic for offloading ops to weight's backend (llama/25832)
REVERT: bc40151 ggml-cpu: Enable BF16 tiled gemm optimization on PowerPC (llama/26068)
REVERT: 1f64a93 opencl: fix fused RMS norm mul view offset (llama/26085)
REVERT: 99a582e hexagon: partial im2col support (llama/26007)
REVERT: 3ca7e8c Update ggml/src/gguf.cpp : Defined virtual keyword for destructor of gguf_writer_base (llama/25867)
REVERT: 0b28b84 HIP: remove rocWMMA FlashAttention (llama/26046)
REVERT: 4c0b27d opencl: cache compiled cl_program binaries on disk (llama/26050)
REVERT: 6a92a50 opencl: do not treat NULL-mask flash attention as causal (llama/25771)
REVERT: c2a30bb hexagon: further improved pipeline of the core bits (L2, DMA, MM, FA) (llama/26049)
REVERT: 5c4119e hexagon: fix Windows crash when op_poll is enabled (llama/26029)
REVERT: 2fbff1e CUDA: fix external compilation of q1_0 MMQ (llama/25778)
REVERT: 17a2a7c metal : add f16 type support to leaky relu (llama/25981)
REVERT: e7059df CUDA: Improve NVFP4 W4A4 activation quantization (llama/25730)
REVERT: 874aa98 hexagon: activation ops update (llama/25974)
REVERT: 596deb3 ggml: enable PowerPC backend variants on AIX (llama/25983)
REVERT: a72a524 webgpu : add CONV_2D_DW (depthwise conv2d) kernel (llama/25847)
REVERT: 08130cf cuda: GET_ROWS quants (llama/25962)
REVERT: e4330b9 hexagon: check tensor type when reusing descriptors (llama/25968)
REVERT: 37a89db cuda: add sqrt_softplus in topk-moe for dsv4 (llama/25896)
REVERT: c4fa8af kleidiai : warn once when a weight type has no KleidiAI kernel (llama/25701)
REVERT: 3ca9985 vulkan: Refactor vk_queue to use per-instance mutexes and unique handles (llama/23570)
REVERT: b186c92 ggml-openvino: Add GGML_BACKEND_DL_IMPL invocation for OpenVINO backend (llama/25795)
REVERT: 924cd99 CUDA: vectorize same-type get_rows with int4 copy (llama/25929)
REVERT: 09e1fe8 hexagon: add CLAMP op (llama/25934)
REVERT: 0fd2904 opencl: Support broadcast for Adreno MUL_MAT and honor `view_offs` for Adreno Q8_0 MUL_MAT for llama-server multi-stream (llama/25910)
REVERT: a0c4f2a opencl: load and use `kernel_gemm_moe_q6_k_f32_ns` from bin kernel lib (llama/25797)
REVERT: 454ea6f opencl: read/write MoE dp4a activation tiles to local memory as 128-bit (vectorized LD/ST perf opt) for Adreno GPUs (llama/25810)
REVERT: f68fee5 opencl: transpose q4_K noshuffle scales for coalesced reads (llama/25805)
REVERT: 65a9776 tests : initialize all tensors in test_dsv4_hc to avoid NaNs in sentinel tensors (llama/25822)

git-subtree-dir: ggml
git-subtree-split: 04b55bba4877e0d269e2db68911608485c08eb3a
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants