From af84f44ac77a911e6089ba26b202b0ab3ffa8a20 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 07:24:12 -0400 Subject: [PATCH 001/123] feat(ds4v): MIX converter for Vision-Exp safetensors plus codec test CPU-only tool turning abliterated Vision-Exp safetensors into ROCmFPX MIX GGUF. Down experts to qtype 105 with embedded P4MIXv1 codebooks. Gate and up experts to qtype 106 with a split GUMIXs1 sidecar. Vision, aligner, image and bias_vl tensors pass through losslessly. Calibration needs an imatrix file or explicit absmax-only. Includes block codec roundtrip plus layout plus rejection unit test. --- server/CMakeLists.txt | 26 + server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.c | 245 +++ server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.h | 9 + server/test/test_ds4v_mix_converter.cpp | 181 +++ server/tools/ds4_mix_converter/CMakeLists.txt | 42 + .../ds4_mix_converter/ds4_mix_converter.cpp | 1362 +++++++++++++++++ 6 files changed, 1865 insertions(+) create mode 100644 server/test/test_ds4v_mix_converter.cpp create mode 100644 server/tools/ds4_mix_converter/CMakeLists.txt create mode 100644 server/tools/ds4_mix_converter/ds4_mix_converter.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 0b237c94a..fea386e5d 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -377,6 +377,22 @@ if(DFLASH27B_MIXED_GGML_SHARED) set(BUILD_SHARED_LIBS ON) endif() add_subdirectory(deps/llama.cpp/ggml EXCLUDE_FROM_ALL) + +option(DFLASH27B_DS4_MIX_CONVERTER + "Build the CPU-only DeepSeek-V4 safetensors to MIX GGUF converter" ON) +if(DFLASH27B_DS4_MIX_CONVERTER) + add_executable(ds4_mix_converter + tools/ds4_mix_converter/ds4_mix_converter.cpp) + target_include_directories(ds4_mix_converter PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/rocmfpx) + target_link_libraries(ds4_mix_converter PRIVATE + ggml-base nlohmann_json::nlohmann_json) + if(UNIX) + target_link_libraries(ds4_mix_converter PRIVATE m) + endif() +endif() + if(DFLASH27B_MIXED_GGML_SHARED) if(_dflash_build_shared_libs_was_defined) set(BUILD_SHARED_LIBS "${_dflash_saved_build_shared_libs}") @@ -947,6 +963,16 @@ if(DFLASH27B_TESTS) endif() list(APPEND _raw_unit_test_targets test_rocmfpx) + add_executable(test_ds4_mix_converter test/test_ds4_mix_converter.cpp) + target_include_directories(test_ds4_mix_converter PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/rocmfpx) + target_link_libraries(test_ds4_mix_converter PRIVATE ggml-base) + if(UNIX) + target_link_libraries(test_ds4_mix_converter PRIVATE m) + endif() + list(APPEND _raw_unit_test_targets test_ds4_mix_converter) + if(DFLASH27B_GPU_BACKEND STREQUAL "hip") add_executable(test_rocmfp4_hip_tail test/test_rocmfp4_hip_tail.cpp) set_source_files_properties(test/test_rocmfp4_hip_tail.cpp PROPERTIES LANGUAGE HIP) diff --git a/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.c b/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.c index ec2690327..242760e18 100644 --- a/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.c +++ b/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.c @@ -1292,3 +1292,248 @@ bool rocmfpx_validate_row_data_fp8(const void * data, size_t nbytes) { return true; } + +static float rocmfpx_bf16_to_fp32(uint16_t v) { + uint32_t bits = (uint32_t) v << 16; + float out; + memcpy(&out, &bits, sizeof(out)); + return out; +} + +static int rocmfpx_mix_nearest_level(float value, const float * levels, int nlevels) { + int lo = 0; + int hi = nlevels - 1; + while (lo < hi) { + const int mid = lo + (hi - lo) / 2; + const float split = 0.5f * (levels[mid] + levels[mid + 1]); + if (value <= split) { + hi = mid; + } else { + lo = mid + 1; + } + } + return lo; +} + +static bool rocmfpx_mix_prepare_books( + const uint16_t * codebooks_bf16, int k, float books[2][8]) { + if (!codebooks_bf16 || k <= 0 || k > 8) { + return false; + } + for (int b = 0; b < 2; ++b) { + for (int i = 0; i < k; ++i) { + const float value = rocmfpx_bf16_to_fp32(codebooks_bf16[b*k + i]); + if (!isfinite(value) || (i > 0 && !(value > books[b][i - 1]))) { + return false; + } + books[b][i] = value; + } + } + return true; +} + +static float rocmfpx_mix_half_error( + const float * x, const float * imatrix, const float * book, int k, + uint8_t e, float best) { + const float scale = rocmfpx_ue4m3_to_fp32(e); + if (!(scale > 0.0f)) { + return INFINITY; + } + const float inv_scale = 1.0f / scale; + float error = 0.0f; + for (int i = 0; i < 16; ++i) { + if (!isfinite(x[i])) { + return INFINITY; + } + const int code = rocmfpx_mix_nearest_level(x[i] * inv_scale, book, k); + const float delta = x[i] - scale * book[code]; + float weight = 1.0f; + if (imatrix) { + if (!isfinite(imatrix[i]) || imatrix[i] < 0.0f) { + return INFINITY; + } + weight = imatrix[i]; + } + error += weight * delta * delta; + if (error > best) { + break; + } + } + return error; +} + +static bool rocmfpx_mix_choose_half( + const float * x, const float * imatrix, float books[2][8], int k, + int * out_book, uint8_t * out_e) { + float max_abs = 0.0f; + for (int i = 0; i < 16; ++i) { + if (!isfinite(x[i])) { + return false; + } + const float ax = fabsf(x[i]); + if (ax > max_abs) { + max_abs = ax; + } + } + if (max_abs == 0.0f) { + *out_book = 0; + *out_e = 0; + return true; + } + + float best_error = INFINITY; + int best_book = 0; + uint8_t best_e = 1; + for (int book = 0; book < 2; ++book) { + float max_level = 0.0f; + for (int i = 0; i < k; ++i) { + const float a = fabsf(books[book][i]); + if (a > max_level) { + max_level = a; + } + } + if (!(max_level > 0.0f)) { + continue; + } + const uint8_t center = rocmfpx_nearest_scale_ue4m3(max_abs / max_level); + for (int delta = -2; delta <= 2; ++delta) { + const int candidate = (int) center + delta; + if (candidate < 1 || candidate > 0x7e) { + continue; + } + const float error = rocmfpx_mix_half_error( + x, imatrix, books[book], k, (uint8_t) candidate, best_error); + if (error < best_error || + (error == best_error && (book < best_book || + (book == best_book && candidate < best_e)))) { + best_error = error; + best_book = book; + best_e = (uint8_t) candidate; + } + } + } + if (!isfinite(best_error)) { + return false; + } + *out_book = best_book; + *out_e = best_e; + return true; +} + +bool rocmfpx_quantize_row_fp2_mix_ref( + const float * GGML_RESTRICT x, block_rocmfp2 * GGML_RESTRICT y, + int64_t k, const uint16_t codebooks_bf16[8], + const float * GGML_RESTRICT imatrix) { + if (!x || !y || k < 0 || k % QK_ROCMFP2 != 0) { + return false; + } + float books[2][8] = {{0}}; + if (!rocmfpx_mix_prepare_books(codebooks_bf16, 4, books)) { + return false; + } + const int64_t nb = k / QK_ROCMFP2; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP2; + block_rocmfp2 * yb = y + ib; + memset(yb, 0, sizeof(*yb)); + for (int half = 0; half < 2; ++half) { + const int off = half*16; + int book = 0; + uint8_t e = 0; + if (!rocmfpx_mix_choose_half( + xb + off, imatrix ? imatrix + ib*QK_ROCMFP2 + off : NULL, + books, 4, &book, &e)) { + return false; + } + yb->e[half] = (uint8_t) (e | (book << 7)); + const float scale = rocmfpx_ue4m3_to_fp32(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + for (int j = 0; j < 16; ++j) { + const int i = off + j; + const int code = scale > 0.0f + ? rocmfpx_mix_nearest_level(xb[i]*inv_scale, books[book], 4) + : rocmfpx_mix_nearest_level(0.0f, books[book], 4); + yb->qs[i >> 2] |= (uint8_t) (code << (2*(i & 3))); + } + } + } + return true; +} + +void rocmfpx_dequantize_row_fp2_mix( + const block_rocmfp2 * GGML_RESTRICT x, float * GGML_RESTRICT y, + int64_t k, const uint16_t codebooks_bf16[8]) { + assert(x && y && k % QK_ROCMFP2 == 0); + float books[2][8] = {{0}}; + const bool valid_books = rocmfpx_mix_prepare_books(codebooks_bf16, 4, books); + assert(valid_books); + if (!valid_books) return; + for (int64_t ib = 0; ib < k/QK_ROCMFP2; ++ib) { + for (int i = 0; i < QK_ROCMFP2; ++i) { + const uint8_t meta = x[ib].e[i >= 16]; + const int book = meta >> 7; + const float scale = rocmfpx_ue4m3_to_fp32(meta & 0x7f); + const int code = (x[ib].qs[i >> 2] >> (2*(i & 3))) & 3; + y[ib*QK_ROCMFP2 + i] = scale * books[book][code]; + } + } +} + +bool rocmfpx_quantize_row_fp3_mix_ref( + const float * GGML_RESTRICT x, block_rocmfp3 * GGML_RESTRICT y, + int64_t k, const uint16_t codebooks_bf16[16], + const float * GGML_RESTRICT imatrix) { + if (!x || !y || k < 0 || k % QK_ROCMFP3 != 0) { + return false; + } + float books[2][8] = {{0}}; + if (!rocmfpx_mix_prepare_books(codebooks_bf16, 8, books)) { + return false; + } + const int64_t nb = k / QK_ROCMFP3; + for (int64_t ib = 0; ib < nb; ++ib) { + const float * xb = x + ib*QK_ROCMFP3; + block_rocmfp3 * yb = y + ib; + memset(yb, 0, sizeof(*yb)); + for (int half = 0; half < 2; ++half) { + const int off = half*16; + int book = 0; + uint8_t e = 0; + if (!rocmfpx_mix_choose_half( + xb + off, imatrix ? imatrix + ib*QK_ROCMFP3 + off : NULL, + books, 8, &book, &e)) { + return false; + } + yb->e[half] = (uint8_t) (e | (book << 7)); + const float scale = rocmfpx_ue4m3_to_fp32(e); + const float inv_scale = scale > 0.0f ? 1.0f / scale : 0.0f; + for (int j = 0; j < 16; ++j) { + const int i = off + j; + const int code = scale > 0.0f + ? rocmfpx_mix_nearest_level(xb[i]*inv_scale, books[book], 8) + : rocmfpx_mix_nearest_level(0.0f, books[book], 8); + rocmfpx_set_bits(yb->qs, i*3, 3, (uint32_t) code); + } + } + } + return true; +} + +void rocmfpx_dequantize_row_fp3_mix( + const block_rocmfp3 * GGML_RESTRICT x, float * GGML_RESTRICT y, + int64_t k, const uint16_t codebooks_bf16[16]) { + assert(x && y && k % QK_ROCMFP3 == 0); + float books[2][8] = {{0}}; + const bool valid_books = rocmfpx_mix_prepare_books(codebooks_bf16, 8, books); + assert(valid_books); + if (!valid_books) return; + for (int64_t ib = 0; ib < k/QK_ROCMFP3; ++ib) { + for (int i = 0; i < QK_ROCMFP3; ++i) { + const uint8_t meta = x[ib].e[i >= 16]; + const int book = meta >> 7; + const float scale = rocmfpx_ue4m3_to_fp32(meta & 0x7f); + const int code = (int) rocmfpx_get_bits(x[ib].qs, i*3, 3); + y[ib*QK_ROCMFP3 + i] = scale * books[book][code]; + } + } +} diff --git a/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.h b/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.h index f9d6cb896..f202e373d 100644 --- a/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.h +++ b/server/deps/llama.cpp/ggml/rocmfpx/rocmfpx.h @@ -86,11 +86,20 @@ GGML_API void rocmfpx_dequantize_row_fp2(const block_rocmfp2 * GGML_RESTRICT x GGML_API void rocmfpx_quantize_row_fp2(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); GGML_API size_t rocmfpx_quantize_fp2(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); +// Adaptive MIX block codecs. `codebooks_bf16` contains two sorted codebooks +// (2*4 or 2*8 levels). The high bit of each half-block metadata byte selects +// the codebook; the low seven bits retain the unsigned E4M3 scale. +GGML_API bool rocmfpx_quantize_row_fp2_mix_ref(const float * GGML_RESTRICT x, block_rocmfp2 * GGML_RESTRICT y, int64_t k, const uint16_t codebooks_bf16[8], const float * GGML_RESTRICT imatrix); +GGML_API void rocmfpx_dequantize_row_fp2_mix(const block_rocmfp2 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k, const uint16_t codebooks_bf16[8]); + GGML_API void rocmfpx_quantize_row_fp3_ref(const float * GGML_RESTRICT x, block_rocmfp3 * GGML_RESTRICT y, int64_t k); GGML_API void rocmfpx_dequantize_row_fp3(const block_rocmfp3 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); GGML_API void rocmfpx_quantize_row_fp3(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); GGML_API size_t rocmfpx_quantize_fp3(const float * GGML_RESTRICT src, void * GGML_RESTRICT dst, int64_t nrows, int64_t n_per_row, const float * imatrix); +GGML_API bool rocmfpx_quantize_row_fp3_mix_ref(const float * GGML_RESTRICT x, block_rocmfp3 * GGML_RESTRICT y, int64_t k, const uint16_t codebooks_bf16[16], const float * GGML_RESTRICT imatrix); +GGML_API void rocmfpx_dequantize_row_fp3_mix(const block_rocmfp3 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k, const uint16_t codebooks_bf16[16]); + GGML_API void rocmfpx_quantize_row_fp6_ref(const float * GGML_RESTRICT x, block_rocmfp6 * GGML_RESTRICT y, int64_t k); GGML_API void rocmfpx_dequantize_row_fp6(const block_rocmfp6 * GGML_RESTRICT x, float * GGML_RESTRICT y, int64_t k); GGML_API void rocmfpx_quantize_row_fp6(const float * GGML_RESTRICT x, void * GGML_RESTRICT y, int64_t k); diff --git a/server/test/test_ds4v_mix_converter.cpp b/server/test/test_ds4v_mix_converter.cpp new file mode 100644 index 000000000..ea5f0d41a --- /dev/null +++ b/server/test/test_ds4v_mix_converter.cpp @@ -0,0 +1,181 @@ +// CPU reference tests for the adaptive MIX block codecs behind qtype 105 +// (Q3_1_ROCMFP3_MIX) and qtype 106 (Q2_1_ROCMFP2_MIX). +// +// WHY THIS EXISTS. The generic ggml to_float/from_float entry points ABORT +// for both MIX types, so the only legal CPU path is the adaptive +// codebook pair in rocmfpx.h. A silent fixed-level fallback would decode +// adaptive experts with the wrong levels. These tests pin the contract. +// They exercise the PREDICATE and the byte layout, never the GPU kernels. +// No device work happens here. Every case is pure CPU codec logic. + +#include "ggml.h" +#include "rocmfpx.h" +#include "CppUnitTestFramework.hpp" +using CppUnitTestFramework::CommonFixture; +#undef CHECK + +#include +#include +#include +#include + +static int g_fails = 0; +#define CHECK(cond, msg) \ + do { \ + if (!(cond)) { std::fprintf(stderr, "FAIL: %s\n", (msg)); ++g_fails; } \ + } while (0) + +namespace { + +static_assert(sizeof(block_rocmfp2) == 10, "qtype-106 ABI changed"); +static_assert(sizeof(block_rocmfp3) == 14, "qtype-105 ABI changed"); + +struct Ds4MixConverterFixture : CommonFixture { + using CommonFixture::CommonFixture; +}; + +static uint16_t f32_to_bf16_bits(float x) { + uint32_t u = 0; + std::memcpy(&u, &x, 4); + if (!std::isfinite(x)) { + return (uint16_t) (u >> 16); + } + const uint32_t rounded = u + 0x7FFFu + ((u >> 16) & 1u); + return (uint16_t) (rounded >> 16); +} + +static float bf16_bits_to_f32(uint16_t h) { + const uint32_t u = (uint32_t) h << 16; + float x = 0.0f; + std::memcpy(&x, &u, 4); + return x; +} + +static void fill_books2(uint16_t out[8]) { + const float b0[4] = {-2.0f, -0.5f, 0.5f, 2.0f}; + const float b1[4] = {-4.0f, -1.0f, 1.0f, 4.0f}; + for (int i = 0; i < 4; ++i) { + out[i] = f32_to_bf16_bits(b0[i]); + out[4 + i] = f32_to_bf16_bits(b1[i]); + } +} + +static void fill_books3(uint16_t out[16]) { + const float b0[8] = {-4.0f, -2.0f, -1.0f, -0.5f, 0.5f, 1.0f, 2.0f, 4.0f}; + const float b1[8] = {-8.0f, -4.0f, -2.0f, -1.0f, 1.0f, 2.0f, 4.0f, 8.0f}; + for (int i = 0; i < 8; ++i) { + out[i] = f32_to_bf16_bits(b0[i]); + out[8 + i] = f32_to_bf16_bits(b1[i]); + } +} + +static float row_max_err(const float * a, const float * b, int n) { + float m = 0.0f; + for (int i = 0; i < n; ++i) { + const float d = std::fabs(a[i] - b[i]); + if (d > m) m = d; + } + return m; +} + +TEST_CASE(Ds4MixConverterFixture, fp2_mix_roundtrip) { + float x[32]; + for (int i = 0; i < 32; ++i) { + x[i] = -4.0f + (float) i * (8.0f / 31.0f); + } + uint16_t books[8]; + fill_books2(books); + block_rocmfp2 q[1]; + float y[32]; + CHECK(rocmfpx_quantize_row_fp2_mix_ref(x, q, 32, books, nullptr), + "fp2 mix quantize accepts valid books without imatrix"); + rocmfpx_dequantize_row_fp2_mix(q, y, 32, books); + CHECK(row_max_err(x, y, 32) < 2.0f, "fp2 mix roundtrip error stays bounded"); +} + +TEST_CASE(Ds4MixConverterFixture, fp3_mix_roundtrip) { + float x[32]; + for (int i = 0; i < 32; ++i) { + x[i] = -4.0f + (float) i * (8.0f / 31.0f); + } + uint16_t books[16]; + fill_books3(books); + block_rocmfp3 q[1]; + float y[32]; + CHECK(rocmfpx_quantize_row_fp3_mix_ref(x, q, 32, books, nullptr), + "fp3 mix quantize accepts valid books without imatrix"); + rocmfpx_dequantize_row_fp3_mix(q, y, 32, books); + CHECK(row_max_err(x, y, 32) < 1.0f, "fp3 mix roundtrip error stays bounded"); +} + +TEST_CASE(Ds4MixConverterFixture, fp2_byte_layout_reference) { + float x[32]; + for (int i = 0; i < 32; ++i) { + x[i] = (i % 2) ? 0.21f : -0.21f; + } + uint16_t books[8]; + fill_books2(books); + block_rocmfp2 q[1]; + float y[32]; + CHECK(rocmfpx_quantize_row_fp2_mix_ref(x, q, 32, books, nullptr), + "fp2 mix quantize succeeds for layout check"); + rocmfpx_dequantize_row_fp2_mix(q, y, 32, books); + float lvl[2][4]; + for (int b = 0; b < 2; ++b) { + for (int i = 0; i < 4; ++i) { + lvl[b][i] = bf16_bits_to_f32(books[b * 4 + i]); + } + } + for (int i = 0; i < 32; ++i) { + const uint8_t meta = q[0].e[i >= 16]; + const int book = meta >> 7; + const float scale = rocmfpx_ue4m3_to_fp32(meta & 0x7f); + const int code = (q[0].qs[i >> 2] >> (2 * (i & 3))) & 3; + const float expect = scale * lvl[book][code]; + CHECK(y[i] == expect, "fp2 decoded value matches the byte layout exactly"); + } +} + +TEST_CASE(Ds4MixConverterFixture, weighted_encode_honors_imatrix) { + float x[32]; + float w[32]; + for (int i = 0; i < 32; ++i) { + x[i] = 0.05f; + w[i] = 0.0f; + } + x[0] = 1.9f; + w[0] = 1000000.0f; + uint16_t books[8]; + fill_books2(books); + block_rocmfp2 q[1]; + float y[32]; + CHECK(rocmfpx_quantize_row_fp2_mix_ref(x, q, 32, books, w), + "fp2 mix quantize accepts an imatrix"); + rocmfpx_dequantize_row_fp2_mix(q, y, 32, books); + CHECK(std::fabs(y[0] - 1.9f) < 0.5f, "weighted encode protects the important value"); +} + +TEST_CASE(Ds4MixConverterFixture, malformed_books_rejected) { + float x[32] = {0.0f}; + block_rocmfp2 q2[1]; + block_rocmfp3 q3[1]; + uint16_t unsorted[8]; + fill_books2(unsorted); + unsorted[1] = unsorted[0]; + CHECK(!rocmfpx_quantize_row_fp2_mix_ref(x, q2, 32, unsorted, nullptr), + "fp2 mix rejects an unsorted codebook"); + uint16_t nanbook[8]; + fill_books2(nanbook); + nanbook[2] = 0x7FC0u; + CHECK(!rocmfpx_quantize_row_fp2_mix_ref(x, q2, 32, nanbook, nullptr), + "fp2 mix rejects a non-finite codebook level"); + CHECK(!rocmfpx_quantize_row_fp2_mix_ref(x, q2, 32, nullptr, nullptr), + "fp2 mix rejects null codebooks"); + uint16_t unsorted3[16]; + fill_books3(unsorted3); + unsorted3[9] = unsorted3[8]; + CHECK(!rocmfpx_quantize_row_fp3_mix_ref(x, q3, 32, unsorted3, nullptr), + "fp3 mix rejects an unsorted codebook"); +} + +} // namespace diff --git a/server/tools/ds4_mix_converter/CMakeLists.txt b/server/tools/ds4_mix_converter/CMakeLists.txt new file mode 100644 index 000000000..6f12682af --- /dev/null +++ b/server/tools/ds4_mix_converter/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4_mix_converter LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_SERVER OFF CACHE BOOL "" FORCE) +set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE) +set(GGML_NATIVE OFF CACHE BOOL "" FORCE) +set(GGML_OPENMP OFF CACHE BOOL "" FORCE) +set(GGML_BLAS OFF CACHE BOOL "" FORCE) +set(GGML_CUDA OFF CACHE BOOL "" FORCE) +set(GGML_HIP OFF CACHE BOOL "" FORCE) + +include(FetchContent) +FetchContent_Declare(nlohmann_json + URL https://codeload.github.com/nlohmann/json/tar.gz/9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 + URL_HASH SHA256=0dbc5e40a01ff142e7e68c03e85247a4dcede2f592d12d3677dee3664d17975a) +FetchContent_MakeAvailable(nlohmann_json) +add_subdirectory(../../deps/llama.cpp/ggml ggml EXCLUDE_FROM_ALL) + +add_executable(ds4_mix_converter ds4_mix_converter.cpp) +target_include_directories(ds4_mix_converter PRIVATE + ../../deps/llama.cpp/ggml/include + ../../deps/llama.cpp/ggml/rocmfpx) +target_link_libraries(ds4_mix_converter PRIVATE ggml-base nlohmann_json::nlohmann_json) +if(UNIX) + target_link_libraries(ds4_mix_converter PRIVATE m) +endif() +target_compile_options(ds4_mix_converter PRIVATE -Wall -Wextra -Wpedantic) + +add_executable(test_ds4_mix_converter ../../test/test_ds4_mix_converter.cpp) +target_include_directories(test_ds4_mix_converter PRIVATE + ../../deps/llama.cpp/ggml/include + ../../deps/llama.cpp/ggml/rocmfpx) +target_link_libraries(test_ds4_mix_converter PRIVATE ggml-base) +if(UNIX) + target_link_libraries(test_ds4_mix_converter PRIVATE m) +endif() +enable_testing() +add_test(NAME ds4_mix_converter_codec COMMAND test_ds4_mix_converter) diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp new file mode 100644 index 000000000..a23f6a875 --- /dev/null +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -0,0 +1,1362 @@ +#include "ggml.h" +#include "gguf.h" +#include "rocmfpx.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; +namespace fs = std::filesystem; + +namespace { + +constexpr uint32_t kQtypeP4Mix = 105; +constexpr uint32_t kQtypeGuMix = 106; +constexpr uint32_t kCodebooks = 2; +constexpr uint32_t kBlock = 32; +constexpr uint32_t kP4Levels = 8; +constexpr uint32_t kGuLevels = 4; +constexpr uint32_t kAlignment = 32; +constexpr size_t kCopyChunk = 8u * 1024u * 1024u; + +[[noreturn]] void fail(const std::string & message) { + throw std::runtime_error(message); +} + +uint64_t checked_mul(uint64_t a, uint64_t b, const std::string & what) { + if (a != 0 && b > std::numeric_limits::max() / a) { + fail("overflow computing " + what); + } + return a*b; +} + +uint64_t element_count(const std::vector & shape, const std::string & name) { + if (shape.empty() || shape.size() > GGML_MAX_DIMS) { + fail("unsupported rank for " + name); + } + uint64_t n = 1; + for (int64_t d : shape) { + if (d <= 0) fail("non-positive dimension for " + name); + n = checked_mul(n, static_cast(d), "element count for " + name); + } + return n; +} + +size_t dtype_size(const std::string & dtype) { + if (dtype == "BF16" || dtype == "F16") return 2; + if (dtype == "F32" || dtype == "I32" || dtype == "U32") return 4; + if (dtype == "I64" || dtype == "U64") return 8; + if (dtype == "I8" || dtype == "U8" || dtype == "F8_E4M3" || dtype == "F8_E8M0") return 1; + fail("unsupported safetensors dtype " + dtype); +} + +uint16_t float_to_bf16(float value) { + uint32_t bits; + std::memcpy(&bits, &value, sizeof(bits)); + const uint32_t rounding = 0x7fffu + ((bits >> 16) & 1u); + return static_cast((bits + rounding) >> 16); +} + +float bf16_to_float(uint16_t value) { + uint32_t bits = static_cast(value) << 16; + float out; + std::memcpy(&out, &bits, sizeof(out)); + return out; +} + +float fp4_e2m1(uint8_t code) { + static constexpr float table[16] = { + 0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f, + 0.0f,-0.5f,-1.0f,-1.5f,-2.0f,-3.0f,-4.0f,-6.0f, + }; + return table[code & 15u]; +} + +float fp8_e4m3fn(uint8_t value) { + const int sign = (value & 0x80u) ? -1 : 1; + const int exponent = (value >> 3) & 15; + const int mantissa = value & 7; + if (exponent == 15 && mantissa == 7) { + fail("F8_E4M3 contains NaN encoding"); + } + const float magnitude = exponent == 0 + ? std::ldexp(static_cast(mantissa), -9) + : std::ldexp(static_cast(8 + mantissa), exponent - 10); + return sign * magnitude; +} + +float fp8_e8m0(uint8_t value) { + if (value == 0xffu) fail("F8_E8M0 contains NaN encoding"); + return std::ldexp(1.0f, static_cast(value) - 127); +} + +struct FileDescriptor { + int fd = -1; + explicit FileDescriptor(const fs::path & path) { + fd = ::open(path.c_str(), O_RDONLY); + if (fd < 0) fail("open " + path.string() + ": " + std::strerror(errno)); + } + FileDescriptor(const FileDescriptor &) = delete; + FileDescriptor & operator=(const FileDescriptor &) = delete; + ~FileDescriptor() { if (fd >= 0) ::close(fd); } +}; + +void pread_exact(int fd, void * dst, size_t n, uint64_t offset, const std::string & what) { + auto * p = static_cast(dst); + size_t done = 0; + while (done < n) { + const ssize_t got = ::pread(fd, p + done, n - done, static_cast(offset + done)); + if (got < 0 && errno == EINTR) continue; + if (got <= 0) fail("short read for " + what); + done += static_cast(got); + } +} + +struct StEntry { + std::string name; + std::string dtype; + std::vector shape; + fs::path path; + uint64_t offset = 0; + uint64_t size = 0; +}; + +class SafeTensorSet { +public: + explicit SafeTensorSet(fs::path root) : root_(std::move(root)) { + if (!fs::is_directory(root_)) fail("input is not a directory: " + root_.string()); + config_ = read_json(root_ / "config.json"); + tokenizer_ = read_json(root_ / "tokenizer.json"); + tokenizer_config_ = read_json(root_ / "tokenizer_config.json"); + const json index = read_json(root_ / "model.safetensors.index.json"); + if (!index.contains("weight_map") || !index["weight_map"].is_object()) { + fail("model.safetensors.index.json has no object weight_map"); + } + std::map weight_map; + std::set shards; + for (const auto & item : index["weight_map"].items()) { + if (!item.value().is_string()) fail("weight_map value is not a string for " + item.key()); + const std::string shard = item.value().get(); + if (fs::path(shard).is_absolute() || shard.find("..") != std::string::npos) { + fail("unsafe shard path " + shard); + } + weight_map.emplace(item.key(), shard); + shards.insert(shard); + } + for (const std::string & shard : shards) load_shard(shard); + if (entries_.size() != weight_map.size()) { + fail("safetensors headers contain " + std::to_string(entries_.size()) + + " tensors but index names " + std::to_string(weight_map.size())); + } + for (const auto & item : weight_map) { + const auto it = entries_.find(item.first); + if (it == entries_.end()) fail("index tensor missing from shard header: " + item.first); + if (it->second.path.filename() != item.second) { + fail("index shard mismatch for " + item.first); + } + } + } + + const StEntry & at(const std::string & name) const { + const auto it = entries_.find(name); + if (it == entries_.end()) fail("missing required tensor " + name); + return it->second; + } + bool contains(const std::string & name) const { return entries_.count(name) != 0; } + const std::unordered_map & entries() const { return entries_; } + const json & config() const { return config_; } + const json & tokenizer() const { return tokenizer_; } + const json & tokenizer_config() const { return tokenizer_config_; } + +private: + static json read_json(const fs::path & path) { + std::ifstream in(path); + if (!in) fail("cannot open " + path.string()); + json result; + try { in >> result; } + catch (const std::exception & e) { fail("invalid JSON " + path.string() + ": " + e.what()); } + return result; + } + + void load_shard(const std::string & filename) { + const fs::path path = root_ / filename; + FileDescriptor fd(path); + struct stat st{}; + if (::fstat(fd.fd, &st) != 0 || st.st_size < 8) fail("invalid shard file " + path.string()); + uint64_t header_len = 0; + pread_exact(fd.fd, &header_len, sizeof(header_len), 0, path.string() + " header length"); +#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ + fail("big-endian hosts are not supported"); +#endif + if (header_len == 0 || header_len > static_cast(st.st_size) - 8 || header_len > (1ull << 31)) { + fail("invalid safetensors header length in " + path.string()); + } + std::string header(static_cast(header_len), 0); + pread_exact(fd.fd, header.data(), header.size(), 8, path.string() + " header"); + json parsed; + try { parsed = json::parse(header); } + catch (const std::exception & e) { fail("invalid safetensors header in " + path.string() + ": " + e.what()); } + for (const auto & item : parsed.items()) { + if (item.key() == "__metadata__") continue; + const json & info = item.value(); + if (!info.contains("dtype") || !info.contains("shape") || !info.contains("data_offsets")) { + fail("incomplete safetensors entry " + item.key()); + } + StEntry entry; + entry.name = item.key(); + entry.dtype = info["dtype"].get(); + entry.shape = info["shape"].get>(); + const auto range = info["data_offsets"].get>(); + if (range.size() != 2 || range[1] < range[0]) fail("bad data_offsets for " + entry.name); + entry.path = path; + entry.offset = 8 + header_len + range[0]; + entry.size = range[1] - range[0]; + const uint64_t expected = checked_mul(element_count(entry.shape, entry.name), dtype_size(entry.dtype), "byte size for " + entry.name); + if (entry.size != expected || entry.offset > static_cast(st.st_size) || + entry.size > static_cast(st.st_size) - entry.offset) { + fail("size or bounds mismatch for " + entry.name); + } + if (!entries_.emplace(entry.name, std::move(entry)).second) { + fail("duplicate tensor " + item.key()); + } + } + } + + fs::path root_; + json config_; + json tokenizer_; + json tokenizer_config_; + std::unordered_map entries_; +}; + +struct ImatrixEntry { + int32_t calls = 0; + std::vector values; +}; +using Imatrix = std::unordered_map; + +int32_t read_i32(std::ifstream & in, const std::string & what) { + int32_t value = 0; + if (!in.read(reinterpret_cast(&value), sizeof(value))) fail("truncated imatrix " + what); + return value; +} + +Imatrix load_imatrix(const fs::path & path) { + std::ifstream in(path, std::ios::binary); + if (!in) fail("cannot open imatrix " + path.string()); + const int32_t count = read_i32(in, "entry count"); + if (count <= 0 || count > 1000000) fail("invalid imatrix entry count"); + Imatrix result; + for (int32_t i = 0; i < count; ++i) { + const int32_t name_len = read_i32(in, "name length"); + if (name_len <= 0 || name_len > 4096) fail("invalid imatrix name length"); + std::string name(static_cast(name_len), 0); + if (!in.read(name.data(), name.size())) fail("truncated imatrix name"); + ImatrixEntry entry; + entry.calls = read_i32(in, "call count"); + const int32_t nvalues = read_i32(in, "value count"); + if (entry.calls <= 0 || nvalues <= 0 || nvalues > (1 << 24)) fail("invalid imatrix entry dimensions for " + name); + entry.values.resize(static_cast(nvalues)); + if (!in.read(reinterpret_cast(entry.values.data()), entry.values.size()*sizeof(float))) { + fail("truncated imatrix values for " + name); + } + for (float value : entry.values) { + if (!std::isfinite(value) || value < 0.0f) fail("invalid imatrix value for " + name); + } + if (!result.emplace(name, std::move(entry)).second) fail("duplicate imatrix entry " + name); + } + char extra = 0; + if (in.read(&extra, 1)) fail("trailing bytes in imatrix " + path.string()); + std::cerr << "[calibration] loaded " << result.size() << " imatrix entries from " << path << "\n"; + return result; +} + +const std::vector * require_imatrix( + const std::optional & imatrix, const std::string & name, size_t in) { + if (!imatrix) return nullptr; + const auto it = imatrix->find(name); + if (it == imatrix->end()) fail("imatrix is missing required entry " + name); + if (it->second.values.size() != in) { + fail("imatrix entry " + name + " has " + std::to_string(it->second.values.size()) + + " values, expected " + std::to_string(in)); + } + return &it->second.values; +} + +enum class Surface : uint32_t { Gate = 0, Up = 1, Down = 2 }; +enum class BookSource { GateUpJoint, DownOnly }; + +struct ExpertRecipe { + const char * source_leaf; + const char * target_leaf; + Surface surface; + ggml_type qtype; + BookSource books; + uint32_t levels; +}; + +constexpr std::array kExpertRecipes{{ + {"w1", "ffn_gate_exps.weight", Surface::Gate, GGML_TYPE_Q2_1_ROCMFP2_MIX, BookSource::GateUpJoint, kGuLevels}, + {"w3", "ffn_up_exps.weight", Surface::Up, GGML_TYPE_Q2_1_ROCMFP2_MIX, BookSource::GateUpJoint, kGuLevels}, + {"w2", "ffn_down_exps.weight", Surface::Down, GGML_TYPE_Q3_1_ROCMFP3_MIX, BookSource::DownOnly, kP4Levels}, +}}; + +std::string source_expert_name(int layer, int expert, const ExpertRecipe & recipe, const char * suffix) { + return "layers." + std::to_string(layer) + ".ffn.experts." + std::to_string(expert) + + "." + recipe.source_leaf + "." + suffix; +} +std::string target_expert_name(int layer, const ExpertRecipe & recipe) { + return "blk." + std::to_string(layer) + "." + recipe.target_leaf; +} + +struct TensorShape { + uint32_t in = 0; + uint32_t out = 0; +}; + +TensorShape validate_expert_source( + const SafeTensorSet & source, int layer, int expert, const ExpertRecipe & recipe) { + const StEntry & weight = source.at(source_expert_name(layer, expert, recipe, "weight")); + const StEntry & scale = source.at(source_expert_name(layer, expert, recipe, "scale")); + if (weight.dtype != "I8" || weight.shape.size() != 2) { + fail("expert weight must be rank-2 packed I8: " + weight.name); + } + const int64_t out = weight.shape[0]; + const int64_t in = weight.shape[1]*2; + if (out <= 0 || in <= 0 || in % kBlock != 0 || out > UINT32_MAX || in > UINT32_MAX) { + fail("unsupported expert dimensions for " + weight.name); + } + if (recipe.qtype == GGML_TYPE_Q2_1_ROCMFP2_MIX && in % 128 != 0) { + fail("qtype-106 input dimension must be a multiple of 128: " + weight.name); + } + if (scale.dtype != "F8_E8M0" || scale.shape != std::vector{out, in/32}) { + fail("expert scale shape/dtype mismatch for " + scale.name); + } + return {static_cast(in), static_cast(out)}; +} + +class HistogramFitter { +public: + static constexpr int kBins = 4097; + HistogramFitter() { for (auto & h : hist_) h.assign(kBins, 0.0); } + + void add_half(const float values[16], const float * importance) { + float max_abs = 0.0f; + double sum_sq = 0.0; + for (int i = 0; i < 16; ++i) { + if (!std::isfinite(values[i])) fail("non-finite expert value during calibration"); + max_abs = std::max(max_abs, std::fabs(values[i])); + sum_sq += static_cast(values[i])*values[i]; + } + if (!(max_abs > 0.0f)) { + hist_[0][kBins/2] += 16.0; + return; + } + const double rms_ratio = std::sqrt(sum_sq/16.0)/max_abs; + const int population = rms_ratio < 0.52 ? 0 : 1; + for (int i = 0; i < 16; ++i) { + const float normalized = std::max(-1.0f, std::min(1.0f, values[i]/max_abs)); + const int bin = std::max(0, std::min(kBins - 1, + static_cast(std::lround((normalized + 1.0f)*0.5f*(kBins - 1))))); + double w = importance ? importance[i] : 1.0; + if (w > 0.0 && std::isfinite(w)) hist_[population][bin] += w; + } + } + + std::vector fit(int levels) const { + if (levels != 4 && levels != 8) fail("unsupported fitted codebook size"); + std::vector result; + result.reserve(static_cast(2*levels)); + std::vector fallback(kBins, 0.0); + for (int i = 0; i < kBins; ++i) fallback[i] = hist_[0][i] + hist_[1][i]; + for (int population = 0; population < 2; ++population) { + const std::vector & h = total(hist_[population]) > 0.0 ? hist_[population] : fallback; + const std::vector centers = lloyd(h, levels); + float previous = -std::numeric_limits::infinity(); + for (int i = 0; i < levels; ++i) { + const uint16_t b = float_to_bf16(centers[i]); + const float roundtrip = bf16_to_float(b); + if (!std::isfinite(roundtrip) || !(roundtrip > previous)) { + fail("fitted codebook collapses after BF16 rounding"); + } + result.push_back(b); + previous = roundtrip; + } + } + return result; + } + +private: + static double total(const std::vector & h) { + double out = 0.0; + for (double value : h) out += value; + return out; + } + static float coordinate(int bin) { + return -1.0f + 2.0f*static_cast(bin)/static_cast(kBins - 1); + } + static std::vector lloyd(const std::vector & h, int k) { + const double mass = total(h); + if (!(mass > 0.0)) fail("cannot fit an empty codebook histogram"); + std::vector c(static_cast(k)); + for (int j = 0; j < k; ++j) { + const double target = mass*(j + 0.5)/k; + double running = 0.0; + int bin = 0; + for (; bin < kBins - 1; ++bin) { + running += h[bin]; + if (running >= target) break; + } + c[j] = coordinate(bin); + } + for (int iteration = 0; iteration < 24; ++iteration) { + std::vector sums(static_cast(k), 0.0); + std::vector weights(static_cast(k), 0.0); + int cluster = 0; + for (int bin = 0; bin < kBins; ++bin) { + const float x = coordinate(bin); + while (cluster + 1 < k && x > 0.5f*(c[cluster] + c[cluster + 1])) ++cluster; + sums[cluster] += h[bin]*x; + weights[cluster] += h[bin]; + } + for (int j = 0; j < k; ++j) if (weights[j] > 0.0) c[j] = static_cast(sums[j]/weights[j]); + std::sort(c.begin(), c.end()); + } + for (int j = 1; j < k; ++j) { + if (!(c[j] > c[j - 1])) fail("degenerate adaptive codebook"); + } + return c; + } + + std::array, 2> hist_; +}; + +struct OpenTensorPair { + FileDescriptor weight_fd; + FileDescriptor scale_fd; + const StEntry & weight; + const StEntry & scale; + OpenTensorPair(const StEntry & w, const StEntry & s) + : weight_fd(w.path), scale_fd(s.path), weight(w), scale(s) {} +}; + +void decode_expert_row( + const OpenTensorPair & input, uint32_t row, uint32_t in, + std::vector & packed, std::vector & scales, + std::vector & values) { + packed.resize(in/2); + scales.resize(in/32); + values.resize(in); + pread_exact(input.weight_fd.fd, packed.data(), packed.size(), + input.weight.offset + static_cast(row)*packed.size(), input.weight.name); + pread_exact(input.scale_fd.fd, scales.data(), scales.size(), + input.scale.offset + static_cast(row)*scales.size(), input.scale.name); + for (uint32_t col = 0; col < in; ++col) { + const uint8_t byte = packed[col/2]; + const uint8_t nibble = (col & 1) ? (byte >> 4) : (byte & 15u); + values[col] = fp4_e2m1(nibble)*fp8_e8m0(scales[col/32]); + if (!std::isfinite(values[col])) fail("non-finite decoded expert value in " + input.weight.name); + } +} + +void add_expert_to_fitter( + const SafeTensorSet & source, int layer, int expert, + const ExpertRecipe & recipe, const std::vector * importance, + HistogramFitter & fitter) { + const TensorShape shape = validate_expert_source(source, layer, expert, recipe); + const StEntry & w = source.at(source_expert_name(layer, expert, recipe, "weight")); + const StEntry & s = source.at(source_expert_name(layer, expert, recipe, "scale")); + OpenTensorPair input(w, s); + std::vector packed, scales; + std::vector values; + for (uint32_t row = 0; row < shape.out; ++row) { + decode_expert_row(input, row, shape.in, packed, scales, values); + for (uint32_t col = 0; col < shape.in; col += 16) { + fitter.add_half(values.data() + col, + importance ? importance->data() + col : nullptr); + } + } +} + +struct CodebookRegistry { + uint32_t levels = 0; + std::vector> experts; +}; + +struct LayerCalibration { + int layer = 0; + TensorShape gate_up_shape; + TensorShape down_shape; + CodebookRegistry gate_up; + CodebookRegistry down; +}; + +struct Options { + fs::path input; + fs::path output; + std::optional imatrix; + bool absmax_only = false; + bool experts_only = false; + bool force = false; + bool validate_input_only = false; + int layer_start = 0; + int layer_count = -1; + int expert_limit = -1; +}; + +void usage(const char * argv0) { + std::cerr << "Usage: " << argv0 << " --input DIR --output FILE (--imatrix FILE | --absmax-only)\n" + << " [--layer-start N] [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force]\n"; +} + +int parse_nonnegative(const char * value, const std::string & option, bool allow_zero = true) { + char * end = nullptr; + errno = 0; + const long parsed = std::strtol(value, &end, 10); + if (errno || !end || *end || parsed < (allow_zero ? 0 : 1) || parsed > INT32_MAX) { + fail("invalid value for " + option + ": " + value); + } + return static_cast(parsed); +} + +Options parse_options(int argc, char ** argv) { + Options out; + for (int i = 1; i < argc; ++i) { + const std::string arg = argv[i]; + auto value = [&]() -> const char * { + if (++i >= argc) fail("missing value after " + arg); + return argv[i]; + }; + if (arg == "--input") out.input = value(); + else if (arg == "--output") out.output = value(); + else if (arg == "--imatrix") out.imatrix = fs::path(value()); + else if (arg == "--absmax-only") out.absmax_only = true; + else if (arg == "--experts-only") out.experts_only = true; + else if (arg == "--force") out.force = true; + else if (arg == "--validate-input-only") out.validate_input_only = true; + else if (arg == "--layer-start") out.layer_start = parse_nonnegative(value(), arg); + else if (arg == "--layer-count") out.layer_count = parse_nonnegative(value(), arg, false); + else if (arg == "--expert-limit") out.expert_limit = parse_nonnegative(value(), arg, false); + else if (arg == "--help" || arg == "-h") { usage(argv[0]); std::exit(0); } + else fail("unknown option " + arg); + } + if (out.input.empty() || out.output.empty()) fail("--input and --output are required"); + if (out.absmax_only == out.imatrix.has_value()) { + fail("choose exactly one of --imatrix FILE or --absmax-only"); + } + return out; +} + +uint32_t config_u32(const json & c, const char * key, uint32_t fallback = 0) { + if (!c.contains(key)) { + if (fallback != 0) return fallback; + fail(std::string("config.json is missing ") + key); + } + const int64_t value = c[key].get(); + if (value <= 0 || value > UINT32_MAX) fail(std::string("invalid config value ") + key); + return static_cast(value); +} + +float config_f32(const json & c, const char * key, float fallback) { + if (!c.contains(key)) return fallback; + const float value = c[key].get(); + if (!std::isfinite(value)) fail(std::string("non-finite config value ") + key); + return value; +} + +template +void append_le(std::vector & out, T value) { +#if __BYTE_ORDER__ != __ORDER_LITTLE_ENDIAN__ + fail("big-endian hosts are not supported"); +#endif + const auto * p = reinterpret_cast(&value); + out.insert(out.end(), p, p + sizeof(value)); +} + +std::vector make_p4_blob(const std::vector & layers, uint32_t experts) { + std::vector out; + const char magic[8] = {'P','4','M','I','X','v','1','\0'}; + out.insert(out.end(), magic, magic + 8); + append_le(out, static_cast(layers.size())); + append_le(out, 0); + for (const LayerCalibration & layer : layers) { + if (layer.down.levels != kP4Levels || layer.down.experts.size() != experts) { + fail("incomplete qtype-105 codebook registry at layer " + std::to_string(layer.layer)); + } + append_le(out, static_cast(layer.layer)); + append_le(out, experts); + append_le(out, layer.down_shape.out); + append_le(out, layer.down_shape.in); + append_le(out, kCodebooks); + append_le(out, kP4Levels); + out.insert(out.end(), experts, 1u); + out.insert(out.end(), experts, 0u); + for (const auto & book : layer.down.experts) { + if (book.size() != kCodebooks*kP4Levels) fail("bad qtype-105 codebook length"); + for (uint16_t value : book) append_le(out, value); + } + } + return out; +} + +std::vector make_gumix_blob(const std::vector & layers, uint32_t experts) { + std::vector out; + const char magic[8] = {'G','U','M','I','X','s','1','\0'}; + out.insert(out.end(), magic, magic + 8); + append_le(out, static_cast(layers.size()*2)); + append_le(out, 0); + for (const LayerCalibration & layer : layers) { + if (layer.gate_up.levels != kGuLevels || layer.gate_up.experts.size() != experts) { + fail("incomplete qtype-106 codebook registry at layer " + std::to_string(layer.layer)); + } + for (Surface surface : {Surface::Gate, Surface::Up}) { + append_le(out, static_cast(layer.layer)); + append_le(out, static_cast(surface)); + append_le(out, experts); + append_le(out, layer.gate_up_shape.out); + append_le(out, layer.gate_up_shape.in); + append_le(out, kCodebooks); + append_le(out, kGuLevels); + out.insert(out.end(), experts, 1u); + for (const auto & book : layer.gate_up.experts) { + if (book.size() != kCodebooks*kGuLevels) fail("bad qtype-106 codebook length"); + for (uint16_t value : book) append_le(out, value); + } + } + } + return out; +} + +void write_atomic_bytes(const fs::path & path, const std::vector & bytes, bool force) { + if (!force && fs::exists(path)) fail("output exists: " + path.string()); + const fs::path temporary = path.string() + ".partial"; + if (fs::exists(temporary)) fs::remove(temporary); + std::ofstream out(temporary, std::ios::binary | std::ios::trunc); + if (!out) fail("cannot create " + temporary.string()); + out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); + out.close(); + if (!out) fail("failed writing " + temporary.string()); + if (force && fs::exists(path)) fs::remove(path); + fs::rename(temporary, path); +} + +enum class Producer { Raw, DenseFp8, Int64ToInt32, Expert }; + +struct TensorSpec { + std::string name; + ggml_type type = GGML_TYPE_COUNT; + std::vector ne; + Producer producer = Producer::Raw; + const StEntry * source = nullptr; + const StEntry * scale = nullptr; + int layer = -1; + const ExpertRecipe * recipe = nullptr; +}; + +std::vector reverse_shape(const StEntry & entry) { + std::vector ne(entry.shape.rbegin(), entry.shape.rend()); + while (ne.size() < GGML_MAX_DIMS) ne.push_back(1); + return ne; +} + +ggml_type direct_ggml_type(const std::string & dtype) { + if (dtype == "BF16") return GGML_TYPE_BF16; + if (dtype == "F16") return GGML_TYPE_F16; + if (dtype == "F32") return GGML_TYPE_F32; + if (dtype == "I32") return GGML_TYPE_I32; + fail("unsupported direct tensor dtype " + dtype); +} + +const std::unordered_map kLayerNameMap = { + {"attn.attn_sink", "attn_sinks.weight"}, + {"attn.kv_norm.weight", "attn_kv_a_norm.weight"}, + {"attn.q_norm.weight", "attn_q_a_norm.weight"}, + {"attn.wkv.weight", "attn_kv.weight"}, + {"attn.wo_a.weight", "attn_output_a.weight"}, + {"attn.wo_b.weight", "attn_output_b.weight"}, + {"attn.wq_a.weight", "attn_q_a.weight"}, + {"attn.wq_b.weight", "attn_q_b.weight"}, + {"attn_norm.weight", "attn_norm.weight"}, + {"attn.compressor.ape", "attn_compressor_ape.weight"}, + {"attn.compressor.norm.weight", "attn_compressor_norm.weight"}, + {"attn.compressor.wgate.weight", "attn_compressor_gate.weight"}, + {"attn.compressor.wkv.weight", "attn_compressor_kv.weight"}, + {"attn.indexer.compressor.ape", "indexer_compressor_ape.weight"}, + {"attn.indexer.compressor.norm.weight", "indexer_compressor_norm.weight"}, + {"attn.indexer.compressor.wgate.weight", "indexer_compressor_gate.weight"}, + {"attn.indexer.compressor.wkv.weight", "indexer_compressor_kv.weight"}, + {"attn.indexer.weights_proj.weight", "indexer.proj.weight"}, + {"attn.indexer.wq_b.weight", "indexer.attn_q_b.weight"}, + {"ffn.gate.bias", "exp_probs_b.bias"}, + {"ffn.gate.tid2eid", "ffn_gate_tid2eid.weight"}, + {"ffn.gate.weight", "ffn_gate_inp.weight"}, + {"ffn.shared_experts.w1.weight", "ffn_gate_shexp.weight"}, + {"ffn.shared_experts.w2.weight", "ffn_down_shexp.weight"}, + {"ffn.shared_experts.w3.weight", "ffn_up_shexp.weight"}, + {"ffn_norm.weight", "ffn_norm.weight"}, + {"hc_attn_base", "hc_attn_base.weight"}, + {"hc_attn_fn", "hc_attn_fn.weight"}, + {"hc_attn_scale", "hc_attn_scale.weight"}, + {"hc_ffn_base", "hc_ffn_base.weight"}, + {"hc_ffn_fn", "hc_ffn_fn.weight"}, + {"hc_ffn_scale", "hc_ffn_scale.weight"}, +}; + +const std::unordered_map kGlobalNameMap = { + {"embed.weight", "token_embd.weight"}, + {"head.weight", "output.weight"}, + {"norm.weight", "output_norm.weight"}, + {"hc_head_base", "output_hc_base.weight"}, + {"hc_head_fn", "output_hc_fn.weight"}, + {"hc_head_scale", "output_hc_scale.weight"}, +}; + +bool starts_with(const std::string & value, const std::string & prefix) { + return value.size() >= prefix.size() && value.compare(0, prefix.size(), prefix) == 0; +} +bool ends_with(const std::string & value, const std::string & suffix) { + return value.size() >= suffix.size() && value.compare(value.size() - suffix.size(), suffix.size(), suffix) == 0; +} + +std::optional> parse_layer_name(const std::string & name) { + if (!starts_with(name, "layers.")) return std::nullopt; + const size_t dot = name.find('.', 7); + if (dot == std::string::npos) fail("malformed layer tensor name " + name); + const std::string number = name.substr(7, dot - 7); + if (number.empty() || number.find_first_not_of("0123456789") != std::string::npos) { + fail("malformed layer index in " + name); + } + return std::make_pair(std::stoi(number), name.substr(dot + 1)); +} + +TensorSpec mapped_source_spec(const std::string & target, const StEntry & source, + const SafeTensorSet & all) { + TensorSpec spec; + spec.name = target; + spec.source = &source; + if (source.dtype == "I64") { + if (!ends_with(source.name, ".tid2eid")) fail("unsupported I64 tensor " + source.name); + spec.type = GGML_TYPE_I32; + spec.producer = Producer::Int64ToInt32; + } else if (source.dtype == "F8_E4M3") { + if (!ends_with(source.name, ".weight")) fail("FP8 tensor is not a weight: " + source.name); + const std::string scale_name = source.name.substr(0, source.name.size() - 6) + "scale"; + const StEntry & scale = all.at(scale_name); + if (scale.dtype != "F8_E8M0" || source.shape.size() != 2 || scale.shape.size() != 2) { + fail("unsupported FP8 tensor/scale pair " + source.name); + } + const std::vector expected = { + (source.shape[0] + 127)/128, (source.shape[1] + 127)/128, + }; + if (scale.shape != expected) fail("FP8 scale shape mismatch for " + source.name); + spec.type = GGML_TYPE_BF16; + spec.producer = Producer::DenseFp8; + spec.scale = &scale; + } else { + spec.type = direct_ggml_type(source.dtype); + spec.producer = Producer::Raw; + } + spec.ne = reverse_shape(source); + if (spec.name.size() >= GGML_MAX_NAME) fail("GGUF tensor name too long: " + spec.name); + return spec; +} + +std::vector make_plan( + const SafeTensorSet & source, const std::vector & layers, + uint32_t experts, bool experts_only) { + std::vector plan; + std::set names; + auto add = [&](TensorSpec spec) { + if (!names.insert(spec.name).second) fail("duplicate output tensor name " + spec.name); + plan.push_back(std::move(spec)); + }; + + if (!experts_only) { + for (const auto & mapping : kGlobalNameMap) { + add(mapped_source_spec(mapping.second, source.at(mapping.first), source)); + } + } + + for (const LayerCalibration & layer : layers) { + if (!experts_only) { + const std::string prefix = "layers." + std::to_string(layer.layer) + "."; + for (const auto & mapping : kLayerNameMap) { + const std::string src = prefix + mapping.first; + if (!source.contains(src)) continue; + if (ends_with(mapping.first, ".scale")) continue; + add(mapped_source_spec("blk." + std::to_string(layer.layer) + "." + mapping.second, + source.at(src), source)); + } + } + for (const ExpertRecipe & recipe : kExpertRecipes) { + const TensorShape shape = recipe.books == BookSource::GateUpJoint + ? layer.gate_up_shape : layer.down_shape; + TensorSpec spec; + spec.name = target_expert_name(layer.layer, recipe); + spec.type = recipe.qtype; + spec.ne = {shape.in, shape.out, static_cast(experts), 1}; + spec.producer = Producer::Expert; + spec.layer = layer.layer; + spec.recipe = &recipe; + add(std::move(spec)); + } + } + + if (!experts_only) { + bool dropped_mtp = false; + static const std::regex expert_leaf( + R"(^ffn\.experts\.[0-9]+\.(w1|w2|w3)\.(weight|scale)$)"); + for (const auto & item : source.entries()) { + const std::string & name = item.first; + if (starts_with(name, "vision.") || starts_with(name, "aligner.") || starts_with(name, "image_")) { + TensorSpec spec = mapped_source_spec(name, item.second, source); + if (spec.producer != Producer::Raw) fail("vision pass-through is not byte-lossless for " + name); + add(std::move(spec)); + continue; + } + if (starts_with(name, "mtp.")) { + dropped_mtp = true; + continue; + } + if (kGlobalNameMap.count(name)) continue; + const auto parsed = parse_layer_name(name); + if (!parsed) fail("unsupported top-level tensor " + name); + const int layer = parsed->first; + const std::string & leaf = parsed->second; + const bool selected = std::any_of(layers.begin(), layers.end(), + [layer](const LayerCalibration & c) { return c.layer == layer; }); + if (!selected) continue; + if (leaf == "ffn.gate.bias_vl") { + TensorSpec spec = mapped_source_spec(name, item.second, source); + if (spec.producer != Producer::Raw) fail("bias_vl pass-through is not byte-lossless for " + name); + add(std::move(spec)); + continue; + } + if (starts_with(leaf, "ffn.experts.")) { + if (!std::regex_match(leaf, expert_leaf)) fail("unsupported expert tensor " + name); + continue; + } + if (kLayerNameMap.count(leaf) || (ends_with(leaf, ".scale") && + source.contains(name.substr(0, name.size() - 5) + "weight"))) continue; + fail("unsupported selected-layer tensor " + name); + } + if (dropped_mtp) { + std::cerr << "[plan] NOTE: mtp.* predictor tensors are intentionally omitted; " + << "the DeepSeek4 target loader does not consume them\n"; + } + } + + std::sort(plan.begin(), plan.end(), [](const TensorSpec & a, const TensorSpec & b) { + return a.name < b.name; + }); + return plan; +} + +void set_model_metadata(gguf_context * ctx, const SafeTensorSet & source, + uint32_t layers, uint32_t experts, bool absmax_only, + bool smoke_artifact, const std::vector & p4_blob) { + const json & c = source.config(); + gguf_set_val_str(ctx, "general.architecture", "deepseek4"); + gguf_set_val_str(ctx, "general.name", "DeepSeek-V4-Flash-Vision-Uncensored MIX"); + gguf_set_val_u32(ctx, "general.alignment", kAlignment); + gguf_set_val_u32(ctx, "general.file_type", 119); + gguf_set_val_str(ctx, "deepseek4.mix.calibration", + absmax_only ? "absmax-only (LOWER QUALITY; no imatrix)" : "importance-matrix weighted"); + gguf_set_val_bool(ctx, "deepseek4.mix.lower_quality_absmax_only", absmax_only); + gguf_set_val_bool(ctx, "deepseek4.mix.experts_only_smoke_artifact", smoke_artifact); + + gguf_set_val_u32(ctx, "deepseek4.block_count", layers); + gguf_set_val_u32(ctx, "deepseek4.embedding_length", config_u32(c, "hidden_size")); + gguf_set_val_u32(ctx, "deepseek4.vocab_size", config_u32(c, "vocab_size")); + gguf_set_val_u32(ctx, "deepseek4.attention.head_count", config_u32(c, "num_attention_heads")); + gguf_set_val_u32(ctx, "deepseek4.attention.head_count_kv", config_u32(c, "num_key_value_heads")); + gguf_set_val_u32(ctx, "deepseek4.attention.key_length", config_u32(c, "head_dim")); + gguf_set_val_u32(ctx, "deepseek4.rope.dimension_count", config_u32(c, "qk_rope_head_dim")); + gguf_set_val_u32(ctx, "deepseek4.attention.q_lora_rank", config_u32(c, "q_lora_rank")); + gguf_set_val_u32(ctx, "deepseek4.attention.output_lora_rank", config_u32(c, "o_lora_rank")); + gguf_set_val_u32(ctx, "deepseek4.attention.output_group_count", config_u32(c, "o_groups")); + gguf_set_val_u32(ctx, "deepseek4.expert_count", experts); + gguf_set_val_u32(ctx, "deepseek4.expert_used_count", std::min(experts, config_u32(c, "num_experts_per_tok"))); + gguf_set_val_u32(ctx, "deepseek4.expert_shared_count", config_u32(c, "n_shared_experts")); + gguf_set_val_u32(ctx, "deepseek4.expert_feed_forward_length", config_u32(c, "moe_intermediate_size")); + gguf_set_val_u32(ctx, "deepseek4.hash_layer_count", std::min(layers, config_u32(c, "num_hash_layers"))); + gguf_set_val_u32(ctx, "deepseek4.attention.sliding_window", config_u32(c, "sliding_window")); + gguf_set_val_u32(ctx, "deepseek4.attention.indexer.head_count", config_u32(c, "index_n_heads")); + gguf_set_val_u32(ctx, "deepseek4.attention.indexer.key_length", config_u32(c, "index_head_dim")); + gguf_set_val_u32(ctx, "deepseek4.attention.indexer.top_k", config_u32(c, "index_topk")); + gguf_set_val_u32(ctx, "deepseek4.hyper_connection.count", config_u32(c, "hc_mult")); + gguf_set_val_u32(ctx, "deepseek4.hyper_connection.sinkhorn_iterations", config_u32(c, "hc_sinkhorn_iters")); + + gguf_set_val_f32(ctx, "deepseek4.rope.freq_base", config_f32(c, "rope_theta", 10000.0f)); + gguf_set_val_f32(ctx, "deepseek4.rope.scaling.factor", 16.0f); + gguf_set_val_f32(ctx, "deepseek4.rope.scaling.yarn_beta_fast", 32.0f); + gguf_set_val_f32(ctx, "deepseek4.rope.scaling.yarn_beta_slow", 1.0f); + gguf_set_val_f32(ctx, "deepseek4.attention.compress_rope_freq_base", config_f32(c, "compress_rope_theta", 160000.0f)); + gguf_set_val_u64(ctx, "deepseek4.rope.scaling.original_context_length", 65536); + gguf_set_val_f32(ctx, "deepseek4.attention.layer_norm_rms_epsilon", 1e-6f); + gguf_set_val_f32(ctx, "deepseek4.hyper_connection.epsilon", config_f32(c, "hc_eps", 1e-6f)); + gguf_set_val_f32(ctx, "deepseek4.expert_weights_scale", config_f32(c, "routed_scaling_factor", 1.5f)); + gguf_set_val_f32(ctx, "deepseek4.swiglu_clamp_exp", config_f32(c, "swiglu_limit", 10.0f)); + + std::vector ratios(layers, 0); + if (c.contains("compress_ratios") && c["compress_ratios"].is_array()) { + if (c["compress_ratios"].size() < layers) fail("config compress_ratios is shorter than selected layers"); + for (uint32_t i = 0; i < layers; ++i) ratios[i] = c["compress_ratios"][i].get(); + } + gguf_set_arr_data(ctx, "deepseek4.attention.compress_ratios", GGUF_TYPE_UINT32, + ratios.data(), ratios.size()); + + gguf_set_val_str(ctx, "tokenizer.ggml.model", "gpt2"); + gguf_set_val_str(ctx, "tokenizer.ggml.pre", "deepseek-v3"); + gguf_set_val_u32(ctx, "tokenizer.ggml.bos_token_id", c.value("bos_token_id", 0u)); + gguf_set_val_u32(ctx, "tokenizer.ggml.eos_token_id", c.value("eos_token_id", 1u)); + gguf_set_val_u32(ctx, "tokenizer.ggml.padding_token_id", c.value("eos_token_id", 1u)); + gguf_set_val_bool(ctx, "tokenizer.ggml.add_bos_token", false); + gguf_set_val_bool(ctx, "tokenizer.ggml.add_eos_token", false); + + const uint32_t vocab_size = config_u32(c, "vocab_size"); + std::vector tokens(vocab_size); + std::vector types(vocab_size, 1); + std::vector scores(vocab_size, 0.0f); + std::vector assigned(vocab_size, false); + const json & tok = source.tokenizer(); + if (!tok.contains("model") || !tok["model"].contains("vocab") || + !tok["model"].contains("merges")) fail("tokenizer.json is missing BPE model fields"); + for (const auto & item : tok["model"]["vocab"].items()) { + const int64_t id = item.value().get(); + if (id < 0 || id >= vocab_size) fail("tokenizer vocab id out of range"); + tokens[id] = item.key(); + assigned[id] = true; + } + if (tok.contains("added_tokens")) { + for (const json & added : tok["added_tokens"]) { + const int64_t id = added.at("id").get(); + if (id < 0 || id >= vocab_size) fail("added token id out of range"); + tokens[id] = added.at("content").get(); + types[id] = added.value("special", true) ? 3 : 4; + assigned[id] = true; + } + } + for (uint32_t i = 0; i < vocab_size; ++i) { + if (!assigned[i]) fail("tokenizer has no token for id " + std::to_string(i)); + } + std::vector token_ptrs; + token_ptrs.reserve(tokens.size()); + for (const std::string & token : tokens) token_ptrs.push_back(token.c_str()); + gguf_set_arr_str(ctx, "tokenizer.ggml.tokens", token_ptrs.data(), token_ptrs.size()); + gguf_set_arr_data(ctx, "tokenizer.ggml.scores", GGUF_TYPE_FLOAT32, scores.data(), scores.size()); + gguf_set_arr_data(ctx, "tokenizer.ggml.token_type", GGUF_TYPE_INT32, types.data(), types.size()); + + std::vector merges = tok["model"]["merges"].get>(); + std::vector merge_ptrs; + merge_ptrs.reserve(merges.size()); + for (const std::string & merge : merges) merge_ptrs.push_back(merge.c_str()); + gguf_set_arr_str(ctx, "tokenizer.ggml.merges", merge_ptrs.data(), merge_ptrs.size()); + + gguf_set_arr_data(ctx, "deepseek4.p4mix.sidecar", GGUF_TYPE_UINT8, + p4_blob.data(), p4_blob.size()); +} + +std::unique_ptr make_tensor_descriptor(const TensorSpec & spec) { + auto tensor = std::make_unique(); + std::memset(tensor.get(), 0, sizeof(*tensor)); + tensor->type = spec.type; + for (int i = 0; i < GGML_MAX_DIMS; ++i) tensor->ne[i] = i < static_cast(spec.ne.size()) ? spec.ne[i] : 1; + const int64_t block = ggml_blck_size(spec.type); + if (block <= 0 || tensor->ne[0] % block != 0) { + fail("tensor " + spec.name + " ne[0] violates qtype block size"); + } + tensor->nb[0] = ggml_type_size(spec.type); + tensor->nb[1] = tensor->nb[0]*static_cast(tensor->ne[0]/block); + for (int i = 2; i < GGML_MAX_DIMS; ++i) tensor->nb[i] = tensor->nb[i - 1]*static_cast(tensor->ne[i - 1]); + std::snprintf(tensor->name, sizeof(tensor->name), "%s", spec.name.c_str()); + return tensor; +} + +void fwrite_exact(FILE * out, const void * data, size_t n, const std::string & what) { + if (n && std::fwrite(data, 1, n, out) != n) fail("failed writing " + what); +} + +void copy_raw(FILE * out, const StEntry & source) { + FileDescriptor fd(source.path); + std::vector buffer(std::min(source.size, kCopyChunk)); + uint64_t done = 0; + while (done < source.size) { + const size_t n = static_cast(std::min(buffer.size(), source.size - done)); + pread_exact(fd.fd, buffer.data(), n, source.offset + done, source.name); + fwrite_exact(out, buffer.data(), n, source.name); + done += n; + } +} + +void write_int64_to_int32(FILE * out, const StEntry & source) { + FileDescriptor fd(source.path); + const uint64_t count = element_count(source.shape, source.name); + std::vector input(std::min(count, kCopyChunk/sizeof(int64_t))); + std::vector output(input.size()); + uint64_t done = 0; + while (done < count) { + const size_t n = static_cast(std::min(input.size(), count - done)); + pread_exact(fd.fd, input.data(), n*sizeof(int64_t), source.offset + done*sizeof(int64_t), source.name); + for (size_t i = 0; i < n; ++i) { + if (input[i] < INT32_MIN || input[i] > INT32_MAX) fail("I64 routing id out of I32 range in " + source.name); + output[i] = static_cast(input[i]); + } + fwrite_exact(out, output.data(), n*sizeof(int32_t), source.name); + done += n; + } +} + +void write_dense_fp8(FILE * out, const StEntry & weight, const StEntry & scale) { + const uint32_t rows = static_cast(weight.shape[0]); + const uint32_t cols = static_cast(weight.shape[1]); + FileDescriptor wf(weight.path), sf(scale.path); + std::vector scales(scale.size); + pread_exact(sf.fd, scales.data(), scales.size(), scale.offset, scale.name); + std::vector input(cols); + std::vector output(cols); + const uint32_t scale_cols = static_cast(scale.shape[1]); + for (uint32_t row = 0; row < rows; ++row) { + pread_exact(wf.fd, input.data(), input.size(), weight.offset + static_cast(row)*cols, weight.name); + for (uint32_t col = 0; col < cols; ++col) { + const uint8_t scale_byte = scales[(row/128)*scale_cols + col/128]; + const float decoded = fp8_e4m3fn(input[col])*fp8_e8m0(scale_byte); + const uint16_t b = float_to_bf16(decoded); + if (bf16_to_float(b) != decoded) { + fail("FP8->BF16 is not exact for " + weight.name + " at row " + + std::to_string(row) + " col " + std::to_string(col)); + } + output[col] = b; + } + fwrite_exact(out, output.data(), output.size()*sizeof(uint16_t), weight.name); + } +} + +const LayerCalibration & find_calibration(const std::vector & all, int layer) { + const auto it = std::find_if(all.begin(), all.end(), [layer](const LayerCalibration & c) { return c.layer == layer; }); + if (it == all.end()) fail("internal error: missing calibration for layer " + std::to_string(layer)); + return *it; +} + +void write_expert_tensor(FILE * out, const SafeTensorSet & source, + const LayerCalibration & calibration, uint32_t experts, + const ExpertRecipe & recipe, const std::optional & imatrix) { + const TensorShape expected = recipe.books == BookSource::GateUpJoint + ? calibration.gate_up_shape : calibration.down_shape; + const CodebookRegistry & registry = recipe.books == BookSource::GateUpJoint + ? calibration.gate_up : calibration.down; + const std::string target = target_expert_name(calibration.layer, recipe); + const std::vector * importance = require_imatrix(imatrix, target, expected.in); + std::vector packed, scales; + std::vector values; + std::vector q2(expected.in/kBlock); + std::vector q3(expected.in/kBlock); + for (uint32_t expert = 0; expert < experts; ++expert) { + const TensorShape shape = validate_expert_source(source, calibration.layer, expert, recipe); + if (shape.in != expected.in || shape.out != expected.out) fail("expert shape drift in " + target); + const StEntry & w = source.at(source_expert_name(calibration.layer, expert, recipe, "weight")); + const StEntry & s = source.at(source_expert_name(calibration.layer, expert, recipe, "scale")); + OpenTensorPair input(w, s); + const auto & books = registry.experts.at(expert); + for (uint32_t row = 0; row < shape.out; ++row) { + decode_expert_row(input, row, shape.in, packed, scales, values); + if (recipe.qtype == GGML_TYPE_Q2_1_ROCMFP2_MIX) { + if (!rocmfpx_quantize_row_fp2_mix_ref(values.data(), q2.data(), shape.in, + books.data(), importance ? importance->data() : nullptr)) { + fail("qtype-106 reference encoder rejected " + w.name); + } + fwrite_exact(out, q2.data(), q2.size()*sizeof(q2[0]), target); + } else if (recipe.qtype == GGML_TYPE_Q3_1_ROCMFP3_MIX) { + if (!rocmfpx_quantize_row_fp3_mix_ref(values.data(), q3.data(), shape.in, + books.data(), importance ? importance->data() : nullptr)) { + fail("qtype-105 reference encoder rejected " + w.name); + } + fwrite_exact(out, q3.data(), q3.size()*sizeof(q3[0]), target); + } else { + fail("recipe table contains unsupported qtype"); + } + } + std::cerr << "[encode] " << target << " expert " << (expert + 1) << "/" << experts << "\n"; + } +} + +std::vector validate_input_layout( + const SafeTensorSet & source, uint32_t layers, uint32_t experts) { + std::vector result; + result.reserve(layers); + for (uint32_t layer = 0; layer < layers; ++layer) { + LayerCalibration current; + current.layer = static_cast(layer); + for (uint32_t expert = 0; expert < experts; ++expert) { + for (const ExpertRecipe & recipe : kExpertRecipes) { + const TensorShape shape = validate_expert_source(source, layer, expert, recipe); + TensorShape & expected = recipe.books == BookSource::GateUpJoint + ? current.gate_up_shape : current.down_shape; + if (expected.in == 0) expected = shape; + if (shape.in != expected.in || shape.out != expected.out) { + fail("expert shape drift during input validation at layer " + std::to_string(layer)); + } + } + } + result.push_back(std::move(current)); + } + return result; +} + +std::vector calibrate( + const SafeTensorSet & source, uint32_t layers, uint32_t experts, + const std::optional & imatrix) { + std::vector result; + result.reserve(layers); + for (uint32_t layer = 0; layer < layers; ++layer) { + LayerCalibration current; + current.layer = static_cast(layer); + current.gate_up.levels = kGuLevels; + current.down.levels = kP4Levels; + for (uint32_t expert = 0; expert < experts; ++expert) { + HistogramFitter gate_up_fitter; + TensorShape gate_shape{}; + for (const ExpertRecipe & recipe : kExpertRecipes) { + if (recipe.books != BookSource::GateUpJoint) continue; + const TensorShape shape = validate_expert_source(source, layer, expert, recipe); + if (gate_shape.in == 0) gate_shape = shape; + if (shape.in != gate_shape.in || shape.out != gate_shape.out) { + fail("qtype-106 gate/up shape mismatch at layer " + std::to_string(layer)); + } + const std::string target = target_expert_name(layer, recipe); + const auto * importance = require_imatrix(imatrix, target, shape.in); + add_expert_to_fitter(source, layer, expert, recipe, importance, gate_up_fitter); + } + HistogramFitter down_fitter; + const ExpertRecipe & down_recipe = kExpertRecipes[2]; + const TensorShape down_shape = validate_expert_source(source, layer, expert, down_recipe); + const auto * down_importance = require_imatrix( + imatrix, target_expert_name(layer, down_recipe), down_shape.in); + add_expert_to_fitter(source, layer, expert, down_recipe, down_importance, down_fitter); + + if (current.gate_up_shape.in == 0) current.gate_up_shape = gate_shape; + if (current.down_shape.in == 0) current.down_shape = down_shape; + if (current.gate_up_shape.in != gate_shape.in || current.gate_up_shape.out != gate_shape.out || + current.down_shape.in != down_shape.in || current.down_shape.out != down_shape.out) { + fail("expert shapes vary within layer " + std::to_string(layer)); + } + current.gate_up.experts.push_back(gate_up_fitter.fit(kGuLevels)); + current.down.experts.push_back(down_fitter.fit(kP4Levels)); + std::cerr << "[calibration] layer " << layer << " expert " << (expert + 1) + << "/" << experts << " fitted joint gate/up and down codebooks\n"; + } + result.push_back(std::move(current)); + } + return result; +} + +void compare_raw_passthrough(int output_fd, uint64_t output_offset, const StEntry & input) { + FileDescriptor source_fd(input.path); + std::vector a(std::min(input.size, kCopyChunk)); + std::vector b(a.size()); + uint64_t done = 0; + while (done < input.size) { + const size_t n = static_cast(std::min(a.size(), input.size - done)); + pread_exact(source_fd.fd, a.data(), n, input.offset + done, input.name + " source verification"); + pread_exact(output_fd, b.data(), n, output_offset + done, input.name + " output verification"); + if (std::memcmp(a.data(), b.data(), n) != 0) fail("lossless pass-through verification failed for " + input.name); + done += n; + } +} + +std::vector read_file(const fs::path & path) { + std::ifstream in(path, std::ios::binary | std::ios::ate); + if (!in) fail("cannot open " + path.string()); + const auto end = in.tellg(); + if (end < 0) fail("cannot size " + path.string()); + std::vector out(static_cast(end)); + in.seekg(0); + if (!out.empty() && !in.read(reinterpret_cast(out.data()), out.size())) fail("cannot read " + path.string()); + return out; +} + +void verify_artifact(const fs::path & output, const fs::path & gumix_path, + const std::vector & plan, + const std::vector & expected_p4, + const std::vector & expected_gumix) { + ggml_context * meta = nullptr; + gguf_init_params params{}; + params.no_alloc = true; + params.ctx = &meta; + gguf_context * ctx = gguf_init_from_file(output.c_str(), params); + if (!ctx) fail("GGUF verification could not parse " + output.string()); + const uint64_t file_size = fs::file_size(output); + const uint64_t data_offset = gguf_get_data_offset(ctx); + const int64_t p4_key = gguf_find_key(ctx, "deepseek4.p4mix.sidecar"); + if (p4_key < 0 || gguf_get_kv_type(ctx, p4_key) != GGUF_TYPE_ARRAY || + gguf_get_arr_type(ctx, p4_key) != GGUF_TYPE_UINT8 || + gguf_get_arr_n(ctx, p4_key) != expected_p4.size() || + std::memcmp(gguf_get_arr_data(ctx, p4_key), expected_p4.data(), expected_p4.size()) != 0) { + fail("embedded deepseek4.p4mix.sidecar verification failed"); + } + if (read_file(gumix_path) != expected_gumix) fail("qtype-106 gumix sidecar verification failed"); + + FileDescriptor output_fd(output); + for (const TensorSpec & spec : plan) { + const int64_t id = gguf_find_tensor(ctx, spec.name.c_str()); + if (id < 0) fail("output is missing tensor " + spec.name); + if (gguf_get_tensor_type(ctx, id) != spec.type) fail("output qtype mismatch for " + spec.name); + const uint64_t offset = data_offset + gguf_get_tensor_offset(ctx, id); + const uint64_t size = gguf_get_tensor_size(ctx, id); + if (offset > file_size || size > file_size - offset) fail("output tensor is out of file bounds: " + spec.name); + if (spec.producer == Producer::Raw) compare_raw_passthrough(output_fd.fd, offset, *spec.source); + } + if (gguf_get_n_tensors(ctx) != static_cast(plan.size())) fail("unexpected tensor count in output"); + gguf_free(ctx); + if (meta) ggml_free(meta); + std::cerr << "[verify] parsed GGUF, checked qtypes/bounds, exact sidecars, and raw pass-through bytes\n"; +} + +void write_gguf(const Options & options, const SafeTensorSet & source, + const std::vector & calibration, + uint32_t layers, uint32_t experts, const std::optional & imatrix) { + const fs::path gumix_path = options.output.string() + ".gumix.bin"; + if (!options.force && (fs::exists(options.output) || fs::exists(gumix_path))) { + fail("output or qtype-106 sidecar exists: " + options.output.string()); + } + if (!options.output.parent_path().empty()) fs::create_directories(options.output.parent_path()); + const fs::path temporary = options.output.string() + ".partial"; + if (fs::exists(temporary)) fs::remove(temporary); + + const std::vector p4 = make_p4_blob(calibration, experts); + const std::vector gumix = make_gumix_blob(calibration, experts); + const std::vector plan = make_plan(source, calibration, experts, options.experts_only); + + gguf_context * ctx = gguf_init_empty(); + if (!ctx) fail("gguf_init_empty failed"); + set_model_metadata(ctx, source, layers, experts, options.absmax_only, + options.experts_only, p4); + std::vector> descriptors; + descriptors.reserve(plan.size()); + for (const TensorSpec & spec : plan) { + descriptors.push_back(make_tensor_descriptor(spec)); + gguf_add_tensor(ctx, descriptors.back().get()); + } + if (!gguf_write_to_file(ctx, temporary.c_str(), true)) fail("failed writing GGUF metadata"); + FILE * out = std::fopen(temporary.c_str(), "ab"); + if (!out) fail("cannot append tensor data to " + temporary.string()); + const off_t initial_position = ::ftello(out); + if (initial_position < 0) fail("cannot determine GGUF data offset"); + const uint64_t data_offset = static_cast(initial_position); + for (const TensorSpec & spec : plan) { + const int64_t id = gguf_find_tensor(ctx, spec.name.c_str()); + if (id < 0) fail("internal GGUF tensor lookup failed for " + spec.name); + const uint64_t expected = data_offset + gguf_get_tensor_offset(ctx, id); + const off_t position = ::ftello(out); + if (position < 0 || static_cast(position) > expected) { + fail("GGUF stream offset overran " + spec.name + " (position=" + + std::to_string(position) + " expected=" + std::to_string(expected) + ")"); + } + std::array zeros{}; + uint64_t gap = expected - static_cast(position); + while (gap) { + const size_t n = static_cast(std::min(gap, zeros.size())); + fwrite_exact(out, zeros.data(), n, "GGUF alignment"); + gap -= n; + } + const off_t before = ::ftello(out); + if (spec.producer == Producer::Raw) { + copy_raw(out, *spec.source); + } else if (spec.producer == Producer::DenseFp8) { + write_dense_fp8(out, *spec.source, *spec.scale); + } else if (spec.producer == Producer::Int64ToInt32) { + write_int64_to_int32(out, *spec.source); + } else { + write_expert_tensor(out, source, find_calibration(calibration, spec.layer), + experts, *spec.recipe, imatrix); + } + const off_t after = ::ftello(out); + if (before < 0 || after < before || static_cast(after - before) != gguf_get_tensor_size(ctx, id)) { + fail("producer wrote wrong byte count for " + spec.name); + } + } + const bool flush_failed = std::fflush(out) != 0; + const bool sync_failed = !flush_failed && ::fsync(::fileno(out)) != 0; + const bool close_failed = std::fclose(out) != 0; + if (flush_failed || sync_failed || close_failed) { + fail("failed finalizing " + temporary.string()); + } + gguf_free(ctx); + + write_atomic_bytes(gumix_path, gumix, options.force); + if (options.force && fs::exists(options.output)) fs::remove(options.output); + fs::rename(temporary, options.output); + verify_artifact(options.output, gumix_path, plan, p4, gumix); +} + +} // namespace + +int main(int argc, char ** argv) { + try { + static_assert(static_cast(GGML_TYPE_Q3_1_ROCMFP3_MIX) == static_cast(kQtypeP4Mix)); + static_assert(static_cast(GGML_TYPE_Q2_1_ROCMFP2_MIX) == static_cast(kQtypeGuMix)); + const Options options = parse_options(argc, argv); + if (options.layer_start != 0) fail("loader-compatible partial artifacts must start at layer 0"); + SafeTensorSet source(options.input); + const uint32_t source_layers = config_u32(source.config(), "num_hidden_layers"); + const uint32_t source_experts = config_u32(source.config(), "n_routed_experts"); + const uint32_t layers = options.layer_count < 0 ? source_layers : static_cast(options.layer_count); + const uint32_t experts = options.expert_limit < 0 ? source_experts : static_cast(options.expert_limit); + if (layers == 0 || layers > source_layers || experts == 0 || experts > source_experts) { + fail("selected layer/expert range exceeds config.json"); + } + if (!options.experts_only && (layers != source_layers || experts != source_experts)) { + fail("layer/expert limits are permitted only with --experts-only smoke artifacts"); + } + if (options.validate_input_only) { + const auto layout = validate_input_layout(source, layers, experts); + const auto plan = make_plan(source, layout, experts, options.experts_only); + std::cerr << "[validate-input] PASS: " << plan.size() + << " loader and pass-through tensors; all expert sources/dtypes/shapes validated\n"; + return 0; + } + std::optional imatrix; + if (options.imatrix) { + imatrix = load_imatrix(*options.imatrix); + } else { + std::cerr << "[calibration] WARNING: ABSMAX-ONLY LOWER QUALITY mode explicitly selected; no imatrix weighting\n"; + } + std::cerr << "[plan] CPU-only conversion layers=0.." << (layers - 1) + << " experts=0.." << (experts - 1) + << (options.experts_only ? " experts-only smoke artifact" : " complete text+vision artifact") << "\n"; + const auto calibration = calibrate(source, layers, experts, imatrix); + write_gguf(options, source, calibration, layers, experts, imatrix); + std::cerr << "[done] " << options.output << "\n"; + return 0; + } catch (const std::exception & e) { + std::cerr << "ds4_mix_converter: ERROR: " << e.what() << "\n"; + return 1; + } +} From a53acc9a3946a46080b3026c861304ed3e526b65 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 17:14:25 -0400 Subject: [PATCH 002/123] test(ds4v): reproduce collapsed fitter and repair clean-build test wiring --- server/CMakeLists.txt | 2 +- server/test/test_ds4_mix_fitter.cpp | 42 +++++++++++++++++++ server/test/test_ds4v_mix_converter.cpp | 6 +-- server/tools/ds4_mix_converter/CMakeLists.txt | 8 +++- 4 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 server/test/test_ds4_mix_fitter.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index fea386e5d..7db269d12 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -963,7 +963,7 @@ if(DFLASH27B_TESTS) endif() list(APPEND _raw_unit_test_targets test_rocmfpx) - add_executable(test_ds4_mix_converter test/test_ds4_mix_converter.cpp) + add_executable(test_ds4_mix_converter test/test_ds4v_mix_converter.cpp test/test_unit_main.cpp) target_include_directories(test_ds4_mix_converter PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/rocmfpx) diff --git a/server/test/test_ds4_mix_fitter.cpp b/server/test/test_ds4_mix_fitter.cpp new file mode 100644 index 000000000..00260dfc5 --- /dev/null +++ b/server/test/test_ds4_mix_fitter.cpp @@ -0,0 +1,42 @@ +#define main ds4_mix_converter_main +#include "../tools/ds4_mix_converter/ds4_mix_converter.cpp" +#undef main + +int main() { + try { + for (float value : {0.0f, -1.0f, 1.0f}) { + HistogramFitter fitter; + float values[16]; + std::fill_n(values, 16, value); + fitter.add_half(values, nullptr); + for (int levels : {4, 8}) { + const auto books = fitter.fit(levels); + if (books != fitter.fit(levels)) fail("fit is nondeterministic"); + for (int population = 0; population < 2; ++population) { + float previous = -std::numeric_limits::infinity(); + for (int i = 0; i < levels; ++i) { + float v = bf16_to_float(books[population*levels+i]); + if (!std::isfinite(v) || v < -1 || v > 1 || v <= previous) + fail("codebook is not finite, bounded, and strictly ordered"); + previous = v; + } + } + float x[32] = {}; + block_rocmfp2 q2; + block_rocmfp3 q3; + if (levels == 4 && !rocmfpx_quantize_row_fp2_mix_ref(x, &q2, 32, books.data(), nullptr)) + fail("repaired fp2 codebook rejected by encoder"); + if (levels == 8 && !rocmfpx_quantize_row_fp3_mix_ref(x, &q3, 32, books.data(), nullptr)) + fail("repaired fp3 codebook rejected by encoder"); + } + } + bool rejected = false; + try { HistogramFitter().fit(4); } catch (const std::runtime_error &) { rejected = true; } + if (!rejected) fail("empty histogram accepted"); + std::cout << "PASS: degenerate fitter, BF16 ordering, determinism, codec acceptance, empty rejection\n"; + return 0; + } catch (const std::exception & e) { + std::cerr << "FAIL: " << e.what() << "\n"; + return 1; + } +} diff --git a/server/test/test_ds4v_mix_converter.cpp b/server/test/test_ds4v_mix_converter.cpp index ea5f0d41a..1b076b8d5 100644 --- a/server/test/test_ds4v_mix_converter.cpp +++ b/server/test/test_ds4v_mix_converter.cpp @@ -19,11 +19,7 @@ using CppUnitTestFramework::CommonFixture; #include #include -static int g_fails = 0; -#define CHECK(cond, msg) \ - do { \ - if (!(cond)) { std::fprintf(stderr, "FAIL: %s\n", (msg)); ++g_fails; } \ - } while (0) +#define CHECK(cond, msg) REQUIRE(cond) namespace { diff --git a/server/tools/ds4_mix_converter/CMakeLists.txt b/server/tools/ds4_mix_converter/CMakeLists.txt index 6f12682af..58489a2e0 100644 --- a/server/tools/ds4_mix_converter/CMakeLists.txt +++ b/server/tools/ds4_mix_converter/CMakeLists.txt @@ -30,7 +30,7 @@ if(UNIX) endif() target_compile_options(ds4_mix_converter PRIVATE -Wall -Wextra -Wpedantic) -add_executable(test_ds4_mix_converter ../../test/test_ds4_mix_converter.cpp) +add_executable(test_ds4_mix_converter ../../test/test_ds4v_mix_converter.cpp ../../test/test_unit_main.cpp) target_include_directories(test_ds4_mix_converter PRIVATE ../../deps/llama.cpp/ggml/include ../../deps/llama.cpp/ggml/rocmfpx) @@ -40,3 +40,9 @@ if(UNIX) endif() enable_testing() add_test(NAME ds4_mix_converter_codec COMMAND test_ds4_mix_converter) + +add_executable(test_ds4_mix_fitter ../../test/test_ds4_mix_fitter.cpp) +target_link_libraries(test_ds4_mix_fitter PRIVATE ggml-base nlohmann_json::nlohmann_json) +target_include_directories(test_ds4_mix_fitter PRIVATE + ../../deps/llama.cpp/ggml/include ../../deps/llama.cpp/ggml/rocmfpx) +add_test(NAME ds4_mix_fitter COMMAND test_ds4_mix_fitter) From 9e403cd41527feb91eadc9f792900a41c2a7323a Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 17:16:01 -0400 Subject: [PATCH 003/123] fix(ds4v): separate collapsed BF16 codebook levels and stamp repairs --- server/test/test_ds4_mix_fitter.cpp | 15 ++++ .../ds4_mix_converter/ds4_mix_converter.cpp | 68 +++++++++++++++---- 2 files changed, 68 insertions(+), 15 deletions(-) diff --git a/server/test/test_ds4_mix_fitter.cpp b/server/test/test_ds4_mix_fitter.cpp index 00260dfc5..aae2c09ca 100644 --- a/server/test/test_ds4_mix_fitter.cpp +++ b/server/test/test_ds4_mix_fitter.cpp @@ -30,6 +30,21 @@ int main() { fail("repaired fp3 codebook rejected by encoder"); } } + bool repaired = false; + const auto close = HistogramFitter::round_centers({-1.0f, 0.5f, 0.5001f, 1.0f}, repaired); + if (!repaired || bf16_to_float(close[2]) <= bf16_to_float(close[1])) + fail("BF16 rounding collision was not repaired"); + repaired = false; + const auto distinct = HistogramFitter::round_centers({-1.0f, -0.5f, 0.5f, 1.0f}, repaired); + if (repaired || distinct != std::vector{0xbf80, 0xbf00, 0x3f00, 0x3f80}) + fail("nondegenerate centers changed"); + HistogramFitter stamped; + float zero[16] = {}; + stamped.add_half(zero, nullptr); + std::vector stamps; + stamped.fit(4, "layer=42 expert=164 down", &stamps); + if (stamps.size() != 2 || stamps[0].find("layer=42 expert=164 down") == std::string::npos) + fail("repair stamp missing expert identity"); bool rejected = false; try { HistogramFitter().fit(4); } catch (const std::runtime_error &) { rejected = true; } if (!rejected) fail("empty histogram accepted"); diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index a23f6a875..d3245ae03 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -390,7 +390,38 @@ class HistogramFitter { } } - std::vector fit(int levels) const { + static std::vector round_centers(const std::vector & centers, bool & repaired) { + std::vector bits; + for (float center : centers) { + if (!std::isfinite(center) || center < -1.0f || center > 1.0f) + fail("invalid adaptive codebook center"); + bits.push_back(float_to_bf16(center)); + } + for (size_t i = 1; i < bits.size(); ++i) { + if (bf16_to_float(bits[i]) <= bf16_to_float(bits[i-1])) { + bits[i] = bf16_step(bits[i-1], true); + repaired = true; + } + } + if (!bits.empty() && bf16_to_float(bits.back()) > 1.0f) { + bits.back() = float_to_bf16(1.0f); + for (size_t i = bits.size()-1; i > 0; --i) { + if (bf16_to_float(bits[i-1]) >= bf16_to_float(bits[i])) + bits[i-1] = bf16_step(bits[i], false); + } + } + float previous = -std::numeric_limits::infinity(); + for (uint16_t b : bits) { + const float value = bf16_to_float(b); + if (!std::isfinite(value) || value < -1.0f || value > 1.0f || value <= previous) + fail("cannot separate adaptive codebook levels in BF16"); + previous = value; + } + return bits; + } + + std::vector fit(int levels, const std::string & label = "unlabelled", + std::vector * repairs = nullptr) const { if (levels != 4 && levels != 8) fail("unsupported fitted codebook size"); std::vector result; result.reserve(static_cast(2*levels)); @@ -399,21 +430,24 @@ class HistogramFitter { for (int population = 0; population < 2; ++population) { const std::vector & h = total(hist_[population]) > 0.0 ? hist_[population] : fallback; const std::vector centers = lloyd(h, levels); - float previous = -std::numeric_limits::infinity(); - for (int i = 0; i < levels; ++i) { - const uint16_t b = float_to_bf16(centers[i]); - const float roundtrip = bf16_to_float(b); - if (!std::isfinite(roundtrip) || !(roundtrip > previous)) { - fail("fitted codebook collapses after BF16 rounding"); - } - result.push_back(b); - previous = roundtrip; + bool repaired = false; + const auto bits = round_centers(centers, repaired); + result.insert(result.end(), bits.begin(), bits.end()); + if (repaired) { + const std::string stamp = label + " population=" + std::to_string(population) + + " levels=" + std::to_string(levels) + " repair=bf16-epsilon-v1"; + std::cerr << "[calibration] WARNING: " << stamp << "\n"; + if (repairs) repairs->push_back(stamp); } } return result; } private: + static uint16_t bf16_step(uint16_t bits, bool up) { + if ((bits & 0x7fffu) == 0) return up ? 0x0080u : 0x8080u; + return static_cast(bits + ((up != ((bits & 0x8000u) != 0)) ? 1 : -1)); + } static double total(const std::vector & h) { double out = 0.0; for (double value : h) out += value; @@ -449,9 +483,6 @@ class HistogramFitter { for (int j = 0; j < k; ++j) if (weights[j] > 0.0) c[j] = static_cast(sums[j]/weights[j]); std::sort(c.begin(), c.end()); } - for (int j = 1; j < k; ++j) { - if (!(c[j] > c[j - 1])) fail("degenerate adaptive codebook"); - } return c; } @@ -516,6 +547,7 @@ struct LayerCalibration { TensorShape down_shape; CodebookRegistry gate_up; CodebookRegistry down; + std::vector repairs; }; struct Options { @@ -1167,8 +1199,9 @@ std::vector calibrate( current.down_shape.in != down_shape.in || current.down_shape.out != down_shape.out) { fail("expert shapes vary within layer " + std::to_string(layer)); } - current.gate_up.experts.push_back(gate_up_fitter.fit(kGuLevels)); - current.down.experts.push_back(down_fitter.fit(kP4Levels)); + const std::string label = "layer=" + std::to_string(layer) + " expert=" + std::to_string(expert); + current.gate_up.experts.push_back(gate_up_fitter.fit(kGuLevels, label + " gate_up", ¤t.repairs)); + current.down.experts.push_back(down_fitter.fit(kP4Levels, label + " down", ¤t.repairs)); std::cerr << "[calibration] layer " << layer << " expert " << (expert + 1) << "/" << experts << " fitted joint gate/up and down codebooks\n"; } @@ -1258,6 +1291,11 @@ void write_gguf(const Options & options, const SafeTensorSet & source, if (!ctx) fail("gguf_init_empty failed"); set_model_metadata(ctx, source, layers, experts, options.absmax_only, options.experts_only, p4); + std::vector repair_stamps; + for (const auto & layer : calibration) + for (const auto & stamp : layer.repairs) repair_stamps.push_back(stamp.c_str()); + gguf_set_val_str(ctx, "deepseek4.mix.codebook_repair", "bf16-epsilon-v1"); + gguf_set_arr_str(ctx, "deepseek4.mix.codebook_repairs", repair_stamps.data(), repair_stamps.size()); std::vector> descriptors; descriptors.reserve(plan.size()); for (const TensorSpec & spec : plan) { From 07e32844cc32602bab8167072e9b301eb832bfe2 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 17:16:46 -0400 Subject: [PATCH 004/123] test(ds4v): replay an individual source expert through calibration --- server/test/test_ds4_mix_fitter.cpp | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/server/test/test_ds4_mix_fitter.cpp b/server/test/test_ds4_mix_fitter.cpp index aae2c09ca..bef9793e0 100644 --- a/server/test/test_ds4_mix_fitter.cpp +++ b/server/test/test_ds4_mix_fitter.cpp @@ -2,7 +2,7 @@ #include "../tools/ds4_mix_converter/ds4_mix_converter.cpp" #undef main -int main() { +int main(int argc, char ** argv) { try { for (float value : {0.0f, -1.0f, 1.0f}) { HistogramFitter fitter; @@ -48,6 +48,24 @@ int main() { bool rejected = false; try { HistogramFitter().fit(4); } catch (const std::runtime_error &) { rejected = true; } if (!rejected) fail("empty histogram accepted"); + if (argc != 1 && argc != 5) fail("usage: test_ds4_mix_fitter [SOURCE IMATRIX LAYER EXPERT]"); + if (argc == 5) { + SafeTensorSet source(argv[1]); + std::optional imatrix = load_imatrix(argv[2]); + const int layer = parse_nonnegative(argv[3], "layer"); + const int expert = parse_nonnegative(argv[4], "expert"); + HistogramFitter gate_up, down; + for (const auto & recipe : kExpertRecipes) { + const auto shape = validate_expert_source(source, layer, expert, recipe); + const auto * importance = require_imatrix(imatrix, target_expert_name(layer, recipe), shape.in); + add_expert_to_fitter(source, layer, expert, recipe, importance, + recipe.books == BookSource::GateUpJoint ? gate_up : down); + } + const std::string label = "layer=" + std::to_string(layer) + " expert=" + std::to_string(expert); + gate_up.fit(kGuLevels, label + " gate_up"); + down.fit(kP4Levels, label + " down"); + std::cout << "PASS: source replay " << label << "\n"; + } std::cout << "PASS: degenerate fitter, BF16 ordering, determinism, codec acceptance, empty rejection\n"; return 0; } catch (const std::exception & e) { From fd5a9940bc4967d320c7d1cc6a89da722ddc0b86 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 17:59:00 -0400 Subject: [PATCH 005/123] feat(ds4v): export lossless vision projector GGUF --- docs/ds4v-mmproj.md | 35 ++ server/tests/test_export_ds4v_mmproj.py | 292 ++++++++++++++ server/tools/export_ds4v_mmproj.py | 508 ++++++++++++++++++++++++ 3 files changed, 835 insertions(+) create mode 100644 docs/ds4v-mmproj.md create mode 100644 server/tests/test_export_ds4v_mmproj.py create mode 100755 server/tools/export_ds4v_mmproj.py diff --git a/docs/ds4v-mmproj.md b/docs/ds4v-mmproj.md new file mode 100644 index 000000000..180c2a67f --- /dev/null +++ b/docs/ds4v-mmproj.md @@ -0,0 +1,35 @@ +# DeepSeek-V4 vision projector export + +`server/tools/export_ds4v_mmproj.py` extracts the vision tower and aligner from +the verified DeepSeek-V4 parent model without loading or changing tensor values. +It requires only Python's standard library. + +```bash +python3 server/tools/export_ds4v_mmproj.py \ + /models/DeepSeek-V4-Flash-Vision-Uncensored \ + /models/ds4v-mmproj.gguf +``` + +The output path must not exist. The exporter validates `config.json`, the +safetensors index, all 267 required names and shapes, BF16 payload lengths, +source bounds, and shard paths before it writes. It publishes the finished file +atomically and removes its temporary file after any pre-publication failure. + +The GGUF keeps every `vision.*`, `aligner.*`, and `image_*` name, shape, dtype, +and payload byte unchanged. `general.architecture` is `deepseek4_vision`. +Required `deepseek4.vision.*` metadata records the 32-block, width-1024, +16-head tower; head width 64; patch size 14; intermediate width 2816; 2D RoPE +layout and theta 10000; ratio-3, width-9216 aligner; language width 4096; +vocabulary 129280; RMS epsilon `1e-6`; image token/pixel/aspect bounds; RGB +mean/std `0.5`; channel-major patches; N-layout recipe version 1 with +compression alignment 4; bottom/right aligner padding; channel-first unfold; +and exact GELU. + +Run the focused synthetic suite with: + +```bash +python3 -m unittest -v server.tests.test_export_ds4v_mmproj +``` + +When NumPy is available, the suite also opens the result with llama.cpp's +vendored `GGUFReader`, independently of the exporter's writer. diff --git a/server/tests/test_export_ds4v_mmproj.py b/server/tests/test_export_ds4v_mmproj.py new file mode 100644 index 000000000..dae81eb2b --- /dev/null +++ b/server/tests/test_export_ds4v_mmproj.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +"""Focused synthetic tests for the lossless DS4V mmproj exporter.""" + +import importlib.util +import json +import struct +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +REPO_ROOT = Path(__file__).resolve().parents[2] +EXPORTER_PATH = REPO_ROOT / "server" / "tools" / "export_ds4v_mmproj.py" +SPEC = importlib.util.spec_from_file_location("export_ds4v_mmproj", EXPORTER_PATH) +assert SPEC is not None and SPEC.loader is not None +exporter = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = exporter +SPEC.loader.exec_module(exporter) + + +TEST_SHAPES = { + "aligner.tiny": (2,), + "image_start": (3,), + "vision.tiny": (2, 2), +} + + +def exact_config(): + return dict(exporter.EXPECTED_CONFIG) + + +def write_json(path, value): + path.write_text(json.dumps(value), encoding="utf-8") + + +def write_safetensors(path, entries, payload): + raw_header = json.dumps(entries, separators=(",", ":")).encode("utf-8") + path.write_bytes(struct.pack(" bool: + return name.startswith("vision.") or name.startswith("aligner.") or name.startswith("image_") + + +def expected_shapes() -> Dict[str, Tuple[int, ...]]: + """Return the complete, source-name-preserving DS4V projector inventory.""" + shapes = { + "aligner.w1.bias": (4096,), + "aligner.w1.weight": (4096, 9216), + "aligner.w2.bias": (4096,), + "aligner.w2.weight": (4096, 4096), + "image_end": (4096,), + "image_newline": (4096,), + "image_pad": (4096,), + "image_start": (4096,), + "vision.norm.weight": (1024,), + "vision.patch_embed.proj.bias": (1024,), + "vision.patch_embed.proj.weight": (1024, 3 * 14 * 14), + } + for block in range(32): + prefix = "vision.blocks.%d" % block + shapes.update({ + prefix + ".attn.wo.bias": (1024,), + prefix + ".attn.wo.weight": (1024, 1024), + prefix + ".attn.wqkv.bias": (3 * 1024,), + prefix + ".attn.wqkv.weight": (3 * 1024, 1024), + prefix + ".mlp.w1.weight": (2 * 2816, 1024), + prefix + ".mlp.w2.weight": (1024, 2816), + prefix + ".norm1.weight": (1024,), + prefix + ".norm2.weight": (1024,), + }) + return shapes + + +EXPECTED_CONFIG = { + "vision_n_layers": 32, + "vision_dim": 1024, + "vision_n_heads": 16, + "vision_inter_dim": 2816, + "vision_patch_size": 14, + "vision_rope_theta": 10000.0, + "vision_downsample_ratio": 3, + "hidden_size": 4096, + "vocab_size": 129280, + "vision_max_n_token": 384, + "vision_min_pixels": 147456, + "vision_max_wh_ratio": 8, +} + + +def metadata() -> List[Tuple[str, str, object]]: + """Metadata contract consumed by the future DS4V vision loader.""" + return [ + ("general.architecture", "string", "deepseek4_vision"), + ("general.name", "string", "DeepSeek-V4 vision projector"), + ("general.type", "string", "mmproj"), + ("general.alignment", "uint32", GGUF_ALIGNMENT), + ("deepseek4.vision.schema_version", "uint32", 1), + ("deepseek4.vision.block_count", "uint32", 32), + ("deepseek4.vision.embedding_length", "uint32", 1024), + ("deepseek4.vision.attention.head_count", "uint32", 16), + ("deepseek4.vision.attention.head_dimension", "uint32", 64), + ("deepseek4.vision.attention.rope_layout", "string", "2d-half-split-height-width"), + ("deepseek4.vision.feed_forward_length", "uint32", 2816), + ("deepseek4.vision.patch_size", "uint32", 14), + ("deepseek4.vision.rope.freq_base", "float32", 10000.0), + ("deepseek4.vision.downsample_ratio", "uint32", 3), + ("deepseek4.vision.aligner_input_length", "uint32", 9216), + ("deepseek4.vision.language_embedding_length", "uint32", 4096), + ("deepseek4.vision.vocabulary_size", "uint32", 129280), + ("deepseek4.vision.attention.layer_norm_rms_epsilon", "float32", 1e-6), + ("deepseek4.vision.image.max_tokens", "uint32", 384), + ("deepseek4.vision.image.min_pixels", "uint32", 147456), + ("deepseek4.vision.image.max_aspect_ratio", "float32", 8.0), + ("deepseek4.vision.image.normalization_mean", "float32_array", (0.5, 0.5, 0.5)), + ("deepseek4.vision.image.normalization_std", "float32_array", (0.5, 0.5, 0.5)), + ("deepseek4.vision.image.patch_layout", "string", "channel-major"), + ("deepseek4.vision.image.layout", "string", "n"), + ("deepseek4.vision.image.layout_version", "uint32", 1), + ("deepseek4.vision.image.layout_recipe", "string", "row-pair-column-interleave"), + ("deepseek4.vision.image.compression_alignment", "uint32", 4), + ("deepseek4.vision.image.sentinel_types", "string", "start,pad,image,newline,end"), + ("deepseek4.vision.image.sentinel_type_count", "uint32", 5), + ("deepseek4.vision.aligner.padding", "string", "bottom-right"), + ("deepseek4.vision.aligner.patch_layout", "string", "channel-first-unfold"), + ("deepseek4.vision.aligner.activation", "string", "gelu-exact"), + ] + + +def _unique_object(pairs: Iterable[Tuple[str, object]]) -> Dict[str, object]: + result = {} + for key, value in pairs: + if key in result: + raise ExportError("duplicate JSON key: %s" % key) + result[key] = value + return result + + +def _read_json(path: Path, limit: int, description: str) -> Mapping[str, object]: + try: + size = path.stat().st_size + except OSError as exc: + raise ExportError("cannot stat %s %s: %s" % (description, path, exc)) from exc + if size > limit: + raise ExportError("%s exceeds %d-byte limit: %s" % (description, limit, path)) + try: + with path.open("r", encoding="utf-8") as handle: + value = json.load(handle, object_pairs_hook=_unique_object) + except (OSError, UnicodeError, json.JSONDecodeError) as exc: + raise ExportError("cannot read %s %s: %s" % (description, path, exc)) from exc + if not isinstance(value, dict): + raise ExportError("%s must contain a JSON object: %s" % (description, path)) + return value + + +def _source_child(root: Path, name: str, description: str) -> Path: + if not name or name in (".", "..") or "/" in name or "\\" in name or Path(name).name != name: + raise ExportError("unsafe %s path: %r" % (description, name)) + candidate = root / name + try: + resolved = candidate.resolve(strict=True) + resolved.relative_to(root) + except (OSError, ValueError) as exc: + raise ExportError("%s escapes or is missing from source directory: %r" % (description, name)) from exc + if not resolved.is_file(): + raise ExportError("%s is not a regular file: %s" % (description, resolved)) + return resolved + + +def _validate_config(config: Mapping[str, object]) -> None: + for key, expected in EXPECTED_CONFIG.items(): + if key not in config: + raise ExportError("config.json is missing required key %s" % key) + actual = config[key] + if isinstance(expected, float): + if not isinstance(actual, (int, float)) or isinstance(actual, bool) or float(actual) != expected: + raise ExportError("config %s must be %r, got %r" % (key, expected, actual)) + elif actual != expected or isinstance(actual, bool): + raise ExportError("config %s must be %r, got %r" % (key, expected, actual)) + + +def _parse_safetensors_header(path: Path) -> Tuple[int, Mapping[str, object], os.stat_result]: + try: + stat_before = path.stat() + with path.open("rb") as handle: + raw_size = handle.read(8) + if len(raw_size) != 8: + raise ExportError("truncated safetensors size header: %s" % path) + header_size = struct.unpack(" MAX_SAFETENSORS_HEADER_BYTES: + raise ExportError("invalid safetensors JSON header size %d: %s" % (header_size, path)) + if 8 + header_size > stat_before.st_size: + raise ExportError("safetensors JSON header exceeds file bounds: %s" % path) + raw_header = handle.read(header_size) + stat_after = path.stat() + except OSError as exc: + raise ExportError("cannot read safetensors header %s: %s" % (path, exc)) from exc + if (stat_before.st_dev, stat_before.st_ino, stat_before.st_size, stat_before.st_mtime_ns) != ( + stat_after.st_dev, stat_after.st_ino, stat_after.st_size, stat_after.st_mtime_ns): + raise ExportError("safetensors file changed while reading its header: %s" % path) + try: + header = json.loads(raw_header, object_pairs_hook=_unique_object) + except (UnicodeError, json.JSONDecodeError) as exc: + raise ExportError("invalid safetensors JSON header %s: %s" % (path, exc)) from exc + if not isinstance(header, dict): + raise ExportError("safetensors header must be an object: %s" % path) + return header_size, header, stat_after + + +def _validate_tensor_entry(name: str, entry: object, data_bytes: int) -> Tuple[str, Tuple[int, ...], int, int]: + if not isinstance(entry, dict): + raise ExportError("tensor %s header entry must be an object" % name) + if set(entry) != {"dtype", "shape", "data_offsets"}: + raise ExportError("tensor %s header entry has unexpected fields" % name) + dtype = entry["dtype"] + shape = entry["shape"] + offsets = entry["data_offsets"] + if dtype != "BF16": + raise ExportError("tensor %s must remain BF16, got %r" % (name, dtype)) + if not isinstance(shape, list) or not shape or any( + not isinstance(dim, int) or isinstance(dim, bool) or dim <= 0 for dim in shape): + raise ExportError("tensor %s has an invalid shape %r" % (name, shape)) + if not isinstance(offsets, list) or len(offsets) != 2 or any( + not isinstance(offset, int) or isinstance(offset, bool) for offset in offsets): + raise ExportError("tensor %s has invalid data offsets %r" % (name, offsets)) + start, end = offsets + if start < 0 or end <= start or end > data_bytes: + raise ExportError("tensor %s data offsets %r exceed safetensors bounds" % (name, offsets)) + elements = 1 + for dim in shape: + elements *= dim + expected_bytes = elements * 2 + if end - start != expected_bytes: + raise ExportError("tensor %s has %d bytes; BF16 shape %r requires %d" % ( + name, end - start, shape, expected_bytes)) + return dtype, tuple(shape), start, end + + +def discover_tensors(source_dir: Path) -> List[SourceTensor]: + try: + root = source_dir.resolve(strict=True) + except OSError as exc: + raise ExportError("source directory does not exist: %s" % source_dir) from exc + if not root.is_dir(): + raise ExportError("source is not a directory: %s" % root) + + config_path = _source_child(root, "config.json", "config") + index_path = _source_child(root, "model.safetensors.index.json", "safetensors index") + _validate_config(_read_json(config_path, MAX_CONFIG_BYTES, "config")) + index = _read_json(index_path, MAX_INDEX_BYTES, "safetensors index") + weight_map = index.get("weight_map") + if not isinstance(weight_map, dict): + raise ExportError("safetensors index is missing object-valued weight_map") + if any(not isinstance(name, str) or not isinstance(shard, str) for name, shard in weight_map.items()): + raise ExportError("safetensors weight_map keys and values must be strings") + + expected = expected_shapes() + selected = {name: shard for name, shard in weight_map.items() if _is_projector_tensor(name)} + missing = sorted(set(expected) - set(selected)) + extra = sorted(set(selected) - set(expected)) + if missing or extra: + details = [] + if missing: + details.append("missing: " + ", ".join(missing[:8]) + (" ..." if len(missing) > 8 else "")) + if extra: + details.append("unexpected: " + ", ".join(extra[:8]) + (" ..." if len(extra) > 8 else "")) + raise ExportError("incomplete projector inventory (%s)" % "; ".join(details)) + + shard_paths = {} + for shard_name in sorted(set(selected.values())): + shard_paths[shard_name] = _source_child(root, shard_name, "safetensors shard") + + headers = {} + for shard_name, path in shard_paths.items(): + header_size, header, file_stat = _parse_safetensors_header(path) + headers[shard_name] = (header_size, header, file_stat) + indexed_here = {name for name, mapped_shard in selected.items() if mapped_shard == shard_name} + present_here = {name for name in header if name != "__metadata__" and _is_projector_tensor(name)} + if present_here != indexed_here: + missing_here = sorted(indexed_here - present_here) + unindexed_here = sorted(present_here - indexed_here) + raise ExportError("projector/index disagreement in %s (missing=%r, unindexed=%r)" % ( + shard_name, missing_here[:8], unindexed_here[:8])) + + tensors = [] + ranges_by_shard = {} + for name in sorted(expected): + shard_name = selected[name] + path = shard_paths[shard_name] + header_size, header, file_stat = headers[shard_name] + if name not in header: + raise ExportError("index tensor %s is absent from %s" % (name, shard_name)) + data_bytes = file_stat.st_size - 8 - header_size + dtype, shape, start, end = _validate_tensor_entry(name, header[name], data_bytes) + if shape != expected[name]: + raise ExportError("tensor %s shape must be %r, got %r" % (name, expected[name], shape)) + ranges_by_shard.setdefault(shard_name, []).append((start, end, name)) + tensors.append(SourceTensor( + name=name, + path=path, + shape=shape, + dtype=dtype, + file_offset=8 + header_size + start, + nbytes=end - start, + file_size=file_stat.st_size, + device=file_stat.st_dev, + inode=file_stat.st_ino, + mtime_ns=file_stat.st_mtime_ns, + )) + + for shard_name, ranges in ranges_by_shard.items(): + previous_end = -1 + previous_name = "" + for start, end, name in sorted(ranges): + if start < previous_end: + raise ExportError("overlapping projector tensors in %s: %s and %s" % ( + shard_name, previous_name, name)) + previous_end = end + previous_name = name + return tensors + + +def _pack_string(value: str) -> bytes: + raw = value.encode("utf-8") + return struct.pack(" bytes: + result = bytearray(_pack_string(key)) + if kind == "uint32": + result += struct.pack(" int: + return (value + alignment - 1) // alignment * alignment + + +def _build_header(tensors: Sequence[SourceTensor]) -> Tuple[bytes, List[OutputTensor]]: + metadata_entries = metadata() + output_tensors = [] + offset = 0 + tensor_info = bytearray() + for tensor in tensors: + output_tensors.append(OutputTensor(tensor, offset)) + tensor_info += _pack_string(tensor.name) + tensor_info += struct.pack(" None: + current = os.fstat(handle.fileno()) + actual = (current.st_dev, current.st_ino, current.st_size, current.st_mtime_ns) + expected = (tensor.device, tensor.inode, tensor.file_size, tensor.mtime_ns) + if actual != expected: + raise ExportError("source shard changed after validation: %s" % tensor.path) + + +def _copy_tensor(output, tensor: SourceTensor) -> None: + try: + with tensor.path.open("rb") as source: + _check_source_identity(source, tensor) + source.seek(tensor.file_offset) + remaining = tensor.nbytes + while remaining: + chunk = source.read(min(remaining, COPY_CHUNK_BYTES)) + if not chunk: + raise ExportError("source tensor became truncated: %s" % tensor.name) + output.write(chunk) + remaining -= len(chunk) + _check_source_identity(source, tensor) + except OSError as exc: + raise ExportError("cannot copy tensor %s from %s: %s" % (tensor.name, tensor.path, exc)) from exc + padding = _align(tensor.nbytes) - tensor.nbytes + if padding: + output.write(bytes(padding)) + + +def _validate_output_path(output_path: Path) -> Path: + if output_path.name in ("", ".", ".."): + raise ExportError("output must name a GGUF file") + try: + parent = output_path.parent.resolve(strict=True) + except OSError as exc: + raise ExportError("output directory does not exist: %s" % output_path.parent) from exc + if not parent.is_dir(): + raise ExportError("output parent is not a directory: %s" % parent) + output = parent / output_path.name + if os.path.lexists(str(output)): + raise ExportError("refusing to overwrite existing output: %s" % output) + return output + + +def export_mmproj(source_dir: Path, output_path: Path) -> Tuple[int, int]: + tensors = discover_tensors(source_dir) + header, output_tensors = _build_header(tensors) + output = _validate_output_path(output_path) + + temp_fd = -1 + temp_name = "" + linked = False + try: + temp_fd, temp_name = tempfile.mkstemp( + prefix=".%s." % output.name, + suffix=".tmp", + dir=str(output.parent), + ) + with os.fdopen(temp_fd, "wb") as handle: + temp_fd = -1 + handle.write(header) + for output_tensor in output_tensors: + _copy_tensor(handle, output_tensor.source) + handle.flush() + os.fsync(handle.fileno()) + + # Hard-linking is an atomic no-overwrite publish on the same filesystem. + # Readers can observe only the complete, fsynced file. + os.link(temp_name, output) + linked = True + os.unlink(temp_name) + temp_name = "" + directory_fd = os.open(str(output.parent), os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + except FileExistsError as exc: + raise ExportError("refusing to overwrite existing output: %s" % output) from exc + except OSError as exc: + if linked: + raise ExportError("output was published but final directory sync failed: %s" % exc) from exc + raise ExportError("cannot write output %s: %s" % (output, exc)) from exc + finally: + if temp_fd >= 0: + os.close(temp_fd) + if temp_name: + try: + os.unlink(temp_name) + except FileNotFoundError: + pass + + return len(tensors), sum(tensor.nbytes for tensor in tensors) + + +def _parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Losslessly extract the DeepSeek-V4 vision projector into standalone GGUF v3.") + parser.add_argument("source_dir", type=Path, + help="parent model directory containing config.json, index, and shards") + parser.add_argument("output", type=Path, help="new output GGUF path; existing paths are refused") + return parser.parse_args(argv) + + +def main(argv: Sequence[str] = ()) -> int: + args = _parse_args(argv or sys.argv[1:]) + try: + tensor_count, payload_bytes = export_mmproj(args.source_dir, args.output) + except ExportError as exc: + print("error: %s" % exc, file=sys.stderr) + return 1 + print("exported %d BF16 tensors (%d payload bytes) to %s" % ( + tensor_count, payload_bytes, args.output)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From ef64f62fdc9aca1202ef58600d020c42d8e4c1b0 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:02:28 -0400 Subject: [PATCH 006/123] chore(ds4v): trim exporter test commentary --- server/tests/test_export_ds4v_mmproj.py | 3 --- server/tools/export_ds4v_mmproj.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/tests/test_export_ds4v_mmproj.py b/server/tests/test_export_ds4v_mmproj.py index dae81eb2b..1304de212 100644 --- a/server/tests/test_export_ds4v_mmproj.py +++ b/server/tests/test_export_ds4v_mmproj.py @@ -1,6 +1,4 @@ #!/usr/bin/env python3 -"""Focused synthetic tests for the lossless DS4V mmproj exporter.""" - import importlib.util import json import struct @@ -77,7 +75,6 @@ def read_string(handle): def read_gguf(path): - """Small test-only parser, separate from the production writer.""" metadata = {} infos = [] with path.open("rb") as handle: diff --git a/server/tools/export_ds4v_mmproj.py b/server/tools/export_ds4v_mmproj.py index d2bd3169b..9921a9ff2 100755 --- a/server/tools/export_ds4v_mmproj.py +++ b/server/tools/export_ds4v_mmproj.py @@ -354,7 +354,7 @@ def _pack_metadata_entry(key: str, kind: str, value: object) -> bytes: result += struct.pack(" Date: Fri, 4 Sep 2026 18:18:10 -0400 Subject: [PATCH 007/123] feat(ds4v): implement isolated BF16 vision tower runtime and CPU probes --- server/src/deepseek4/deepseek4_vision.cpp | 334 ++++++++++++++++++++++ server/src/deepseek4/deepseek4_vision.h | 61 ++++ server/tools/ds4v_vision/CMakeLists.txt | 21 ++ server/tools/ds4v_vision/compare.py | 30 ++ server/tools/ds4v_vision/geometry.cpp | 82 ++++++ server/tools/ds4v_vision/probe.cpp | 72 +++++ 6 files changed, 600 insertions(+) create mode 100644 server/src/deepseek4/deepseek4_vision.cpp create mode 100644 server/src/deepseek4/deepseek4_vision.h create mode 100644 server/tools/ds4v_vision/CMakeLists.txt create mode 100644 server/tools/ds4v_vision/compare.py create mode 100644 server/tools/ds4v_vision/geometry.cpp create mode 100644 server/tools/ds4v_vision/probe.cpp diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp new file mode 100644 index 000000000..b14a4dbd4 --- /dev/null +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -0,0 +1,334 @@ +#include "deepseek4_vision.h" +#include "common/gguf_bounds.h" +#include "common/gguf_mmap.h" +#include "ggml.h" +#include "ggml-alloc.h" +#include "gguf.h" +#include +#include +#include +#include +#include +#include + +namespace dflash::vision { +namespace { +using Tensor = ggml_tensor; +constexpr size_t MAX_SCRATCH = size_t(2) * 1024 * 1024 * 1024; +Tensor * rounded(ggml_context * c, Tensor * x) { + return ggml_cast(c, ggml_cast(c, x, GGML_TYPE_BF16), GGML_TYPE_F32); +} +void require(bool value, const std::string & error) { if (!value) throw std::runtime_error(error); } +struct Meta { + gguf_context * g = nullptr; + ggml_context * c = nullptr; + ~Meta() { if (g) gguf_free(g); if (c) ggml_free(c); } +}; +std::map> inventory() { + std::map> result; + auto add = [&](const std::string & n, int64_t a, int64_t b = 1) { result[n] = {a,b,1,1}; }; + add("vision.patch_embed.proj.weight",588,1024); add("vision.patch_embed.proj.bias",1024); + add("vision.norm.weight",1024); + add("aligner.w1.weight",9216,4096); add("aligner.w1.bias",4096); + add("aligner.w2.weight",4096,4096); add("aligner.w2.bias",4096); + for (auto n : {"image_start","image_pad","image_newline","image_end"}) add(n,4096); + for (int i=0;i<32;++i) { + const auto p = "vision.blocks." + std::to_string(i); + add(p+".norm1.weight",1024); add(p+".norm2.weight",1024); + add(p+".attn.wqkv.weight",1024,3072); add(p+".attn.wqkv.bias",3072); + add(p+".attn.wo.weight",1024,1024); add(p+".attn.wo.bias",1024); + add(p+".mlp.w1.weight",1024,5632); add(p+".mlp.w2.weight",2816,1024); + } + return result; +} +void validate_metadata(gguf_context * g, int dimension, int vocabulary) { + auto key = [&](const std::string & n, gguf_type type) { + auto id = gguf_find_key(g,n.c_str()); + require(id >= 0 && gguf_get_kv_type(g,id)==type,"missing or wrong metadata type: "+n); + return id; + }; + auto str = [&](const std::string & n, const char * value) { + require(std::string(gguf_get_val_str(g,key(n,GGUF_TYPE_STRING)))==value,"unsupported metadata: "+n); + }; + auto integer = [&](const std::string & n, uint32_t value) { + require(gguf_get_val_u32(g,key(n,GGUF_TYPE_UINT32))==value,"unsupported metadata: "+n); + }; + auto real = [&](const std::string & n,float value) { + require(gguf_get_val_f32(g,key(n,GGUF_TYPE_FLOAT32))==value,"unsupported metadata: "+n); + }; + str("general.architecture","deepseek4_vision"); str("general.type","mmproj"); + integer("general.alignment",32); + const std::string p="deepseek4.vision."; + for (auto entry : std::map{{"schema_version",1},{"block_count",32}, + {"embedding_length",1024},{"attention.head_count",16},{"attention.head_dimension",64}, + {"feed_forward_length",2816},{"patch_size",14},{"downsample_ratio",3},{"aligner_input_length",9216}, + {"language_embedding_length",4096},{"vocabulary_size",129280},{"image.max_tokens",384}, + {"image.min_pixels",147456},{"image.layout_version",1},{"image.compression_alignment",4}, + {"image.sentinel_type_count",5}}) integer(p+entry.first,entry.second); + require(dimension==4096 && vocabulary==129280,"language model dimension/vocabulary mismatch"); + real(p+"rope.freq_base",10000.f); real(p+"attention.layer_norm_rms_epsilon",1e-6f); + real(p+"image.max_aspect_ratio",8.f); + for (auto entry : std::map{{"attention.rope_layout","2d-half-split-height-width"}, + {"image.patch_layout","channel-major"},{"image.layout","n"}, + {"image.layout_recipe","row-pair-column-interleave"},{"image.sentinel_types","start,pad,image,newline,end"}, + {"aligner.padding","bottom-right"},{"aligner.patch_layout","channel-first-unfold"}, + {"aligner.activation","gelu-exact"}}) str(p+entry.first,entry.second); + for (auto n : {"image.normalization_mean","image.normalization_std"}) { + auto id = key(p+n,GGUF_TYPE_ARRAY); + require(gguf_get_arr_type(g,id)==GGUF_TYPE_FLOAT32 && gguf_get_arr_n(g,id)==3,"bad normalization array"); + const auto * values = static_cast(gguf_get_arr_data(g,id)); + require(values[0]==.5f && values[1]==.5f && values[2]==.5f,"unsupported normalization"); + } +} +struct Graph { + ggml_context * c; + ggml_cgraph * graph; + std::vector> stages; + explicit Graph() { + const size_t nodes=512; + c=ggml_init({nodes*ggml_tensor_overhead()+ggml_graph_overhead_custom(nodes,false),nullptr,true}); + require(c!=nullptr,"graph metadata allocation failed"); + graph=ggml_new_graph_custom(c,nodes,false); + } + ~Graph() { ggml_free(c); } + void stage(const std::string & name,Tensor * t,bool enabled) { + if (enabled) { ggml_set_output(t); stages.emplace_back(name,t); } + } +}; +std::vector read(Tensor * t) { + std::vector result(ggml_nelements(t)); + ggml_backend_tensor_get(t,result.data(),0,result.size()*sizeof(float)); + return result; +} +} +namespace detail { +void rotary_tables(PatchGrid grid,std::vector & cosine,std::vector & sine) { + const int n=grid.height*grid.width; + cosine.resize(size_t(n)*32); sine.resize(size_t(n)*32); + for(int i=0;ine[1],x->ne[2],x->nb[1],x->nb[2],0); + auto b=ggml_view_3d(c,x,32,x->ne[1],x->ne[2],x->nb[1],x->nb[2],32*sizeof(float)); + auto first=ggml_sub(c,ggml_mul(c,a,cosine),ggml_mul(c,b,sine)); + auto second=ggml_add(c,ggml_mul(c,b,cosine),ggml_mul(c,a,sine)); + return rounded(c,ggml_concat(c,first,second,0)); +} +Tensor * attention(ggml_context * c,Tensor * q,Tensor * k,Tensor * v) { + q=ggml_cont(c,ggml_permute(c,q,0,2,1,3)); + k=ggml_cont(c,ggml_permute(c,k,0,2,1,3)); + auto scores=ggml_mul_mat(c,k,q); + ggml_mul_mat_set_prec(scores,GGML_PREC_F32); + auto probabilities=ggml_soft_max(c,ggml_scale(c,scores,1.f/std::sqrt(float(q->ne[0])))); + v=ggml_cont(c,ggml_permute(c,v,1,2,0,3)); // [N, D, heads] + auto out=ggml_mul_mat(c,v,probabilities); + ggml_mul_mat_set_prec(out,GGML_PREC_F32); + out=ggml_cont(c,ggml_permute(c,out,0,2,1,3)); + return rounded(c,ggml_reshape_2d(c,out,out->ne[0]*out->ne[1],out->ne[2])); +} +Tensor * unfold(ggml_context * c,Tensor * x,PatchGrid grid,int channels) { + x=ggml_reshape_3d(c,x,channels,grid.width,grid.height); + x=ggml_cont(c,ggml_permute(c,x,2,0,1,3)); // [width,height,channels] + x=ggml_pad(c,x,(3-grid.width%3)%3,(3-grid.height%3)%3,0,0); + auto kernel=ggml_new_tensor_4d(c,GGML_TYPE_F32,3,3,channels,1); + auto out=ggml_im2col(c,kernel,x,3,3,0,0,1,1,true,GGML_TYPE_F32); + return ggml_reshape_2d(c,out,channels*9,((grid.width+2)/3)*((grid.height+2)/3)); +} +} +struct VisionRuntime::Impl { + VisionConfig config; + ggml_backend_t backend=nullptr; + ggml_context * weights=nullptr; + ggml_backend_buffer_t buffer=nullptr; + ggml_gallocr_t allocator=nullptr; + ~Impl() { + if(allocator) ggml_gallocr_free(allocator); + if(buffer) ggml_backend_buffer_free(buffer); + if(weights) ggml_free(weights); + } + Tensor * weight(const std::string & name) { return ggml_get_tensor(weights,name.c_str()); } + Tensor * linear(ggml_context * c,Tensor * x,const std::string & name,bool bias=true) { + auto y=ggml_mul_mat(c,weight(name+".weight"),x); + ggml_mul_mat_set_prec(y,GGML_PREC_F32); + if(bias) y=ggml_add(c,y,ggml_cast(c,weight(name+".bias"),GGML_TYPE_F32)); + return rounded(c,y); + } + Tensor * norm(ggml_context * c,Tensor * x,const std::string & name) { + return rounded(c,ggml_mul(c,ggml_rms_norm(c,x,config.rms_epsilon), + ggml_cast(c,weight(name+".weight"),GGML_TYPE_F32))); + } + std::vector execute(Graph & g,Tensor * input,const std::vector & values,Tensor * output, + const StageObserver & observer,Tensor * cosine=nullptr,Tensor * sine=nullptr, + const std::vector & cos_values={},const std::vector & sin_values={}) { + ggml_set_input(input); ggml_set_output(output); + ggml_build_forward_expand(g.graph,output); + if(!allocator) allocator=ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend)); + require(allocator!=nullptr,"scratch allocator creation failed"); + // reserve_n_size calculates requirements without allocating a backend buffer. + size_t required=0; + ggml_gallocr_reserve_n_size(allocator,g.graph,nullptr,nullptr,&required); + require(required<=MAX_SCRATCH,"vision scratch exceeds 2 GiB bound"); + for(int i=0;ine[0],t->ne[1],t->ne[2],t->ne[3]},read(t)); + } + return read(output); + } +}; +VisionRuntime::VisionRuntime()=default; +VisionRuntime::~VisionRuntime()=default; +bool VisionRuntime::load(const std::string & path,ggml_backend_t backend,int dimension,int vocabulary,std::string & error) { + error.clear(); + try { + require(backend!=nullptr,"null vision backend"); + Meta meta; + meta.g=gguf_init_from_file(path.c_str(),{true,&meta.c}); + require(meta.g && meta.c,"could not parse vision GGUF"); + validate_metadata(meta.g,dimension,vocabulary); + auto expected=inventory(); + require(gguf_get_n_tensors(meta.g)==int64_t(expected.size()),"wrong projector tensor count"); + common::GgufMmap mapped; + require(mapped.open(path,error),error); + std::vector> ranges; + for(int64_t i=0;itype==GGML_TYPE_BF16,"wrong tensor dtype: "+name); + for(int d=0;d<4;++d) require(t->ne[d]==it->second[d],"wrong tensor shape: "+name); + const size_t offset=gguf_get_tensor_offset(meta.g,i),size=gguf_get_tensor_size(meta.g,i); + require(common::gguf_tensor_in_file(gguf_get_data_offset(meta.g),offset,size,mapped.size()),"tensor outside file: "+name); + require(offset%32==0,"unaligned tensor: "+name); + ranges.emplace_back(offset,offset+size); + expected.erase(it); + } + std::sort(ranges.begin(),ranges.end()); + for(size_t i=1;i=ranges[i-1].second,"overlapping tensor data"); + require(expected.empty(),"missing projector tensor"); + auto candidate=std::make_unique(); + candidate->backend=backend; + candidate->weights=meta.c; meta.c=nullptr; + candidate->buffer=ggml_backend_alloc_ctx_tensors(candidate->weights,backend); + require(candidate->buffer!=nullptr,"projector weight allocation failed"); + ggml_backend_buffer_set_usage(candidate->buffer,GGML_BACKEND_BUFFER_USAGE_WEIGHTS); + const auto * bytes=static_cast(mapped.data()); + for(int64_t i=0;iweight(gguf_get_tensor_name(meta.g,i)); + ggml_backend_tensor_set(t,bytes+gguf_get_data_offset(meta.g)+gguf_get_tensor_offset(meta.g,i),0,ggml_nbytes(t)); + } + impl_=std::move(candidate); // Failed reload leaves prior runtime intact. + return true; + } catch(const std::exception & e) { error=e.what(); return false; } +} +bool VisionRuntime::encode(const std::vector & patches,PatchGrid grid,VisionOutput & output, + std::string & error,bool retain,const StageObserver & observer) { + error.clear(); output={}; + try { + require(bool(impl_),"vision runtime is not loaded"); + require(grid.height>0 && grid.width>0 && grid.height<=1152 && grid.width<=1152,"invalid patch grid"); + const int64_t n=int64_t(grid.height)*grid.width; + const int64_t rows=int64_t((grid.height+2)/3)*((grid.width+2)/3); + require(rows<=impl_->config.max_image_tokens && n<=3456,"patch grid exceeds image token budget"); + require(patches.size()==size_t(n)*588,"patch count/shape mismatch"); + for(float value:patches) require(std::isfinite(value),"non-finite image patch"); + std::vector x; + { + Graph g; + auto input=ggml_new_tensor_2d(g.c,GGML_TYPE_F32,588,n); + auto y=impl_->linear(g.c,rounded(g.c,input),"vision.patch_embed.proj"); + g.stage("patch_embed",y,bool(observer)); + x=impl_->execute(g,input,patches,y,observer); + } + std::vector cos_values,sin_values; + detail::rotary_tables(grid,cos_values,sin_values); + for(int i=0;i<32;++i) { + Graph g; + const auto p="vision.blocks."+std::to_string(i); + auto input=ggml_new_tensor_2d(g.c,GGML_TYPE_F32,1024,n); + auto cosine=ggml_new_tensor_3d(g.c,GGML_TYPE_F32,32,1,n); + auto sine=ggml_new_tensor_3d(g.c,GGML_TYPE_F32,32,1,n); + ggml_set_input(cosine); ggml_set_input(sine); + auto normalized=impl_->norm(g.c,input,p+".norm1"); + auto qkv=impl_->linear(g.c,normalized,p+".attn.wqkv"); + auto slice=[&](int offset) { + return ggml_cont(g.c,ggml_view_3d(g.c,qkv,64,16,n,64*sizeof(float),3072*sizeof(float),offset*1024*sizeof(float))); + }; + auto q=detail::rotate(g.c,slice(0),cosine,sine); + auto k=detail::rotate(g.c,slice(1),cosine,sine); + auto attention=detail::attention(g.c,q,k,slice(2)); + auto projected=impl_->linear(g.c,attention,p+".attn.wo"); + auto residual=rounded(g.c,ggml_add(g.c,input,projected)); + auto mlp=impl_->linear(g.c,impl_->norm(g.c,residual,p+".norm2"),p+".mlp.w1",false); + auto gate=ggml_cont(g.c,ggml_view_2d(g.c,mlp,2816,n,5632*sizeof(float),0)); + auto up=ggml_view_2d(g.c,mlp,2816,n,5632*sizeof(float),2816*sizeof(float)); + auto product=rounded(g.c,ggml_mul(g.c,rounded(g.c,ggml_silu(g.c,gate)),up)); + auto y=rounded(g.c,ggml_add(g.c,residual,impl_->linear(g.c,product,p+".mlp.w2",false))); + if(i==0) { + g.stage("block0.norm1",normalized,bool(observer)); g.stage("block0.qkv",qkv,bool(observer)); + g.stage("block0.q",q,bool(observer)); g.stage("block0.k",k,bool(observer)); + g.stage("block0.attention",attention,bool(observer)); + } + g.stage("block"+std::to_string(i),y,bool(observer)); + x=impl_->execute(g,input,x,y,observer,cosine,sine,cos_values,sin_values); + } + { + Graph g; + auto input=ggml_new_tensor_2d(g.c,GGML_TYPE_F32,1024,n); + auto y=impl_->norm(g.c,input,"vision.norm"); + g.stage("features",y,bool(observer)); + x=impl_->execute(g,input,x,y,observer); + } + VisionOutput result; + if(retain) result.features=x; + { + Graph g; + auto input=ggml_new_tensor_2d(g.c,GGML_TYPE_F32,1024,n); + auto unfolded=detail::unfold(g.c,input,grid,1024); + auto first=impl_->linear(g.c,unfolded,"aligner.w1"); + auto activated=rounded(g.c,ggml_gelu_erf(g.c,first)); + auto y=impl_->linear(g.c,activated,"aligner.w2"); + g.stage("unfold",unfolded,bool(observer)); g.stage("aligner.w1",first,bool(observer)); + g.stage("aligner.gelu",activated,bool(observer)); g.stage("embeddings",y,bool(observer)); + result.embeddings=impl_->execute(g,input,x,y,observer); + } + for(float value:result.embeddings) require(std::isfinite(value),"non-finite vision output"); + result.rows=int(rows); result.columns=4096; output=std::move(result); + return true; + } catch(const std::exception & e) { error=e.what(); return false; } +} +bool VisionRuntime::sentinel(Sentinel identity,std::vector & output,std::string & error) const { + error.clear(); output.clear(); + if(!impl_) { error="vision runtime is not loaded"; return false; } + const char * name=nullptr; + switch(identity) { + case Sentinel::Start: name="image_start"; break; case Sentinel::Pad: name="image_pad"; break; + case Sentinel::Newline: name="image_newline"; break; case Sentinel::End: name="image_end"; break; + default: error="invalid sentinel identity"; return false; + } + std::vector raw(4096); + ggml_backend_tensor_get(impl_->weight(name),raw.data(),0,raw.size()*sizeof(ggml_bf16_t)); + output.resize(4096); + ggml_bf16_to_fp32_row(raw.data(),output.data(),int64_t(raw.size())); + return true; +} +void VisionRuntime::release_scratch() { if(impl_ && impl_->allocator) { ggml_gallocr_free(impl_->allocator); impl_->allocator=nullptr; } } +const VisionConfig * VisionRuntime::config() const { return impl_ ? &impl_->config : nullptr; } +size_t VisionRuntime::weight_bytes() const { return impl_ ? ggml_backend_buffer_get_size(impl_->buffer) : 0; } +size_t VisionRuntime::scratch_bytes() const { return impl_ && impl_->allocator ? ggml_gallocr_get_buffer_size(impl_->allocator,0) : 0; } +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h new file mode 100644 index 000000000..82e7af9a3 --- /dev/null +++ b/server/src/deepseek4/deepseek4_vision.h @@ -0,0 +1,61 @@ +#pragma once + +#include "ggml-backend.h" +#include +#include +#include +#include +#include + +namespace dflash::vision { + +struct VisionConfig { + int layers = 32, dimension = 1024, heads = 16, intermediate = 2816; + int patch_size = 14, downsample = 3, language_dimension = 4096, vocabulary = 129280; + int max_image_tokens = 384; + float rope_theta = 10000.0f, rms_epsilon = 1e-6f; +}; +struct PatchGrid { int height, width; }; +enum class Sentinel { Start, Pad, Newline, End }; +struct VisionOutput { + int rows = 0, columns = 0; + std::vector embeddings; + // Optional raster [height * width, 1024] qualification output. + std::vector features; +}; +// Callback receives row-major values with the GGML shape (fastest axis first). +// It is synchronous: retaining diagnostics is the caller's responsibility. +using StageObserver = std::function &, + const std::vector &)>; + +// Owns weights and reusable graph allocator; borrows the backend, which must +// outlive this object. Sequential use only. No image decoding or token layout. +class VisionRuntime { +public: + VisionRuntime(); + ~VisionRuntime(); + VisionRuntime(const VisionRuntime &) = delete; + VisionRuntime & operator=(const VisionRuntime &) = delete; + bool load(const std::string & path, ggml_backend_t backend, + int language_dimension, int vocabulary, std::string & error); + bool encode(const std::vector & channel_major_patches, PatchGrid grid, + VisionOutput & output, std::string & error, bool retain_features = false, + const StageObserver & observer = {}); + bool sentinel(Sentinel identity, std::vector & output, std::string & error) const; + void release_scratch(); + const VisionConfig * config() const; + size_t weight_bytes() const; + size_t scratch_bytes() const; +private: + struct Impl; + std::unique_ptr impl_; +}; + +// The same geometry primitives used by the runtime and standalone unit tests. +namespace detail { +void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine); +ggml_tensor * rotate(ggml_context *, ggml_tensor *, ggml_tensor * cosine, ggml_tensor * sine); +ggml_tensor * attention(ggml_context *, ggml_tensor * q, ggml_tensor * k, ggml_tensor * v); +ggml_tensor * unfold(ggml_context *, ggml_tensor *, PatchGrid, int channels); +} +} // namespace dflash::vision diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt new file mode 100644 index 000000000..b6a2f1277 --- /dev/null +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4v_vision LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(GGML_CUDA OFF CACHE BOOL "" FORCE) +set(GGML_HIP OFF CACHE BOOL "" FORCE) +set(GGML_METAL OFF CACHE BOOL "" FORCE) +set(GGML_VULKAN OFF CACHE BOOL "" FORCE) +set(GGML_BLAS OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +add_subdirectory(../../deps/llama.cpp/ggml ggml) +add_library(ds4v_vision STATIC ../../src/deepseek4/deepseek4_vision.cpp) +target_include_directories(ds4v_vision PUBLIC ../../src) +target_link_libraries(ds4v_vision PUBLIC ggml) +add_executable(ds4v_vision_probe probe.cpp) +target_link_libraries(ds4v_vision_probe PRIVATE ds4v_vision) +add_executable(ds4v_vision_geometry geometry.cpp) +target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) +enable_testing() +add_test(NAME ds4v_vision_geometry COMMAND ds4v_vision_geometry) diff --git a/server/tools/ds4v_vision/compare.py b/server/tools/ds4v_vision/compare.py new file mode 100644 index 000000000..5b2278707 --- /dev/null +++ b/server/tools/ds4v_vision/compare.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Compare native raster tensors against independently generated parent fixtures.""" +import argparse +import hashlib +import json +from pathlib import Path +import numpy as np + +p = argparse.ArgumentParser() +p.add_argument('reference', type=Path) +p.add_argument('native', type=Path) +p.add_argument('--output', type=Path, required=True) +a = p.parse_args() +manifest = json.loads((a.reference / 'manifest.json').read_text()) +results = {} +for label, entry in manifest['images'].items(): + results[label] = {} + for stage in ('features', 'embeddings'): + meta = entry[stage] + reference_path = a.reference / meta['file'] + assert hashlib.sha256(reference_path.read_bytes()).hexdigest() == meta['sha256'] + ref = np.fromfile(reference_path, np.float32) + actual = np.fromfile(a.native / f'{label}-{stage}.f32', np.float32) + assert actual.size == ref.size == np.prod(meta['shape']), (label, stage, 'shape mismatch') + finite = bool(np.isfinite(actual).all()) + delta = actual.astype(np.float64) - ref.astype(np.float64) + cosine = np.dot(actual.astype(np.float64), ref.astype(np.float64)) / (np.linalg.norm(actual.astype(np.float64)) * np.linalg.norm(ref.astype(np.float64))) + results[label][stage] = dict(shape=meta['shape'], finite=finite, max_abs=float(np.abs(delta).max()), rmse=float(np.sqrt(np.mean(delta ** 2))), cosine=float(cosine), exact_fraction=float(np.mean(actual==ref))) +a.output.write_text(json.dumps(results, indent=2)+'\n') +print(json.dumps(results, indent=2)) diff --git a/server/tools/ds4v_vision/geometry.cpp b/server/tools/ds4v_vision/geometry.cpp new file mode 100644 index 000000000..9c4a12cf3 --- /dev/null +++ b/server/tools/ds4v_vision/geometry.cpp @@ -0,0 +1,82 @@ +#include "deepseek4/deepseek4_vision.h" +#include "ggml.h" +#include "ggml-alloc.h" +#include "ggml-cpu.h" +#include +#include +#include +#include + +using namespace dflash::vision; +static void check(bool value,const char * message) { if(!value) throw std::runtime_error(message); } +static float bf16(float value) { return ggml_bf16_to_fp32(ggml_fp32_to_bf16(value)); } +struct Test { + ggml_backend_t backend=ggml_backend_cpu_init(); + ggml_context * c=ggml_init({1024*1024,nullptr,true}); + ggml_gallocr_t alloc=ggml_gallocr_new(ggml_backend_cpu_buffer_type()); + std::vector>> inputs; + Test() { ggml_backend_cpu_set_n_threads(backend,2); } + ~Test() { ggml_gallocr_free(alloc); ggml_free(c); ggml_backend_free(backend); } + ggml_tensor * input(int a,int b,int d,std::vector data) { + auto t=ggml_new_tensor_3d(c,GGML_TYPE_F32,a,b,d); + ggml_set_input(t); inputs.emplace_back(t,std::move(data)); return t; + } + std::vector run(ggml_tensor * t) { + ggml_set_output(t); + auto g=ggml_new_graph(c); ggml_build_forward_expand(g,t); + check(ggml_gallocr_alloc_graph(alloc,g),"test graph allocation failed"); + for(auto & p:inputs) ggml_backend_tensor_set(p.first,p.second.data(),0,p.second.size()*4); + check(ggml_backend_graph_compute(backend,g)==GGML_STATUS_SUCCESS,"test compute failed"); + std::vector out(ggml_nelements(t)); ggml_backend_tensor_get(t,out.data(),0,out.size()*4); return out; + } +}; +int main() { + try { + { + Test t; + std::vector cosine,sine; detail::rotary_tables({2,3},cosine,sine); + std::vector x(64*2*6); + for(size_t i=0;i(6,0)); + auto k=t.input(2,1,3,std::vector(6,0)); + auto v=t.input(2,1,3,{1,2,4,5,10,11}); + auto out=t.run(detail::attention(t.c,q,k,v)); + for(int i=0;i<3;++i) { check(out[2*i]==5,"bidirectional attention mismatch"); check(out[2*i+1]==6,"attention channel mismatch"); } + } + { + Test t; + const int h=4,w=5,c=2; + std::vector data(h*w*c); + for(int y=0;y x={-3.f,-1.f,-.1f,0.f,.1f,1.f,3.f}; + auto out=t.run(ggml_gelu_erf(t.c,t.input(7,1,1,x))); + for(int i=0;i<7;++i) check(std::abs(out[i]-.5f*x[i]*(1+std::erf(x[i]/std::sqrt(2.f))))<1e-6f,"exact erf GELU mismatch"); + } + std::cout<<"PASS: half-split 2D RoPE, full bidirectional attention, padded channel-first unfold, exact erf GELU\n"; + return 0; + } catch(const std::exception & e) { std::cerr< +#include +#include +#include +#include + +using namespace dflash::vision; +static std::vector read_file(const std::string & path) { + std::ifstream f(path,std::ios::binary|std::ios::ate); + if(!f || f.tellg()<0 || size_t(f.tellg())%4) throw std::runtime_error("bad patch file"); + std::vector out(size_t(f.tellg())/4); + f.seekg(0); f.read(reinterpret_cast(out.data()),out.size()*4); + if(!f) throw std::runtime_error("patch file read failed"); + return out; +} +static void save(const std::string & path,const std::vector & values) { + std::ofstream f(path,std::ios::binary); + f.write(reinterpret_cast(values.data()),values.size()*4); + if(!f) throw std::runtime_error("output write failed: "+path); +} +int main(int argc,char ** argv) { + if(argc!=8 && argc!=5) { + std::cerr<<"usage: ds4v_vision_probe mmproj patches.f32 height width output-dir label stages(0|1)\n" + <<" ds4v_vision_probe mmproj --load-only dimension vocabulary\n"; return 2; + } + auto backend=ggml_backend_cpu_init(); + ggml_backend_cpu_set_n_threads(backend,2); + int status=0; + try { + VisionRuntime runtime; + std::string error; + bool load_only=argc==5 && std::string(argv[2])=="--load-only"; + int dimension=load_only?std::stoi(argv[3]):4096,vocabulary=load_only?std::stoi(argv[4]):129280; + if(!runtime.load(argv[1],backend,dimension,vocabulary,error)) throw std::runtime_error(error); + std::cout<<"weights_bytes="< & shape,const std::vector & values) { + save(output_dir+"/"+label+"-"+name+".f32",values); + std::cout<<"stage="< sentinel; + if(!runtime.sentinel(identity,sentinel,error) || sentinel.size()!=4096) throw std::runtime_error("sentinel failure"); + } + std::cout<<"output_shape="< Date: Fri, 4 Sep 2026 18:19:06 -0400 Subject: [PATCH 008/123] fix(ds4v): reserve measured scratch before graph allocation --- server/src/deepseek4/deepseek4_vision.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index b14a4dbd4..afe3d2d80 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -176,6 +176,7 @@ struct VisionRuntime::Impl { require(required<=MAX_SCRATCH,"vision scratch exceeds 2 GiB bound"); for(int i=0;i Date: Fri, 4 Sep 2026 18:21:16 -0400 Subject: [PATCH 009/123] test(ds4v): compare parent stages and reject malformed projectors --- server/tools/ds4v_vision/loader_tests.py | 48 +++++++++ server/tools/ds4v_vision/reference_stages.py | 103 +++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 server/tools/ds4v_vision/loader_tests.py create mode 100644 server/tools/ds4v_vision/reference_stages.py diff --git a/server/tools/ds4v_vision/loader_tests.py b/server/tools/ds4v_vision/loader_tests.py new file mode 100644 index 000000000..353c4c222 --- /dev/null +++ b/server/tools/ds4v_vision/loader_tests.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""Malformed GGUF table tests; rejected before allocating the sparse payload.""" +import argparse +from dataclasses import replace +from pathlib import Path +import subprocess +import sys +import tempfile + +sys.path.insert(0,str(Path(__file__).resolve().parents[1])) +import export_ds4v_mmproj as exporter +p=argparse.ArgumentParser() +p.add_argument('probe',type=Path) +p.add_argument('projector',type=Path) +a=p.parse_args() +tensors=[exporter.SourceTensor(name,Path('/unused'),shape,'BF16',0,2*__import__('math').prod(shape),0,0,0,0) + for name,shape in sorted(exporter.expected_shapes().items())] +original_metadata=exporter.metadata +cases=[('schema', 'deepseek4.vision.schema_version',2,'unsupported metadata'), + ('recipe','deepseek4.vision.image.layout_recipe','unknown','unsupported metadata'), + ('vocabulary','deepseek4.vision.vocabulary_size',1,'unsupported metadata'), + ('dimension','deepseek4.vision.language_embedding_length',1,'unsupported metadata'), + ('rope','deepseek4.vision.attention.rope_layout','adjacent','unsupported metadata'), + ('gelu','deepseek4.vision.aligner.activation','gelu-tanh','unsupported metadata')] +with tempfile.TemporaryDirectory(prefix='ds4v-loader-') as temporary: + root=Path(temporary) + for label,key,value,error in cases+[('missing',None,None,'wrong projector tensor count'), + ('shape',None,None,'wrong tensor shape'), + ('unknown',None,None,'unknown or duplicate tensor'), + ('truncated',None,None,'tensor outside file')]: + exporter.metadata=lambda: [(k,t,value if k==key else v) for k,t,v in original_metadata()] + selected=list(tensors) + if label=='missing': selected.pop() + if label=='shape': selected[0]=replace(selected[0],shape=(2048,),nbytes=4096) + if label=='unknown': selected[0]=replace(selected[0],name='unknown.tensor') + header,out=exporter._build_header(selected) + path=root/(label+'.gguf') + with path.open('wb') as f: + f.write(header) + if label!='truncated': f.truncate(len(header)+sum(exporter._align(t.nbytes) for t in selected)) + run=subprocess.run([str(a.probe),str(path),'--load-only','4096','129280'],text=True,capture_output=True) + assert run.returncode==1 and error in run.stderr,(label,run.returncode,run.stderr) + print('PASS',label,flush=True) + path.unlink() + for dim,vocab in [(4095,129280),(4096,129279)]: + run=subprocess.run([str(a.probe),str(a.projector),'--load-only',str(dim),str(vocab)],text=True,capture_output=True) + assert run.returncode==1 and 'language model dimension/vocabulary mismatch' in run.stderr,run.stderr + print('PASS','language contract',dim,vocab,flush=True) diff --git a/server/tools/ds4v_vision/reference_stages.py b/server/tools/ds4v_vision/reference_stages.py new file mode 100644 index 000000000..96eaa3599 --- /dev/null +++ b/server/tools/ds4v_vision/reference_stages.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Run the unmodified parent modules with observation hooks and compare stages. + +Use only with the original CPU fixture environment. Two Torch threads; no GPU. +Outputs metrics, and confirms instrumentation reproduces original final fixtures. +""" +import argparse +import hashlib +import json +from pathlib import Path +import sys +from types import SimpleNamespace +import numpy as np +import torch +from safetensors import safe_open + +p = argparse.ArgumentParser() +p.add_argument('source', type=Path) +p.add_argument('reference', type=Path) +p.add_argument('native', type=Path) +p.add_argument('output', type=Path) +a = p.parse_args() +torch.set_num_threads(2) +torch.set_num_interop_threads(2) +torch.set_default_dtype(torch.bfloat16) +sys.path.insert(0, str(a.source / 'inference')) +import vision +manifest = json.loads((a.reference / 'manifest.json').read_text()) +for name, expected in manifest['source_hashes'].items(): + assert hashlib.sha256((a.source / name).read_bytes()).hexdigest() == expected, name +config = json.loads((a.source / 'config.json').read_text()) +config['dim'] = config['hidden_size'] +args = SimpleNamespace(**config) +vit, aligner = vision.ViT(args).eval(), vision.Aligner(args).eval() +index = json.loads((a.source / 'model.safetensors.index.json').read_text())['weight_map'] +for prefix, module in [('vision.', vit), ('aligner.', aligner)]: + state = {} + for shard in sorted({v for k, v in index.items() if k.startswith(prefix)}): + with safe_open(a.source / shard, framework='pt', device='cpu') as f: + for name in f.keys(): + if name.startswith(prefix): + state[name[len(prefix):]] = f.get_tensor(name) + module.load_state_dict(state, strict=True) + del state +results = {} +label = None + +def compare(stage, value): + reference = value.detach().float().contiguous().numpy() + native_path = a.native / f'{label}-{stage}.f32' + actual = np.fromfile(native_path, np.float32).reshape(reference.shape) + x, y = actual.astype(np.float64).ravel(), reference.astype(np.float64).ravel() + delta = x-y + metrics = dict(shape=list(reference.shape), finite=bool(np.isfinite(x).all()), max_abs=float(np.abs(delta).max()), rmse=float(np.sqrt(np.mean(delta**2))), cosine=float(np.dot(x,y)/(np.linalg.norm(x)*np.linalg.norm(y))), exact_fraction=float(np.mean(x==y))) + results[label][stage] = metrics + print(label, stage, json.dumps(metrics), flush=True) + a.output.write_text(json.dumps(results, indent=2)+'\n') + +hooks=[] +for module, name in [(vit.patch_embed,'patch_embed'), (vit.blocks[0].norm1,'block0.norm1'), + (vit.blocks[0].attn.wqkv,'block0.qkv'), (vit.norm,'features'), + (aligner.w1,'aligner.w1'), (aligner.w2,'embeddings')]: + hooks.append(module.register_forward_hook(lambda m, inp, out, name=name: compare(name,out))) +for i, block in enumerate(vit.blocks): + hooks.append(block.register_forward_hook(lambda m, inp, out, i=i: compare(f'block{i}',out))) +hooks.append(aligner.w1.register_forward_pre_hook(lambda m, inp: compare('unfold',inp[0]))) +hooks.append(aligner.w2.register_forward_pre_hook(lambda m, inp: compare('aligner.gelu',inp[0]))) +original_rotary = vision.apply_rotary +original_sdpa = vision.F.scaled_dot_product_attention +rotary_count = 0 +attention_count = 0 + +def rotary(*args, **kwargs): + global rotary_count + out = original_rotary(*args, **kwargs) + if rotary_count < 2: + compare('block0.q' if rotary_count == 0 else 'block0.k', out) + rotary_count += 1 + return out + +def sdpa(*args, **kwargs): + global attention_count + out = original_sdpa(*args, **kwargs) + if attention_count == 0: + compare('block0.attention',out.transpose(0,1).reshape(out.shape[1],-1)) + attention_count += 1 + return out + +vision.apply_rotary = rotary +vision.F.scaled_dot_product_attention = sdpa +for label in ('corn','carrots'): + results[label] = {} + rotary_count = attention_count = 0 + meta = manifest['images'][label] + patches = torch.from_numpy(np.fromfile(a.reference / meta['patches']['file'],np.float32).reshape(meta['patches']['shape'])).to(torch.bfloat16) + with torch.inference_mode(): + features = vit(patches,*meta['vit_grid']) + embeddings = aligner(features,*meta['vit_grid']) + for name,value in [('features',features),('embeddings',embeddings)]: + original = np.fromfile(a.reference / meta[name]['file'],np.float32).reshape(meta[name]['shape']) + assert np.array_equal(original,value.float().numpy()), f'{label} instrumented parent {name} changed' + results[label]['original_fixture_bitwise_match'] = True + a.output.write_text(json.dumps(results,indent=2)+'\n') From 8758e092d7bb5b4d2facd5576bd5c487703ae7da Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:23:15 -0400 Subject: [PATCH 010/123] fix(ds4v): pin diagnostic snapshots independently of tensor views --- server/src/deepseek4/deepseek4_vision.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index afe3d2d80..dcb9e0bb2 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -92,7 +92,13 @@ struct Graph { } ~Graph() { ggml_free(c); } void stage(const std::string & name,Tensor * t,bool enabled) { - if (enabled) { ggml_set_output(t); stages.emplace_back(name,t); } + if (enabled) { + // An output flag on a view does not pin its underlying allocation. + // A dedicated snapshot prevents later in-place operations or allocator + // reuse from corrupting diagnostic values (notably reshape/unfold). + auto snapshot=ggml_dup(c,t); + ggml_set_output(snapshot); stages.emplace_back(name,snapshot); + } } }; std::vector read(Tensor * t) { @@ -167,6 +173,7 @@ struct VisionRuntime::Impl { const StageObserver & observer,Tensor * cosine=nullptr,Tensor * sine=nullptr, const std::vector & cos_values={},const std::vector & sin_values={}) { ggml_set_input(input); ggml_set_output(output); + for(auto & stage:g.stages) ggml_build_forward_expand(g.graph,stage.second); ggml_build_forward_expand(g.graph,output); if(!allocator) allocator=ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend)); require(allocator!=nullptr,"scratch allocator creation failed"); From 8bac57689d7fad7427923a18e75b1fec043b156c Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:24:23 -0400 Subject: [PATCH 011/123] test(ds4v): isolate block kernels with native inputs and BF16 ULP metrics --- server/tools/ds4v_vision/reference_stages.py | 28 ++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/server/tools/ds4v_vision/reference_stages.py b/server/tools/ds4v_vision/reference_stages.py index 96eaa3599..f11ef687d 100644 --- a/server/tools/ds4v_vision/reference_stages.py +++ b/server/tools/ds4v_vision/reference_stages.py @@ -44,6 +44,7 @@ del state results = {} label = None +mode = "original" def compare(stage, value): reference = value.detach().float().contiguous().numpy() @@ -52,8 +53,14 @@ def compare(stage, value): x, y = actual.astype(np.float64).ravel(), reference.astype(np.float64).ravel() delta = x-y metrics = dict(shape=list(reference.shape), finite=bool(np.isfinite(x).all()), max_abs=float(np.abs(delta).max()), rmse=float(np.sqrt(np.mean(delta**2))), cosine=float(np.dot(x,y)/(np.linalg.norm(x)*np.linalg.norm(y))), exact_fraction=float(np.mean(x==y))) - results[label][stage] = metrics - print(label, stage, json.dumps(metrics), flush=True) + # Monotone BF16 bit ordering gives ULP distances, including negatives. + def ordered_bf16(value): + bits = np.ascontiguousarray(value, dtype=np.float32).view(np.uint32) >> 16 + return np.where(bits & 0x8000, 0x8000 - (bits & 0x7fff), 0x8000 + bits).astype(np.int32) + ulps = np.abs(ordered_bf16(x)-ordered_bf16(y)) + metrics.update(bf16_ulp_max=int(ulps.max()), bf16_ulp_p99=float(np.percentile(ulps,99)), bf16_within_one_ulp=float(np.mean(ulps<=1))) + results[label][stage if mode == 'original' else mode + '.' + stage] = metrics + print(label, mode, stage, json.dumps(metrics), flush=True) a.output.write_text(json.dumps(results, indent=2)+'\n') hooks=[] @@ -90,6 +97,7 @@ def sdpa(*args, **kwargs): vision.F.scaled_dot_product_attention = sdpa for label in ('corn','carrots'): results[label] = {} + mode = 'original' rotary_count = attention_count = 0 meta = manifest['images'][label] patches = torch.from_numpy(np.fromfile(a.reference / meta['patches']['file'],np.float32).reshape(meta['patches']['shape'])).to(torch.bfloat16) @@ -101,3 +109,19 @@ def sdpa(*args, **kwargs): assert np.array_equal(original,value.float().numpy()), f'{label} instrumented parent {name} changed' results[label]['original_fixture_bitwise_match'] = True a.output.write_text(json.dumps(results,indent=2)+'\n') + + # Isolate kernel/rounding effects: every parent block receives the exact + # native preceding residual, avoiding cumulative differences from earlier blocks. + mode = 'same_input' + cos, sin = vision.get_vision_cos_sin(*meta['vit_grid'], vit.rope_dim, vit.rope_theta) + with torch.inference_mode(): + for i, block in enumerate(vit.blocks): + previous = 'patch_embed' if i == 0 else f'block{i-1}' + native_input = torch.from_numpy(np.fromfile(a.native / f'{label}-{previous}.f32', np.float32).reshape(-1,1024)).to(torch.bfloat16) + rotary_count = attention_count = 0 if i == 0 else 100 + block(native_input,cos,sin) + native_last = torch.from_numpy(np.fromfile(a.native / f'{label}-block31.f32',np.float32).reshape(-1,1024)).to(torch.bfloat16) + vit.norm(native_last) + native_features = torch.from_numpy(np.fromfile(a.native / f'{label}-features.f32',np.float32).reshape(-1,1024)).to(torch.bfloat16) + aligner(native_features,*meta['vit_grid']) + a.output.write_text(json.dumps(results,indent=2)+'\n') From d26f1c9cc9d2e5030a6ee0e463f74b03de3e1119 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:26:33 -0400 Subject: [PATCH 012/123] test(ds4v): verify transactional reload and strict tensor table rejection --- server/src/deepseek4/deepseek4_vision.cpp | 2 +- server/tools/ds4v_vision/loader_tests.py | 18 +++++++++++++++++- server/tools/ds4v_vision/probe.cpp | 5 +++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index dcb9e0bb2..b591e1a1b 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -211,7 +211,7 @@ bool VisionRuntime::load(const std::string & path,ggml_backend_t backend,int dim auto expected=inventory(); require(gguf_get_n_tensors(meta.g)==int64_t(expected.size()),"wrong projector tensor count"); common::GgufMmap mapped; - require(mapped.open(path,error),error); + if(!mapped.open(path,error)) throw std::runtime_error(error); std::vector> ranges; for(int64_t i=0;i sentinel_after_reload; + if(!runtime.sentinel(Sentinel::Start,sentinel_after_reload,error)) throw std::runtime_error("failed reload lost sentinels"); + if(runtime.sentinel(static_cast(99),sentinel_after_reload,error)) throw std::runtime_error("invalid sentinel accepted"); const auto patches=read_file(argv[2]); PatchGrid grid{std::stoi(argv[3]),std::stoi(argv[4])}; const std::string output_dir=argv[5],label=argv[6]; From da3fa6e2ced957807a770ee4dbf735286a68c07b Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:27:49 -0400 Subject: [PATCH 013/123] test(ds4v): measure parent BF16 Math SDPA sensitivity --- server/tools/ds4v_vision/reference_math.py | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 server/tools/ds4v_vision/reference_math.py diff --git a/server/tools/ds4v_vision/reference_math.py b/server/tools/ds4v_vision/reference_math.py new file mode 100644 index 000000000..e733ac2b6 --- /dev/null +++ b/server/tools/ds4v_vision/reference_math.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""Parent BF16 SDPA Math sensitivity, never a replacement fixture or parity gate.""" +import argparse +import hashlib +import json +from pathlib import Path +import sys +from types import SimpleNamespace +import numpy as np +import torch +from safetensors import safe_open +from torch.nn.attention import sdpa_kernel, SDPBackend + +p=argparse.ArgumentParser() +p.add_argument('source',type=Path) +p.add_argument('reference',type=Path) +p.add_argument('native',type=Path) +p.add_argument('output',type=Path) +a=p.parse_args() +torch.set_num_threads(2) +torch.set_num_interop_threads(2) +torch.set_default_dtype(torch.bfloat16) +sys.path.insert(0,str(a.source/'inference')) +from vision import ViT, Aligner +manifest=json.loads((a.reference/'manifest.json').read_text()) +for name,digest in manifest['source_hashes'].items(): + assert hashlib.sha256((a.source/name).read_bytes()).hexdigest()==digest +config=json.loads((a.source/'config.json').read_text()) +config['dim']=config['hidden_size'] +vit,aligner=ViT(SimpleNamespace(**config)).eval(),Aligner(SimpleNamespace(**config)).eval() +index=json.loads((a.source/'model.safetensors.index.json').read_text())['weight_map'] +for prefix,module in [('vision.',vit),('aligner.',aligner)]: + state={} + for shard in sorted({v for k,v in index.items() if k.startswith(prefix)}): + with safe_open(a.source/shard,framework='pt',device='cpu') as f: + for name in f.keys(): + if name.startswith(prefix): state[name[len(prefix):]]=f.get_tensor(name) + module.load_state_dict(state,strict=True) + del state + +def metrics(x,y): + assert x.shape==y.shape + x,y=x.astype(np.float64),y.astype(np.float64) + delta=x-y + return dict(shape=list(x.shape),finite=bool(np.isfinite(x).all() and np.isfinite(y).all()),max_abs=float(np.abs(delta).max()),rmse=float(np.sqrt(np.mean(delta**2))),cosine=float(np.dot(x.ravel(),y.ravel())/(np.linalg.norm(x)*np.linalg.norm(y))),exact_fraction=float(np.mean(x==y))) + +results={'torch':torch.__version__,'backend':'SDPBackend.MATH','precision':'original BF16 modules, math SDPA default F32 reduction','images':{}} +a.output.mkdir(parents=True,exist_ok=True) +for label in ('corn','carrots'): + entry=manifest['images'][label] + patches=torch.from_numpy(np.fromfile(a.reference/entry['patches']['file'],np.float32).reshape(entry['patches']['shape'])).to(torch.bfloat16) + with torch.inference_mode(),sdpa_kernel(SDPBackend.MATH): + features=vit(patches,*entry['vit_grid']) + embeddings=aligner(features,*entry['vit_grid']) + results['images'][label]={} + for name,tensor in [('features',features),('embeddings',embeddings)]: + math=tensor.float().contiguous().numpy() + math.tofile(a.output/f'{label}-{name}.f32') + original=np.fromfile(a.reference/entry[name]['file'],np.float32).reshape(entry[name]['shape']) + native=np.fromfile(a.native/f'{label}-{name}.f32',np.float32).reshape(entry[name]['shape']) + results['images'][label][name]={'math_vs_original_cpu_flash':metrics(math,original),'native_vs_math':metrics(native,math)} + (a.output/'comparison.json').write_text(json.dumps(results,indent=2)+'\n') + print(label,json.dumps(results['images'][label]),flush=True) From 1cf2b260b0e820b8bfcba4385547cbaab11fc32d Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:29:15 -0400 Subject: [PATCH 014/123] test(ds4v): identify actual parent SDPA dispatch and parser rejection --- server/tools/ds4v_vision/attention_dispatch.py | 18 ++++++++++++++++++ server/tools/ds4v_vision/loader_tests.py | 4 ++-- server/tools/ds4v_vision/reference_math.py | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 server/tools/ds4v_vision/attention_dispatch.py diff --git a/server/tools/ds4v_vision/attention_dispatch.py b/server/tools/ds4v_vision/attention_dispatch.py new file mode 100644 index 000000000..fbcb65144 --- /dev/null +++ b/server/tools/ds4v_vision/attention_dispatch.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +"""Record actual PyTorch attention dispatch for source-shaped 3D Q/K/V.""" +import json +import sys +from pathlib import Path +import numpy as np +import torch + +torch.set_num_threads(2) +torch.set_num_interop_threads(2) +native=Path(sys.argv[1]) +q=torch.from_numpy(np.fromfile(native/'corn-block0.q.f32',np.float32).reshape(782,16,64)).to(torch.bfloat16).transpose(0,1) +k=torch.from_numpy(np.fromfile(native/'corn-block0.k.f32',np.float32).reshape(782,16,64)).to(torch.bfloat16).transpose(0,1) +qkv=torch.from_numpy(np.fromfile(native/'corn-block0.qkv.f32',np.float32).reshape(782,3072)).to(torch.bfloat16) +v=qkv.chunk(3,dim=-1)[2].reshape(782,16,64).transpose(0,1) +with torch.inference_mode(),torch.profiler.profile(activities=[torch.profiler.ProfilerActivity.CPU]) as profile: + result=torch.nn.functional.scaled_dot_product_attention(q,k,v) +print(json.dumps({'torch':torch.__version__,'q_shape':list(q.shape),'q_stride':list(q.stride()),'k_stride':list(k.stride()),'v_stride':list(v.stride()),'output_dtype':str(result.dtype),'operations':[e.key for e in profile.key_averages()]},indent=2)) diff --git a/server/tools/ds4v_vision/loader_tests.py b/server/tools/ds4v_vision/loader_tests.py index 8b30a1b63..ffff54ac2 100644 --- a/server/tools/ds4v_vision/loader_tests.py +++ b/server/tools/ds4v_vision/loader_tests.py @@ -31,7 +31,7 @@ ('dtype',None,None,'wrong tensor dtype'), ('metadata_type',None,None,'missing or wrong metadata type'), ('missing_metadata',None,None,'missing or wrong metadata type'), - ('overlap',None,None,'overlapping tensor data')]: + ('overlap',None,None,'could not parse vision GGUF')]: exporter.metadata=lambda: [(k,t,value if k==key else v) for k,t,v in original_metadata()] if label=='metadata_type': exporter.metadata=lambda: [(k,'string' if k=='deepseek4.vision.schema_version' else t,v) for k,t,v in original_metadata()] @@ -48,7 +48,7 @@ tensor=selected[0 if label=='dtype' else 1] name=exporter._pack_string(tensor.name) type_offset=header.index(name)+len(name)+4+8*len(tensor.shape) - if label=='dtype': struct.pack_into(' Date: Fri, 4 Sep 2026 18:30:52 -0400 Subject: [PATCH 015/123] fix(ds4v): preserve parent Math SDPA operand scaling order --- server/src/deepseek4/deepseek4_vision.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index b591e1a1b..6bded8f55 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -129,9 +129,13 @@ Tensor * rotate(ggml_context * c,Tensor * x,Tensor * cosine,Tensor * sine) { Tensor * attention(ggml_context * c,Tensor * q,Tensor * k,Tensor * v) { q=ggml_cont(c,ggml_permute(c,q,0,2,1,3)); k=ggml_cont(c,ggml_permute(c,k,0,2,1,3)); - auto scores=ggml_mul_mat(c,k,q); + // PyTorch 2.10 Math SDPA (the source's 3D input dispatch) scales both + // operands in F32 before GEMM. Preserve this order and its rounding; + // scaling the scores afterwards is algebraically equal but not bit equal. + const float scale=float(std::sqrt(1.0/std::sqrt(double(q->ne[0])))); + auto scores=ggml_mul_mat(c,ggml_scale(c,k,scale),ggml_scale(c,q,scale)); ggml_mul_mat_set_prec(scores,GGML_PREC_F32); - auto probabilities=ggml_soft_max(c,ggml_scale(c,scores,1.f/std::sqrt(float(q->ne[0])))); + auto probabilities=ggml_soft_max(c,scores); v=ggml_cont(c,ggml_permute(c,v,1,2,0,3)); // [N, D, heads] auto out=ggml_mul_mat(c,v,probabilities); ggml_mul_mat_set_prec(out,GGML_PREC_F32); From 2bfba862636ed9d96d7acfacad1d407da4dbb22a Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:32:24 -0400 Subject: [PATCH 016/123] test(ds4v): cover multihead attention and BF16 halfway rounding --- server/tools/ds4v_vision/geometry.cpp | 34 ++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/server/tools/ds4v_vision/geometry.cpp b/server/tools/ds4v_vision/geometry.cpp index 9c4a12cf3..5de8d343f 100644 --- a/server/tools/ds4v_vision/geometry.cpp +++ b/server/tools/ds4v_vision/geometry.cpp @@ -56,6 +56,38 @@ int main() { auto out=t.run(detail::attention(t.c,q,k,v)); for(int i=0;i<3;++i) { check(out[2*i]==5,"bidirectional attention mismatch"); check(out[2*i+1]==6,"attention channel mismatch"); } } + { + Test t; + const int heads=2,n=3,d=4; + std::vector q(n*heads*d),k(q.size()),v(q.size()); + for(int p=0;p({1.f,1.015625f,-1.f,-1.015625f}),"BF16 halfway rounding mismatch"); + } { Test t; const int h=4,w=5,c=2; @@ -76,7 +108,7 @@ int main() { auto out=t.run(ggml_gelu_erf(t.c,t.input(7,1,1,x))); for(int i=0;i<7;++i) check(std::abs(out[i]-.5f*x[i]*(1+std::erf(x[i]/std::sqrt(2.f))))<1e-6f,"exact erf GELU mismatch"); } - std::cout<<"PASS: half-split 2D RoPE, full bidirectional attention, padded channel-first unfold, exact erf GELU\n"; + std::cout<<"PASS: half-split 2D RoPE, full bidirectional/multihead attention, BF16 halfway rounding, padded channel-first unfold, exact erf GELU\n"; return 0; } catch(const std::exception & e) { std::cerr< Date: Fri, 4 Sep 2026 18:34:46 -0400 Subject: [PATCH 017/123] docs(ds4v): record CPU qualification and remaining numerical limits --- server/tools/ds4v_vision/README.md | 126 ++++++++++++++++++++++++++++ server/tools/ds4v_vision/compare.py | 2 + 2 files changed, 128 insertions(+) create mode 100644 server/tools/ds4v_vision/README.md diff --git a/server/tools/ds4v_vision/README.md b/server/tools/ds4v_vision/README.md new file mode 100644 index 000000000..39c516389 --- /dev/null +++ b/server/tools/ds4v_vision/README.md @@ -0,0 +1,126 @@ +# Native DS4V vision runtime (Candidate A) + +Reusable, backend-owned projector/tower implementation. `VisionRuntime` borrows +one caller-selected backend and owns its validated BF16 weight buffer plus a +reusable graph allocator. Load is transactional: a rejected reload preserves the +prior runtime. No HTTP, decoder, image decode, sentinel placement, or N-layout +changes are included. `Sentinel` exposes the four learned delimiter vectors; +image rows are the `VisionOutput.embeddings` result. + +The loader checks all exporter semantic fields and all 267 names, BF16 dtypes, +shapes, alignment, and file bounds before backend allocation. It rejects unknown +schema, layout, activation, language dimension/vocabulary, missing/extra tensors, +and malformed GGUF metadata. Original source names and weight bytes are retained. +The accepted artifact has no source-repository or source-hash metadata; provenance +is verified externally against the accepted exporter hash, not invented by the +runtime. + +## Arithmetic and resource rationale + +F32 graph tensors hold BF16-rounded activations. `cast(BF16)` then `cast(F32)` +preserves every source BF16 boundary: biased linear, RMSNorm with F32 weights, +rotary Q/K, attention output, residual additions, SiLU, gated product, and each +aligner operation. Normalization and rotary arithmetic remain F32. GELU uses +`ggml_gelu_erf`. Q/K use explicit F32 cosine/sine tables with half-split channel +pairs and height frequencies before width frequencies. Im2col performs exact +channel-first, bottom/right padded 3x3 unfolding. + +Actual Torch 2.10 CPU tracing shows the source's **3D** SDPA dispatches +`aten::_scaled_dot_product_attention_math`. That implementation scales both F32 +Q and K by sqrt(1/sqrt(head_dimension)) before matrix multiplication, so this +runtime preserves the same operation order. Scaling scores afterwards changed +rounding enough to measurably worsen both full fixtures. See +[PyTorch Math SDPA source](https://github.com/pytorch/pytorch/blob/v2.10.0/aten/src/ATen/native/transformers/attention.cpp#L807-L891). +Explicit full softmax attention has no causal mask. CPUFlash is not the source +fixture path: forced Math reproduces both original fixtures bit for bit. + +One block graph at a time bounds quadratic scratch, with a hard 2 GiB graph +buffer limit measured before allocation. Input grids are capped at 384 aligner +rows and 3456 patches. Diagnostic tensors are independent snapshots; a flag on +a view alone does not protect its backing allocation. No attention matrices are +retained across blocks. Caller diagnostics run synchronously. Each block currently +returns its F32 residual to host and uploads it into the following graph; HIP +transfer cost and GPU behavior are unqualified. `release_scratch()` frees graph +buffers while keeping weights/sentinels. Sequential calls only. + +Alternatives considered: built-in VISION RoPE has different frequency recurrence +rounding; adjacent-pair text RoPE is mathematically wrong. Flash attention would +add an unqualified kernel/dtype path. An all-F32 tower removes required source +rounding; F16 weights are not a lossless BF16 substitute. Keeping all 32 graphs +or attention diagnostics would multiply quadratic scratch. The chosen explicit +primitive graph makes each stage inspectable at the cost of host transfers. + +## Standalone CPU qualification + +Run these commands on `soulf`, from its isolated candidate worktree. The Mac is +for authorship only. CPU builds use two jobs; probe and reference execution use +two threads. Original fixtures and reference environment remain read-only. + +```sh +cmake -S server/tools/ds4v_vision -B /tmp/ds4v-tower-a-build -DCMAKE_BUILD_TYPE=Release +cmake --build /tmp/ds4v-tower-a-build -j2 +OMP_NUM_THREADS=2 ctest --test-dir /tmp/ds4v-tower-a-build --output-on-failure +python3 server/tools/ds4v_vision/loader_tests.py /tmp/ds4v-tower-a-build/ds4v_vision_probe /home/marcelorm/ds4v-work/ds4v-mmproj.gguf +``` + +Full probes (the last argument enables independent stage snapshots): + +```sh +OMP_NUM_THREADS=2 /usr/bin/time -v /tmp/ds4v-tower-a-build/ds4v_vision_probe /home/marcelorm/ds4v-work/ds4v-mmproj.gguf /home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference/corn-patches.f32 23 34 artifacts/vision-tower-a/native corn 1 +OMP_NUM_THREADS=2 /usr/bin/time -v /tmp/ds4v-tower-a-build/ds4v_vision_probe /home/marcelorm/ds4v-work/ds4v-mmproj.gguf /home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference/carrots-patches.f32 42 61 artifacts/vision-tower-a/native carrots 1 +``` + +Use `/home/marcelorm/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python` +with `OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2` for these tools: + +- `compare.py REFERENCE NATIVE --output comparison.json`: original fixture + hashes, raster shapes, finite values, max absolute error, RMSE, cosine. +- `reference_stages.py SOURCE REFERENCE NATIVE stages.json`: original source + hooks reproduce both fixture finals bitwise, then each original block consumes + the native incoming residual to isolate local arithmetic from accumulated drift. + Includes BF16 ULP distances; large max ULP distances across zero should be read + with absolute error and p99, not interpreted as uniform relative error. +- `reference_math.py SOURCE REFERENCE NATIVE OUTPUT_DIR`: labeled parent-only + Math sensitivity. It preserves original fixtures and is not a new tolerance. +- `attention_dispatch.py NATIVE`: records operator dispatch with source Q/K/V + shapes, strides, and BF16 dtype. + +`SOURCE` is `/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored`. +`REFERENCE` is `/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference`. +Evidence is `/home/marcelorm/lucebox-ds4v-tower-a/artifacts/vision-tower-a`. +The projector SHA256 is +`58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c`. + +## Verdict: ISSUES (numerical acceptance remains open) + +Geometry checks, strict loader rejection, finite shapes, transactional reload, +sentinel access, and scratch release pass. Six deterministic arithmetic/geometry +checks are in CTest; sixteen malformed projector/language cases are in the loader +script. Corn observer-off and observer-on finals match bitwise. All graphs execute +on CPU without GPU, decoder, or HTTP changes. + +Final native graph versus **original** CPU fixtures: + +| Image/output | Shape | Max absolute | RMSE | Cosine | +|---|---|---:|---:|---:| +| carrots features | 2562 x 1024 | 0.1376953125 | 0.00259378329 | 0.99967582518 | +| carrots embeddings | 294 x 4096 | 0.02642822266 | 0.00142200006 | 0.99981156934 | +| corn features | 782 x 1024 | 1.484375 | 0.00629541949 | 0.99822935535 | +| corn embeddings | 96 x 4096 | 0.09423828125 | 0.00319258993 | 0.99907754975 | + +All four outputs are finite. These are measured errors, not a claimed parity +threshold. Differences begin at patch embedding and accumulate through BF16 +residuals. Stage diagnostics isolate sharp amplification at blocks 12 and 31. +Same-input unfolding is bitwise exact for both fixtures, and same-input final +norm/aligner comparisons isolate small local kernel errors. The source Math +attention check does not explain away remaining end-to-end drift. No numerical +tolerance was widened. Full decoder logits, GPU transfer cost, and HIP arithmetic +remain unqualified. + +Final CPU resource measurements with stage snapshots enabled: weights 932,786,176 +bytes; corn scratch 77,774,592 bytes and 3.30829 s encode; carrots scratch +546,669,312 bytes and 17.6707 s encode. Peak process RSS was 1,827,056 KiB +(about 1.74 GiB), including the transient read-only mapped weight source during +load. The configured 2 GiB graph scratch cap is distinct from total process RSS. +Largest permitted grids are bounded analytically and by allocation measurement; +only the original 782/2562-patch grids have full numerical reference qualification. diff --git a/server/tools/ds4v_vision/compare.py b/server/tools/ds4v_vision/compare.py index 5b2278707..1283345f4 100644 --- a/server/tools/ds4v_vision/compare.py +++ b/server/tools/ds4v_vision/compare.py @@ -15,6 +15,8 @@ results = {} for label, entry in manifest['images'].items(): results[label] = {} + patches = entry['patches'] + assert hashlib.sha256((a.reference / patches['file']).read_bytes()).hexdigest() == patches['sha256'] for stage in ('features', 'embeddings'): meta = entry[stage] reference_path = a.reference / meta['file'] From 8e28cfb95faba172a1e29da18859ea70168a1a45 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:46:52 -0400 Subject: [PATCH 018/123] test(ds4v): isolate vendored GGUF reader import --- server/tests/test_export_ds4v_mmproj.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/server/tests/test_export_ds4v_mmproj.py b/server/tests/test_export_ds4v_mmproj.py index 1304de212..6a20cf922 100644 --- a/server/tests/test_export_ds4v_mmproj.py +++ b/server/tests/test_export_ds4v_mmproj.py @@ -1,9 +1,11 @@ #!/usr/bin/env python3 +import importlib import importlib.util import json import struct import sys import tempfile +import types import unittest from pathlib import Path from unittest import mock @@ -154,14 +156,27 @@ def test_lossless_export_has_names_shapes_types_bytes_and_metadata(self): self.assertEqual(raw[data_start + offset:data_start + offset + len(payloads[name])], payloads[name]) def test_vendored_gguf_reader_accepts_output(self): - gguf_path = REPO_ROOT / "server" / "deps" / "llama.cpp" / "gguf-py" - sys.path.insert(0, str(gguf_path)) + gguf_path = REPO_ROOT / "server" / "deps" / "llama.cpp" / "gguf-py" / "gguf" try: - import gguf + import numpy # noqa: F401 except ImportError as exc: self.skipTest("vendored GGUF reader dependency unavailable: %s" % exc) + + saved_modules = {name: module for name, module in sys.modules.items() + if name == "gguf" or name.startswith("gguf.")} + for name in saved_modules: + del sys.modules[name] + package = types.ModuleType("gguf") + package.__path__ = [str(gguf_path)] + sys.modules["gguf"] = package + try: + gguf_reader = importlib.import_module("gguf.gguf_reader") finally: - sys.path.pop(0) + loaded_modules = [name for name in sys.modules + if name == "gguf" or name.startswith("gguf.")] + for name in loaded_modules: + del sys.modules[name] + sys.modules.update(saved_modules) with tempfile.TemporaryDirectory() as temp: root = Path(temp) @@ -169,7 +184,7 @@ def test_vendored_gguf_reader_accepts_output(self): output = root / "vision.gguf" self.export_small(make_source(root, entries, payload), output) - reader = gguf.GGUFReader(output) + reader = gguf_reader.GGUFReader(output) self.assertEqual(reader.get_field("general.architecture").contents(), "deepseek4_vision") self.assertEqual(len(reader.tensors), len(TEST_SHAPES)) for tensor in reader.tensors: From 782bf157e1f63031f017ddada7b0f33fe856bbf6 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:47:43 -0400 Subject: [PATCH 019/123] Revert "test(ds4v): isolate vendored GGUF reader import" This reverts commit 8e28cfb95faba172a1e29da18859ea70168a1a45. --- server/tests/test_export_ds4v_mmproj.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/server/tests/test_export_ds4v_mmproj.py b/server/tests/test_export_ds4v_mmproj.py index 6a20cf922..1304de212 100644 --- a/server/tests/test_export_ds4v_mmproj.py +++ b/server/tests/test_export_ds4v_mmproj.py @@ -1,11 +1,9 @@ #!/usr/bin/env python3 -import importlib import importlib.util import json import struct import sys import tempfile -import types import unittest from pathlib import Path from unittest import mock @@ -156,27 +154,14 @@ def test_lossless_export_has_names_shapes_types_bytes_and_metadata(self): self.assertEqual(raw[data_start + offset:data_start + offset + len(payloads[name])], payloads[name]) def test_vendored_gguf_reader_accepts_output(self): - gguf_path = REPO_ROOT / "server" / "deps" / "llama.cpp" / "gguf-py" / "gguf" + gguf_path = REPO_ROOT / "server" / "deps" / "llama.cpp" / "gguf-py" + sys.path.insert(0, str(gguf_path)) try: - import numpy # noqa: F401 + import gguf except ImportError as exc: self.skipTest("vendored GGUF reader dependency unavailable: %s" % exc) - - saved_modules = {name: module for name, module in sys.modules.items() - if name == "gguf" or name.startswith("gguf.")} - for name in saved_modules: - del sys.modules[name] - package = types.ModuleType("gguf") - package.__path__ = [str(gguf_path)] - sys.modules["gguf"] = package - try: - gguf_reader = importlib.import_module("gguf.gguf_reader") finally: - loaded_modules = [name for name in sys.modules - if name == "gguf" or name.startswith("gguf.")] - for name in loaded_modules: - del sys.modules[name] - sys.modules.update(saved_modules) + sys.path.pop(0) with tempfile.TemporaryDirectory() as temp: root = Path(temp) @@ -184,7 +169,7 @@ def test_vendored_gguf_reader_accepts_output(self): output = root / "vision.gguf" self.export_small(make_source(root, entries, payload), output) - reader = gguf_reader.GGUFReader(output) + reader = gguf.GGUFReader(output) self.assertEqual(reader.get_field("general.architecture").contents(), "deepseek4_vision") self.assertEqual(len(reader.tensors), len(TEST_SHAPES)) for tensor in reader.tensors: From 5845ed53fe4825f8394209094b42c7f3df19501a Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:48:55 -0400 Subject: [PATCH 020/123] test(ds4v): cover complete image token budget --- server/tools/ds4v_vision/probe.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/tools/ds4v_vision/probe.cpp b/server/tools/ds4v_vision/probe.cpp index 1f680e6ff..3a10dcf64 100644 --- a/server/tools/ds4v_vision/probe.cpp +++ b/server/tools/ds4v_vision/probe.cpp @@ -36,6 +36,14 @@ int main(int argc,char ** argv) { if(!runtime.load(argv[1],backend,dimension,vocabulary,error)) throw std::runtime_error(error); std::cout<<"weights_bytes="<>{ + {{48,72},false}, {{3,564},false}, {{6,564},false}, + {{3,3},true}, {{3,561},true}, {{6,561},true}, {{564,3},true}}) { + VisionOutput rejected; + if(runtime.encode({},check.first,rejected,error)) throw std::runtime_error("empty patches accepted"); + const auto expected=check.second?"patch count/shape mismatch":"patch grid exceeds image token budget"; + if(error!=expected) throw std::runtime_error("grid budget check: expected "+std::string(expected)+", got "+error); + } if(runtime.load(argv[1],backend,4095,129280,error)) throw std::runtime_error("incompatible reload accepted"); if(!runtime.config() || runtime.weight_bytes()==0) throw std::runtime_error("failed reload destroyed runtime"); std::vector sentinel_after_reload; From 5bf705ed881e94c0bdd1b975fa49063fb1c5dbd2 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:51:26 -0400 Subject: [PATCH 021/123] fix(ds4v): bound the complete image block before encoding --- server/src/deepseek4/deepseek4_vision.cpp | 8 ++++++-- server/tools/ds4v_vision/README.md | 14 +++++++++----- server/tools/ds4v_vision/compare.py | 13 +++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 6bded8f55..0873b2930 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -255,8 +255,12 @@ bool VisionRuntime::encode(const std::vector & patches,PatchGrid grid,Vis require(bool(impl_),"vision runtime is not loaded"); require(grid.height>0 && grid.width>0 && grid.height<=1152 && grid.width<=1152,"invalid patch grid"); const int64_t n=int64_t(grid.height)*grid.width; - const int64_t rows=int64_t((grid.height+2)/3)*((grid.width+2)/3); - require(rows<=impl_->config.max_image_tokens && n<=3456,"patch grid exceeds image token budget"); + const int64_t aligner_height=(grid.height+2)/3, aligner_width=(grid.width+2)/3; + const int64_t rows=aligner_height*aligner_width; + const int64_t padded_height=aligner_height+aligner_height%2; + const int64_t row_length=aligner_width+1; + const int64_t block_tokens=padded_height*row_length+2+(padded_height/2*row_length%2)*2; + require(block_tokens+3<=impl_->config.max_image_tokens,"patch grid exceeds image token budget"); require(patches.size()==size_t(n)*588,"patch count/shape mismatch"); for(float value:patches) require(std::isfinite(value),"non-finite image patch"); std::vector x; diff --git a/server/tools/ds4v_vision/README.md b/server/tools/ds4v_vision/README.md index 39c516389..5ad1525cb 100644 --- a/server/tools/ds4v_vision/README.md +++ b/server/tools/ds4v_vision/README.md @@ -1,4 +1,4 @@ -# Native DS4V vision runtime (Candidate A) +# Native DS4V vision runtime Reusable, backend-owned projector/tower implementation. `VisionRuntime` borrows one caller-selected backend and owns its validated BF16 weight buffer plus a @@ -35,8 +35,9 @@ Explicit full softmax attention has no causal mask. CPUFlash is not the source fixture path: forced Math reproduces both original fixtures bit for bit. One block graph at a time bounds quadratic scratch, with a hard 2 GiB graph -buffer limit measured before allocation. Input grids are capped at 384 aligner -rows and 3456 patches. Diagnostic tensors are independent snapshots; a flag on +buffer limit measured before allocation. Input grids must fit the complete +384-token N-layout budget, including delimiters, row/odd-row/parity padding, +and three reserved leading alignment tokens. Diagnostic tensors are independent snapshots; a flag on a view alone does not protect its backing allocation. No attention matrices are retained across blocks. Caller diagnostics run synchronously. Each block currently returns its F32 residual to host and uploads it into the following graph; HIP @@ -75,6 +76,9 @@ with `OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2` for these tools: - `compare.py REFERENCE NATIVE --output comparison.json`: original fixture hashes, raster shapes, finite values, max absolute error, RMSE, cosine. + It exits 3 when the unchanged Candidate B gates fail: feature maxabs <=0.25, + RMSE <=0.03, cosine >=0.9995; embedding maxabs <=0.75, RMSE <=0.08, + cosine >=0.9990. These gates were fixed before the candidate fixture runs. - `reference_stages.py SOURCE REFERENCE NATIVE stages.json`: original source hooks reproduce both fixture finals bitwise, then each original block consumes the native incoming residual to isolate local arithmetic from accumulated drift. @@ -108,8 +112,8 @@ Final native graph versus **original** CPU fixtures: | corn features | 782 x 1024 | 1.484375 | 0.00629541949 | 0.99822935535 | | corn embeddings | 96 x 4096 | 0.09423828125 | 0.00319258993 | 0.99907754975 | -All four outputs are finite. These are measured errors, not a claimed parity -threshold. Differences begin at patch embedding and accumulate through BF16 +All four outputs are finite. Corn fails the fixed feature gate, so the comparison +command returns 3. Differences begin at patch embedding and accumulate through BF16 residuals. Stage diagnostics isolate sharp amplification at blocks 12 and 31. Same-input unfolding is bitwise exact for both fixtures, and same-input final norm/aligner comparisons isolate small local kernel errors. The source Math diff --git a/server/tools/ds4v_vision/compare.py b/server/tools/ds4v_vision/compare.py index 1283345f4..8b044aabd 100644 --- a/server/tools/ds4v_vision/compare.py +++ b/server/tools/ds4v_vision/compare.py @@ -6,6 +6,11 @@ from pathlib import Path import numpy as np +GATES = { + 'features': dict(max_abs=0.25, rmse=0.03, cosine=0.9995), + 'embeddings': dict(max_abs=0.75, rmse=0.08, cosine=0.9990), +} + p = argparse.ArgumentParser() p.add_argument('reference', type=Path) p.add_argument('native', type=Path) @@ -13,6 +18,7 @@ a = p.parse_args() manifest = json.loads((a.reference / 'manifest.json').read_text()) results = {} +passed = True for label, entry in manifest['images'].items(): results[label] = {} patches = entry['patches'] @@ -28,5 +34,12 @@ delta = actual.astype(np.float64) - ref.astype(np.float64) cosine = np.dot(actual.astype(np.float64), ref.astype(np.float64)) / (np.linalg.norm(actual.astype(np.float64)) * np.linalg.norm(ref.astype(np.float64))) results[label][stage] = dict(shape=meta['shape'], finite=finite, max_abs=float(np.abs(delta).max()), rmse=float(np.sqrt(np.mean(delta ** 2))), cosine=float(cosine), exact_fraction=float(np.mean(actual==ref))) + measured = results[label][stage] + gate = GATES[stage] + measured['gate'] = gate + measured['pass'] = (finite and measured['max_abs'] <= gate['max_abs'] and + measured['rmse'] <= gate['rmse'] and measured['cosine'] >= gate['cosine']) + passed = passed and measured['pass'] a.output.write_text(json.dumps(results, indent=2)+'\n') print(json.dumps(results, indent=2)) +raise SystemExit(0 if passed else 3) From e30c1b0c2c0739d0a7292edcc5c3f5212ee48256 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 18:58:31 -0400 Subject: [PATCH 022/123] test(ds4v): specify image routing and raw visibility policy --- .../src/deepseek4/deepseek4_image_policy.cpp | 11 ++++ server/src/deepseek4/deepseek4_image_policy.h | 31 ++++++++++ server/tools/ds4v_image_policy/CMakeLists.txt | 11 ++++ server/tools/ds4v_image_policy/test.cpp | 60 +++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 server/src/deepseek4/deepseek4_image_policy.cpp create mode 100644 server/src/deepseek4/deepseek4_image_policy.h create mode 100644 server/tools/ds4v_image_policy/CMakeLists.txt create mode 100644 server/tools/ds4v_image_policy/test.cpp diff --git a/server/src/deepseek4/deepseek4_image_policy.cpp b/server/src/deepseek4/deepseek4_image_policy.cpp new file mode 100644 index 000000000..0b0c5a5a9 --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_policy.cpp @@ -0,0 +1,11 @@ +#include "deepseek4_image_policy.h" + +namespace dflash::vision { +bool select_image_experts(const float *, const float *, size_t, size_t, + ImageExpertSelection & output, std::string & error, float) { + output = {}; error = "not implemented"; return false; +} +bool raw_key_visible(int64_t, int64_t, int64_t, int64_t, int64_t, bool & visible) { + visible = false; return false; +} +} diff --git a/server/src/deepseek4/deepseek4_image_policy.h b/server/src/deepseek4/deepseek4_image_policy.h new file mode 100644 index 000000000..2bb45f3c5 --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_policy.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include +#include + +namespace dflash::vision { + +constexpr size_t MAX_IMAGE_EXPERTS = 256; +struct ImageExpertSelection { + size_t count = 0; + std::array indices{}; + std::array weights{}; +}; + +// Scores are unbiased sqrt(softplus(logits)); bias affects selection only. +// Equal corrected scores select lower expert indices first. Torch topk does +// not specify equal-score ordering, so tie parity is not part of this contract. +bool select_image_experts(const float * scores, const float * bias, size_t experts, + size_t topk, ImageExpertSelection & output, std::string & error, + float route_scale = 1.5f); + +// Only raw keys are covered. Pass (-1,-1) for no image; otherwise the range is +// [IMAGE_START, IMAGE_END+1), excluding leading compression padding. A range +// widens visibility only when it contains query. False means invalid arguments; +// visible is cleared on failure. No request span or compressed-row state is owned. +bool raw_key_visible(int64_t query, int64_t key, int64_t window, + int64_t image_begin, int64_t image_end, bool & visible); + +} // namespace dflash::vision diff --git a/server/tools/ds4v_image_policy/CMakeLists.txt b/server/tools/ds4v_image_policy/CMakeLists.txt new file mode 100644 index 000000000..5974848c5 --- /dev/null +++ b/server/tools/ds4v_image_policy/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4v_image_policy LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +add_library(ds4v_image_policy STATIC ../../src/deepseek4/deepseek4_image_policy.cpp) +target_include_directories(ds4v_image_policy PUBLIC ../../src) +target_compile_options(ds4v_image_policy PRIVATE -Wall -Wextra -Werror) +add_executable(test_ds4v_image_policy test.cpp) +target_link_libraries(test_ds4v_image_policy PRIVATE ds4v_image_policy) +enable_testing() +add_test(NAME ds4v_image_policy COMMAND test_ds4v_image_policy) diff --git a/server/tools/ds4v_image_policy/test.cpp b/server/tools/ds4v_image_policy/test.cpp new file mode 100644 index 000000000..854ad7d42 --- /dev/null +++ b/server/tools/ds4v_image_policy/test.cpp @@ -0,0 +1,60 @@ +#include "deepseek4/deepseek4_image_policy.h" +#include +#include +#include +#include + +using namespace dflash::vision; +static void check(bool ok, const char * reason) { if (!ok) throw std::runtime_error(reason); } +static bool visible(int64_t q, int64_t k, int64_t w, int64_t begin=-1, int64_t end=-1) { + bool result = false; + check(raw_key_visible(q,k,w,begin,end,result), "valid visibility rejected"); + return result; +} +int main() { + try { + ImageExpertSelection result; + std::string error; + float scores[] = {1,2,3,4}, bias[] = {0,8,0,0}; + check(select_image_experts(scores,bias,4,2,result,error), "valid selection rejected"); + check(result.count==2 && result.indices[0]==1 && result.indices[1]==3, "bias selection"); + check(result.weights[0]==0.5f && result.weights[1]==1.0f, "unbiased weights"); + float ties[] = {3,2,1,0}; + check(select_image_experts(scores,ties,4,4,result,error), "ties rejected"); + for (int i=0;i<4;++i) check(result.indices[i]==i, "lower-index tie handling"); + auto reject = [&](const float * s,const float * b,size_t n,size_t k,float scale=1.5f) { + result.count=9; + check(!select_image_experts(s,b,n,k,result,error,scale), "invalid selection accepted"); + check(result.count==0 && !error.empty(), "failure result/error contract"); + }; + reject(nullptr,bias,4,2); reject(scores,nullptr,4,2); + reject(scores,bias,0,0); reject(scores,bias,257,2); + reject(scores,bias,4,0); reject(scores,bias,4,5); + reject(scores,bias,4,2,0); reject(scores,bias,4,2,-1); + reject(scores,bias,4,2,std::numeric_limits::infinity()); + float invalid[] = {-1,2,3,4}; reject(invalid,bias,4,2); + invalid[0]=std::numeric_limits::quiet_NaN(); reject(invalid,bias,4,2); + invalid[0]=std::numeric_limits::infinity(); reject(scores,invalid,4,2); + float huge[] = {std::numeric_limits::max(),1,1,1}; reject(huge,huge,4,2); + float zero[] = {0,0,0,0}; reject(zero,bias,4,2); + float sum_overflow[] = {std::numeric_limits::max(),std::numeric_limits::max(),0,0}; + reject(sum_overflow,zero,4,2); + check(visible(0,0,1) && !visible(0,1,1), "initial causal row"); + check(visible(10,7,4) && !visible(10,6,4) && !visible(10,11,4), "window boundaries"); + check(visible(10,20,4,8,21) && visible(10,7,4,8,21), "image union"); + check(visible(20,8,4,8,21) && !visible(20,7,4,8,21), "long image"); + check(!visible(7,8,4,8,21) && !visible(21,8,4,8,21), "outside image isolation"); + check(!visible(10,30,4,8,21), "other future image isolation"); + const int64_t max=std::numeric_limits::max(); + check(visible(max,max,1) && visible(max,max-2,3) && !visible(max,max-3,3), "near integer limit"); + check(visible(max-2,max-1,3,max-3,max), "near limit image"); + bool value=true; + for (auto args : {std::array{-1,0,1,-1,-1}, {0,-1,1,-1,-1}, + {0,0,0,-1,-1}, {0,0,1,-1,2}, {0,0,1,2,2}, {0,0,1,3,2}}) { + check(!raw_key_visible(args[0],args[1],args[2],args[3],args[4],value) && !value, + "invalid visibility accepted"); + } + std::cout << "PASS routing selection/weights/ties, invalid contracts, raw visibility boundaries and int64 limits\n"; + return 0; + } catch (const std::exception & e) { std::cerr << "FAIL: " << e.what() << '\n'; return 1; } +} From a238e8eb5b13b77433ca1d6aa2384d891678fb5f Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:00:48 -0400 Subject: [PATCH 023/123] feat(ds4v): add exact RGB vision preprocessing --- .../deepseek4/deepseek4_vision_preprocess.cpp | 774 ++++++++++++++++++ .../deepseek4/deepseek4_vision_preprocess.h | 134 +++ .../ds4v_preprocess_probe/CMakeLists.txt | 14 + server/tools/ds4v_preprocess_probe/README.md | 26 + .../ds4v_preprocess_probe.cpp | 324 ++++++++ .../generate_reference_fixtures.py | 184 +++++ 6 files changed, 1456 insertions(+) create mode 100644 server/src/deepseek4/deepseek4_vision_preprocess.cpp create mode 100644 server/src/deepseek4/deepseek4_vision_preprocess.h create mode 100644 server/tools/ds4v_preprocess_probe/CMakeLists.txt create mode 100644 server/tools/ds4v_preprocess_probe/README.md create mode 100644 server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp create mode 100644 server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py diff --git a/server/src/deepseek4/deepseek4_vision_preprocess.cpp b/server/src/deepseek4/deepseek4_vision_preprocess.cpp new file mode 100644 index 000000000..6cd09f1bc --- /dev/null +++ b/server/src/deepseek4/deepseek4_vision_preprocess.cpp @@ -0,0 +1,774 @@ +#include "deepseek4_vision_preprocess.h" + +#include +#include +#include +#include +#include +#include + +namespace dflash::vision { +namespace { + +constexpr int kPrecisionBits = 22; + +PreprocessStatus ok() { + return {}; +} + +PreprocessStatus fail(PreprocessError code, std::string message) { + return {code, std::move(message)}; +} + +bool checked_mul(std::uint64_t a, std::uint64_t b, std::uint64_t & out) { + if (a != 0 && b > std::numeric_limits::max() / a) { + return false; + } + out = a * b; + return true; +} + +bool checked_add(std::uint64_t a, std::uint64_t b, std::uint64_t & out) { + if (b > std::numeric_limits::max() - a) { + return false; + } + out = a + b; + return true; +} + +std::uint64_t round_ties_to_even(double value) { + const double base_double = std::floor(value); + const double fraction = value - base_double; + const auto base = static_cast(base_double); + if (fraction > 0.5 || (fraction == 0.5 && (base & 1U) != 0)) { + return base + 1; + } + return base; +} + +struct GridTokens { + std::uint32_t rows = 0; + std::uint32_t cols = 0; + std::uint64_t tokens = 0; +}; + +PreprocessStatus grid_tokens( + std::uint32_t height, + std::uint32_t width, + const PreprocessConfig & config, + GridTokens & result) { + if (height == 0 || width == 0 || height % config.patch_size != 0 || + width % config.patch_size != 0) { + return fail(PreprocessError::ResizePlanFailed, + "planned dimensions must be nonzero patch multiples"); + } + + const std::uint64_t vit_rows = height / config.patch_size; + const std::uint64_t vit_cols = width / config.patch_size; + const std::uint64_t rows = + (vit_rows + config.downsample_ratio - 1) / config.downsample_ratio; + const std::uint64_t cols = + (vit_cols + config.downsample_ratio - 1) / config.downsample_ratio; + if (rows == 0 || cols == 0 || rows > std::numeric_limits::max() || + cols > std::numeric_limits::max()) { + return fail(PreprocessError::ResizePlanFailed, "aligner grid is out of range"); + } + + std::uint64_t row_len = 0; + std::uint64_t tokens = 0; + if (!checked_add(cols, 1, row_len) || !checked_mul(rows, row_len, tokens) || + !checked_add(tokens, 2, tokens)) { + return fail(PreprocessError::ResizePlanFailed, "layout token count overflow"); + } + if ((rows & 1U) != 0 && !checked_add(tokens, row_len, tokens)) { + return fail(PreprocessError::ResizePlanFailed, "layout row padding overflow"); + } + const std::uint64_t trailing = (((rows + 1) / 2) * row_len % 2) * 2; + if (!checked_add(tokens, trailing, tokens)) { + return fail(PreprocessError::ResizePlanFailed, "layout tail padding overflow"); + } + + result.rows = static_cast(rows); + result.cols = static_cast(cols); + result.tokens = tokens; + return ok(); +} + +PreprocessStatus solve_resize_ratio( + std::uint32_t height, + std::uint32_t width, + std::uint32_t max_tokens, + const PreprocessConfig & config, + std::uint32_t & best_height, + std::uint32_t & best_width, + GridTokens & grid) { + if (max_tokens <= 2 || height == 0 || width == 0) { + return fail(PreprocessError::ResizePlanFailed, "resize budget is too small"); + } + + const double ratio = static_cast(height) / static_cast(width); + const double max_width_float = + std::sqrt((static_cast(max_tokens) - 2.0) / ratio + 0.25) - 0.5; + const double max_height_float = max_width_float * ratio; + std::uint64_t solved_width = 0; + std::uint64_t solved_height = 0; + const std::uint64_t stride = + static_cast(config.patch_size) * config.downsample_ratio; + + if (max_width_float < 1.0) { + const std::uint64_t max_width = 1; + std::uint64_t max_height = (max_tokens - 2) / (max_width + 1); + if ((max_height & 1U) != 0) { + --max_height; + } + solved_width = max_width * stride; + solved_height = max_height * stride; + } else if (max_height_float < 2.0) { + const std::uint64_t max_height = 2; + const std::uint64_t max_width = (max_tokens - 2) / max_height - 1; + if (max_width <= 1) { + return fail(PreprocessError::ResizePlanFailed, + "resize budget cannot represent a wide image"); + } + solved_width = max_width * stride; + solved_height = max_height * stride; + } else { + const auto max_width = static_cast(std::floor(max_width_float)); + auto max_height = static_cast(std::floor(max_height_float)); + if ((max_height & 1U) != 0) { + --max_height; + } + if (max_width == 0 || max_height == 0) { + return fail(PreprocessError::ResizePlanFailed, "resize solver produced an empty grid"); + } + const double beta = std::min( + static_cast(max_width * stride) / static_cast(width), + static_cast(max_height * stride) / static_cast(height)); + solved_width = static_cast( + std::floor(static_cast(width) * beta / config.patch_size)) * + config.patch_size; + solved_height = static_cast( + std::floor(static_cast(height) * beta / config.patch_size)) * + config.patch_size; + } + + if (solved_width == 0 || solved_height == 0 || + solved_width > std::numeric_limits::max() || + solved_height > std::numeric_limits::max()) { + return fail(PreprocessError::ResizePlanFailed, + "resize solver produced out-of-range dimensions"); + } + best_width = static_cast(solved_width); + best_height = static_cast(solved_height); + return grid_tokens(best_height, best_width, config, grid); +} + +struct Coefficients { + int kernel_size = 0; + std::vector bounds; + std::vector weights; +}; + +// Pillow 12.3.0 Resample.c reference used for byte parity: +// https://github.com/python-pillow/Pillow/blob/12.3.0/src/libImaging/Resample.c +double bicubic(double x) { + constexpr double a = -0.5; + if (x < 0.0) { + x = -x; + } + if (x < 1.0) { + return ((a + 2.0) * x - (a + 3.0)) * x * x + 1.0; + } + if (x < 2.0) { + return (((x - 5.0) * x + 8.0) * x - 4.0) * a; + } + return 0.0; +} + +PreprocessStatus precompute_coefficients(int input_size, int output_size, Coefficients & out) { + if (input_size <= 0 || output_size <= 0) { + return fail(PreprocessError::ResizePlanFailed, "resample dimensions must be positive"); + } + const float input_begin = 0.0F; + const float input_end = static_cast(input_size); + double filter_scale = + (static_cast(input_end) - static_cast(input_begin)) / output_size; + const double scale = filter_scale; + if (filter_scale < 1.0) { + filter_scale = 1.0; + } + const double support = 2.0 * filter_scale; + const int kernel_size = static_cast(std::ceil(support)) * 2 + 1; + std::uint64_t coefficient_count = 0; + if (!checked_mul(static_cast(output_size), + static_cast(kernel_size), coefficient_count) || + coefficient_count > std::numeric_limits::max()) { + return fail(PreprocessError::OutputTooLarge, "resample coefficient count overflow"); + } + + std::vector floating(static_cast(coefficient_count), 0.0); + out.bounds.resize(static_cast(output_size) * 2); + out.weights.resize(static_cast(coefficient_count)); + const double inverse_filter_scale = 1.0 / filter_scale; + for (int output = 0; output < output_size; ++output) { + const double center = input_begin + (output + 0.5) * scale; + int first = static_cast(center - support + 0.5); + if (first < 0) { + first = 0; + } + int count = static_cast(center + support + 0.5); + if (count > input_size) { + count = input_size; + } + count -= first; + double sum = 0.0; + const std::size_t base = static_cast(output) * kernel_size; + for (int index = 0; index < count; ++index) { + const double weight = bicubic( + (index + first - center + 0.5) * inverse_filter_scale); + floating[base + index] = weight; + sum += weight; + } + if (sum != 0.0) { + for (int index = 0; index < count; ++index) { + floating[base + index] /= sum; + } + } + out.bounds[static_cast(output) * 2] = first; + out.bounds[static_cast(output) * 2 + 1] = count; + } + + constexpr double scale_to_fixed = static_cast(1U << kPrecisionBits); + for (std::size_t index = 0; index < floating.size(); ++index) { + const double value = floating[index] * scale_to_fixed; + out.weights[index] = static_cast( + value < 0.0 ? value - 0.5 : value + 0.5); + } + out.kernel_size = kernel_size; + return ok(); +} + +std::uint8_t clip_fixed(std::int32_t value) { + const std::int32_t rounded = value >> kPrecisionBits; + return static_cast(std::clamp(rounded, 0, 255)); +} + +PreprocessStatus resize_horizontal( + const std::vector & input, + int input_width, + int input_height, + int output_width, + std::vector & output) { + Coefficients coeffs; + if (const auto status = precompute_coefficients(input_width, output_width, coeffs); !status) { + return status; + } + output.resize(static_cast(output_width) * input_height * 3); + for (int y = 0; y < input_height; ++y) { + for (int x = 0; x < output_width; ++x) { + const int first = coeffs.bounds[static_cast(x) * 2]; + const int count = coeffs.bounds[static_cast(x) * 2 + 1]; + const std::int32_t * weights = + coeffs.weights.data() + static_cast(x) * coeffs.kernel_size; + for (int channel = 0; channel < 3; ++channel) { + std::int32_t sum = 1 << (kPrecisionBits - 1); + for (int index = 0; index < count; ++index) { + const std::size_t source = + (static_cast(y) * input_width + first + index) * 3 + channel; + sum += static_cast(input[source]) * weights[index]; + } + const std::size_t destination = + (static_cast(y) * output_width + x) * 3 + channel; + output[destination] = clip_fixed(sum); + } + } + } + return ok(); +} + +PreprocessStatus resize_vertical( + const std::vector & input, + int input_width, + int input_height, + int output_height, + std::vector & output) { + Coefficients coeffs; + if (const auto status = precompute_coefficients(input_height, output_height, coeffs); !status) { + return status; + } + output.resize(static_cast(input_width) * output_height * 3); + for (int y = 0; y < output_height; ++y) { + const int first = coeffs.bounds[static_cast(y) * 2]; + const int count = coeffs.bounds[static_cast(y) * 2 + 1]; + const std::int32_t * weights = + coeffs.weights.data() + static_cast(y) * coeffs.kernel_size; + for (int x = 0; x < input_width; ++x) { + for (int channel = 0; channel < 3; ++channel) { + std::int32_t sum = 1 << (kPrecisionBits - 1); + for (int index = 0; index < count; ++index) { + const std::size_t source = + (static_cast(first + index) * input_width + x) * 3 + channel; + sum += static_cast(input[source]) * weights[index]; + } + const std::size_t destination = + (static_cast(y) * input_width + x) * 3 + channel; + output[destination] = clip_fixed(sum); + } + } + } + return ok(); +} + +PreprocessStatus pillow_resize( + const std::vector & input, + int input_width, + int input_height, + int output_width, + int output_height, + std::vector & output) { + if (input_width == output_width && input_height == output_height) { + output = input; + return ok(); + } + + std::vector intermediate; + const bool vertical_first = + static_cast(input_height) > + static_cast(input_width) * 100 && + output_height < input_height; + if (vertical_first) { + if (const auto status = resize_vertical( + input, input_width, input_height, output_height, intermediate); + !status) { + return status; + } + if (output_width == input_width) { + output = std::move(intermediate); + return ok(); + } + return resize_horizontal( + intermediate, input_width, output_height, output_width, output); + } + + const std::vector * vertical_input = &input; + int vertical_width = input_width; + if (output_width != input_width) { + if (const auto status = resize_horizontal( + input, input_width, input_height, output_width, intermediate); + !status) { + return status; + } + vertical_input = &intermediate; + vertical_width = output_width; + } + if (output_height != input_height) { + return resize_vertical( + *vertical_input, vertical_width, input_height, output_height, output); + } + output = *vertical_input; + return ok(); +} + +PreprocessStatus resize_and_pad( + const std::vector & input, + int input_width, + int input_height, + int output_width, + int output_height, + bool direct_resize, + std::vector & output) { + if (direct_resize) { + return pillow_resize( + input, input_width, input_height, output_width, output_height, output); + } + + int contained_width = output_width; + int contained_height = output_height; + const double input_ratio = static_cast(input_width) / input_height; + const double destination_ratio = static_cast(output_width) / output_height; + if (input_ratio != destination_ratio) { + if (input_ratio > destination_ratio) { + contained_height = static_cast(round_ties_to_even( + static_cast(input_height) / input_width * output_width)); + } else { + contained_width = static_cast(round_ties_to_even( + static_cast(input_width) / input_height * output_height)); + } + } + if (contained_width <= 0 || contained_height <= 0) { + return fail(PreprocessError::ResizePlanFailed, + "ImageOps.contain produced an empty dimension"); + } + + std::vector contained; + if (const auto status = pillow_resize( + input, input_width, input_height, contained_width, contained_height, contained); + !status) { + return status; + } + if (contained_width == output_width && contained_height == output_height) { + output = std::move(contained); + return ok(); + } + + output.assign(static_cast(output_width) * output_height * 3, 127); + const int offset_x = contained_width == output_width + ? 0 + : static_cast(round_ties_to_even((output_width - contained_width) * 0.5)); + const int offset_y = contained_width != output_width + ? 0 + : static_cast(round_ties_to_even((output_height - contained_height) * 0.5)); + for (int y = 0; y < contained_height; ++y) { + const auto * source = contained.data() + static_cast(y) * contained_width * 3; + auto * destination = output.data() + + (static_cast(y + offset_y) * output_width + offset_x) * 3; + std::memcpy(destination, source, static_cast(contained_width) * 3); + } + return ok(); +} + +std::uint16_t float_to_bf16(float value) { + std::uint32_t bits = 0; + static_assert(sizeof(bits) == sizeof(value)); + std::memcpy(&bits, &value, sizeof(bits)); + const std::uint32_t rounding_bias = 0x7FFFU + ((bits >> 16U) & 1U); + return static_cast((bits + rounding_bias) >> 16U); +} + +std::uint16_t normalize_pixel(std::uint8_t pixel) { + // Keep the source operation sequence and its F32 rounding points. + volatile float normalized = static_cast(pixel); + normalized = normalized / 255.0F; + normalized = normalized - 0.5F; + normalized = normalized / 0.5F; + return float_to_bf16(normalized); +} + +PreprocessStatus make_patches( + const std::vector & rgb, + const ResizePlan & plan, + const PreprocessConfig & config, + std::vector & patches) { + std::uint64_t pixel_count = 0; + std::uint64_t value_count = 0; + if (!checked_mul(plan.resized_width, plan.resized_height, pixel_count) || + !checked_mul(pixel_count, 3, value_count) || + value_count > std::numeric_limits::max()) { + return fail(PreprocessError::OutputTooLarge, "patch tensor size overflow"); + } + if (rgb.size() != value_count) { + return fail(PreprocessError::ResizePlanFailed, "resized RGB buffer has the wrong size"); + } + patches.resize(static_cast(value_count)); + + const std::uint32_t patch = config.patch_size; + std::size_t destination = 0; + for (std::uint32_t patch_y = 0; patch_y < plan.vit_rows; ++patch_y) { + for (std::uint32_t patch_x = 0; patch_x < plan.vit_cols; ++patch_x) { + for (std::uint32_t channel = 0; channel < 3; ++channel) { + for (std::uint32_t y = 0; y < patch; ++y) { + for (std::uint32_t x = 0; x < patch; ++x) { + const std::uint32_t source_y = patch_y * patch + y; + const std::uint32_t source_x = patch_x * patch + x; + const std::size_t source = + (static_cast(source_y) * plan.resized_width + source_x) * + 3 + + channel; + patches[destination++] = normalize_pixel(rgb[source]); + } + } + } + } + } + return ok(); +} + +} // namespace + +PreprocessStatus validate_config(const PreprocessConfig & config) { + const PreprocessConfig expected; + if (config.patch_size != expected.patch_size || + config.downsample_ratio != expected.downsample_ratio || + config.max_tokens != expected.max_tokens || + config.min_pixels != expected.min_pixels || + config.max_aspect_ratio != expected.max_aspect_ratio || + config.compress_pad_to != expected.compress_pad_to || + config.vocab_size != expected.vocab_size || + config.normalization_mean != expected.normalization_mean || + config.normalization_std != expected.normalization_std) { + return fail(PreprocessError::InvalidConfig, + "preprocessing config does not match the fixed DeepSeek-V4 vision recipe"); + } + return ok(); +} + +PreprocessStatus validate_decoded_dimensions( + std::uint32_t width, + std::uint32_t height, + const PreprocessLimits & limits) { + if (width == 0 || height == 0) { + return fail(PreprocessError::InvalidDimensions, "decoded RGB dimensions must be positive"); + } + if (limits.max_decoded_pixels == 0 || limits.max_dimension == 0 || + limits.max_output_pixels == 0) { + return fail(PreprocessError::InvalidConfig, "preprocessing limits must be positive"); + } + if (width > limits.max_dimension || height > limits.max_dimension) { + return fail(PreprocessError::InputTooLarge, "decoded RGB dimension exceeds the limit"); + } + std::uint64_t pixels = 0; + if (!checked_mul(width, height, pixels) || pixels > limits.max_decoded_pixels) { + return fail(PreprocessError::InputTooLarge, "decoded RGB pixel count exceeds the limit"); + } + return ok(); +} + +PreprocessStatus plan_image( + std::uint32_t width, + std::uint32_t height, + ResizePlan & plan, + const PreprocessConfig & config, + const PreprocessLimits & limits) { + if (const auto status = validate_config(config); !status) { + return status; + } + if (const auto status = validate_decoded_dimensions(width, height, limits); !status) { + return status; + } + + const bool direct_resize = + static_cast(width) >= + static_cast(height) * config.max_aspect_ratio; + std::uint64_t planned_width = width; + std::uint64_t planned_height = height; + const std::uint64_t max_width = + static_cast(height) * config.max_aspect_ratio; + if (planned_width > max_width) { + planned_width = max_width; + } + + std::uint64_t planned_pixels = 0; + if (!checked_mul(planned_width, planned_height, planned_pixels)) { + return fail(PreprocessError::ResizePlanFailed, "planned pixel count overflow"); + } + if (planned_pixels > 0 && planned_pixels < config.min_pixels) { + const double ratio = std::sqrt( + static_cast(config.min_pixels) / static_cast(planned_pixels)); + planned_width = static_cast(planned_width * ratio); + planned_height = static_cast(planned_height * ratio); + } + if (planned_width == 0 || planned_height == 0 || + planned_width > std::numeric_limits::max() || + planned_height > std::numeric_limits::max()) { + return fail(PreprocessError::ResizePlanFailed, + "minimum-pixel scaling produced invalid dimensions"); + } + + const std::uint64_t aligned_width = + ((planned_width + config.patch_size - 1) / config.patch_size) * config.patch_size; + const std::uint64_t aligned_height = + ((planned_height + config.patch_size - 1) / config.patch_size) * config.patch_size; + if (aligned_width > std::numeric_limits::max() || + aligned_height > std::numeric_limits::max()) { + return fail(PreprocessError::ResizePlanFailed, "aligned dimensions are out of range"); + } + std::uint32_t best_width = static_cast(aligned_width); + std::uint32_t best_height = static_cast(aligned_height); + GridTokens grid; + if (const auto status = grid_tokens(best_height, best_width, config, grid); !status) { + return status; + } + + const std::uint32_t reserved = config.compress_pad_to - 1; + if (config.max_tokens <= reserved + 2) { + return fail(PreprocessError::InvalidConfig, "token budget cannot hold image sentinels"); + } + const std::uint32_t image_budget = config.max_tokens - reserved; + std::uint32_t solver_budget = image_budget; + std::uint32_t attempts = 0; + while (grid.tokens > image_budget) { + if (solver_budget <= 2 || ++attempts > config.max_tokens) { + return fail(PreprocessError::ResizePlanFailed, + "could not solve image dimensions within the token budget"); + } + if (const auto status = solve_resize_ratio( + static_cast(planned_height), + static_cast(planned_width), + solver_budget, + config, + best_height, + best_width, + grid); + !status) { + return status; + } + --solver_budget; + } + + std::uint64_t output_pixels = 0; + if (!checked_mul(best_width, best_height, output_pixels) || + output_pixels > limits.max_output_pixels) { + return fail(PreprocessError::OutputTooLarge, + "planned resized image exceeds the output pixel limit"); + } + plan.resized_width = best_width; + plan.resized_height = best_height; + plan.vit_rows = best_height / config.patch_size; + plan.vit_cols = best_width / config.patch_size; + plan.aligner_rows = grid.rows; + plan.aligner_cols = grid.cols; + plan.direct_resize = direct_resize; + return ok(); +} + +PreprocessStatus build_image_layout( + std::uint32_t aligner_rows, + std::uint32_t aligner_cols, + std::uint64_t start_position, + ImageLayout & layout, + const PreprocessConfig & config) { + if (const auto status = validate_config(config); !status) { + return status; + } + if (aligner_rows == 0 || aligner_cols == 0) { + return fail(PreprocessError::InvalidDimensions, "aligner grid dimensions must be positive"); + } + + const std::uint64_t leading_pad = + config.compress_pad_to - 1 - start_position % config.compress_pad_to; + const std::uint64_t padded_rows = + static_cast(aligner_rows) + (aligner_rows % 2); + const std::uint64_t row_length = static_cast(aligner_cols) + 1; + std::uint64_t body_size = 0; + if (!checked_mul(padded_rows, row_length, body_size)) { + return fail(PreprocessError::TokenBudgetExceeded, "layout body size overflow"); + } + const std::uint64_t trailing_pad = (padded_rows / 2 * row_length % 2) * 2; + std::uint64_t total = 0; + if (!checked_add(leading_pad, 1, total) || !checked_add(total, body_size, total) || + !checked_add(total, trailing_pad, total) || !checked_add(total, 1, total)) { + return fail(PreprocessError::TokenBudgetExceeded, "layout token count overflow"); + } + if (total > config.max_tokens) { + std::ostringstream message; + message << "image layout needs " << total << " tokens, limit is " << config.max_tokens; + return fail(PreprocessError::TokenBudgetExceeded, message.str()); + } + std::uint64_t block_end = 0; + std::uint64_t visible_begin = 0; + if (!checked_add(start_position, total, block_end) || + !checked_add(start_position, leading_pad, visible_begin)) { + return fail(PreprocessError::PositionOverflow, "absolute image span overflow"); + } + + layout.types.clear(); + layout.permutation.clear(); + layout.types.reserve(static_cast(total)); + layout.permutation.reserve(static_cast(aligner_rows) * aligner_cols); + layout.types.insert(layout.types.end(), static_cast(leading_pad), + ImageTokenType::Pad); + layout.types.push_back(ImageTokenType::Start); + + for (std::uint64_t pair = 0; pair < padded_rows / 2; ++pair) { + for (std::uint64_t column = 0; column < row_length; ++column) { + for (std::uint64_t within_pair = 0; within_pair < 2; ++within_pair) { + const std::uint64_t row = pair * 2 + within_pair; + if (row < aligner_rows && column < aligner_cols) { + layout.types.push_back(ImageTokenType::Image); + layout.permutation.push_back(static_cast( + row * aligner_cols + column)); + } else if (row < aligner_rows && column == aligner_cols) { + layout.types.push_back(ImageTokenType::Newline); + } else { + layout.types.push_back(ImageTokenType::Pad); + } + } + } + } + layout.types.insert(layout.types.end(), static_cast(trailing_pad), + ImageTokenType::Pad); + layout.types.push_back(ImageTokenType::End); + layout.span = {start_position, visible_begin, block_end, block_end}; + return ok(); +} + +PreprocessResult preprocess_rgb( + const DecodedRgbView & input, + std::uint64_t start_position, + const PreprocessConfig & config, + const PreprocessLimits & limits) { + PreprocessResult result; + if (const auto status = validate_config(config); !status) { + result.status = status; + return result; + } + if (const auto status = validate_decoded_dimensions(input.width, input.height, limits); + !status) { + result.status = status; + return result; + } + std::uint64_t pixels = 0; + std::uint64_t expected_bytes = 0; + if (!checked_mul(input.width, input.height, pixels) || + !checked_mul(pixels, 3, expected_bytes) || input.data == nullptr || + expected_bytes != input.size) { + result.status = fail(PreprocessError::InputSizeMismatch, + "decoded RGB byte count must equal width * height * 3"); + return result; + } + if (const auto status = plan_image( + input.width, input.height, result.image.plan, config, limits); + !status) { + result.status = status; + return result; + } + if (const auto status = build_image_layout( + result.image.plan.aligner_rows, + result.image.plan.aligner_cols, + start_position, + result.image.layout, + config); + !status) { + result.status = status; + return result; + } + + std::vector owned_input(input.data, input.data + input.size); + if (const auto status = resize_and_pad( + owned_input, + static_cast(input.width), + static_cast(input.height), + static_cast(result.image.plan.resized_width), + static_cast(result.image.plan.resized_height), + result.image.plan.direct_resize, + result.image.resized_rgb); + !status) { + result.status = status; + return result; + } + if (const auto status = make_patches( + result.image.resized_rgb, result.image.plan, config, result.image.patches_bf16); + !status) { + result.status = status; + return result; + } + result.status = ok(); + return result; +} + +const char * preprocess_error_name(PreprocessError error) { + switch (error) { + case PreprocessError::None: return "none"; + case PreprocessError::InvalidConfig: return "invalid_config"; + case PreprocessError::InvalidDimensions: return "invalid_dimensions"; + case PreprocessError::InputSizeMismatch: return "input_size_mismatch"; + case PreprocessError::InputTooLarge: return "input_too_large"; + case PreprocessError::ResizePlanFailed: return "resize_plan_failed"; + case PreprocessError::OutputTooLarge: return "output_too_large"; + case PreprocessError::TokenBudgetExceeded: return "token_budget_exceeded"; + case PreprocessError::PositionOverflow: return "position_overflow"; + } + return "unknown"; +} + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_vision_preprocess.h b/server/src/deepseek4/deepseek4_vision_preprocess.h new file mode 100644 index 000000000..bb24c5cf2 --- /dev/null +++ b/server/src/deepseek4/deepseek4_vision_preprocess.h @@ -0,0 +1,134 @@ +#pragma once + +#include +#include +#include +#include + +namespace dflash::vision { + +struct PreprocessConfig { + std::uint32_t patch_size = 14; + std::uint32_t downsample_ratio = 3; + std::uint32_t max_tokens = 384; + std::uint64_t min_pixels = 147456; + std::uint32_t max_aspect_ratio = 8; + std::uint32_t compress_pad_to = 4; + std::uint32_t vocab_size = 129280; + float normalization_mean = 0.5F; + float normalization_std = 0.5F; +}; + +struct PreprocessLimits { + // The RGB entrypoint receives caller-owned memory, but validates these limits + // before allocating resized or patch buffers. A decoder can use the same + // dimension check before allocating its decoded image. + std::uint64_t max_decoded_pixels = 64ULL * 1024ULL * 1024ULL; + std::uint32_t max_dimension = 65'535; + std::uint64_t max_output_pixels = 16ULL * 1024ULL * 1024ULL; +}; + +struct DecodedRgbView { + std::uint32_t width = 0; + std::uint32_t height = 0; + const std::uint8_t * data = nullptr; + std::size_t size = 0; +}; + +enum class ImageTokenType : std::int64_t { + Start = 0, + Pad = 1, + Image = 2, + Newline = 3, + End = 4, +}; + +struct TokenSpan { + // All intervals are half-open absolute token positions. + std::uint64_t block_begin = 0; + std::uint64_t visible_begin = 0; + std::uint64_t visible_end = 0; + std::uint64_t block_end = 0; +}; + +struct ResizePlan { + std::uint32_t resized_width = 0; + std::uint32_t resized_height = 0; + std::uint32_t vit_rows = 0; + std::uint32_t vit_cols = 0; + std::uint32_t aligner_rows = 0; + std::uint32_t aligner_cols = 0; + bool direct_resize = false; +}; + +struct ImageLayout { + std::vector types; + std::vector permutation; + TokenSpan span; +}; + +struct PreparedImage { + ResizePlan plan; + std::vector resized_rgb; + // Raw IEEE bfloat16 words in native integer representation. The tensor + // shape is [vit_rows * vit_cols, 3, patch_size, patch_size]. + std::vector patches_bf16; + ImageLayout layout; +}; + +enum class PreprocessError { + None = 0, + InvalidConfig, + InvalidDimensions, + InputSizeMismatch, + InputTooLarge, + ResizePlanFailed, + OutputTooLarge, + TokenBudgetExceeded, + PositionOverflow, +}; + +struct PreprocessStatus { + PreprocessError code = PreprocessError::None; + std::string message; + + explicit operator bool() const { return code == PreprocessError::None; } +}; + +struct PreprocessResult { + PreprocessStatus status; + PreparedImage image; + + explicit operator bool() const { return static_cast(status); } +}; + +PreprocessStatus validate_config(const PreprocessConfig & config); + +PreprocessStatus validate_decoded_dimensions( + std::uint32_t width, + std::uint32_t height, + const PreprocessLimits & limits = {}); + +PreprocessStatus plan_image( + std::uint32_t width, + std::uint32_t height, + ResizePlan & plan, + const PreprocessConfig & config = {}, + const PreprocessLimits & limits = {}); + +PreprocessStatus build_image_layout( + std::uint32_t aligner_rows, + std::uint32_t aligner_cols, + std::uint64_t start_position, + ImageLayout & layout, + const PreprocessConfig & config = {}); + +PreprocessResult preprocess_rgb( + const DecodedRgbView & input, + std::uint64_t start_position, + const PreprocessConfig & config = {}, + const PreprocessLimits & limits = {}); + +const char * preprocess_error_name(PreprocessError error); + +} // namespace dflash::vision diff --git a/server/tools/ds4v_preprocess_probe/CMakeLists.txt b/server/tools/ds4v_preprocess_probe/CMakeLists.txt new file mode 100644 index 000000000..27e245176 --- /dev/null +++ b/server/tools/ds4v_preprocess_probe/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.20) +project(ds4v_preprocess_probe LANGUAGES CXX) + +add_executable(ds4v_preprocess_probe + ds4v_preprocess_probe.cpp + ../../src/deepseek4/deepseek4_vision_preprocess.cpp) +target_include_directories(ds4v_preprocess_probe PRIVATE ../../src/deepseek4) +target_compile_features(ds4v_preprocess_probe PRIVATE cxx_std_17) +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(ds4v_preprocess_probe PRIVATE -Wall -Wextra -Wpedantic -Werror) +endif() + +enable_testing() +add_test(NAME ds4v_preprocess_self_test COMMAND ds4v_preprocess_probe --self-test) diff --git a/server/tools/ds4v_preprocess_probe/README.md b/server/tools/ds4v_preprocess_probe/README.md new file mode 100644 index 000000000..20787da3a --- /dev/null +++ b/server/tools/ds4v_preprocess_probe/README.md @@ -0,0 +1,26 @@ +# DeepSeek-V4 vision preprocessing probe + +This standalone CPU target verifies the reusable decoded-RGB preprocessing unit without configuring the server or a GPU SDK. It implements the fixed source recipe only: patch size 14, downsample ratio 3, maximum 384 layout tokens, minimum 147456 planned pixels, maximum wide-image ratio 8, and mean/std 0.5 normalization. + +Generate reference fixtures on a machine containing the original parent source and its Python environment: + +```sh +python generate_reference_fixtures.py \ + --source /path/to/DeepSeek-V4-Flash-Vision-Uncensored \ + --output /tmp/ds4v-preprocess-fixtures +``` + +Build and run the probe: + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel 2 +ctest --test-dir build --output-on-failure +./build/ds4v_preprocess_probe --fixtures /tmp/ds4v-preprocess-fixtures +``` + +The fixture check compares resized RGB bytes, BF16 patch words, dimensions, all five grounded start-position layouts, permutations, spans, and a second deterministic run. The built-in self-test covers invalid fixed config, invalid and oversized decoded dimensions without allocating them, wrong RGB byte counts, output bounds, layout budget overflow, and absolute-position overflow. + +JPEG and PNG decoding are a separate gate. This target accepts already decoded interleaved RGB bytes and has no image-codec or Python runtime dependency. + +The resampler follows Pillow 12.3.0 `src/libImaging/Resample.c`, including signed 22-bit fixed-point bicubic coefficients and the tall-image vertical-first path. diff --git a/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp b/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp new file mode 100644 index 000000000..8153ffa8c --- /dev/null +++ b/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp @@ -0,0 +1,324 @@ +#include "deepseek4_vision_preprocess.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +using dflash::vision::DecodedRgbView; +using dflash::vision::ImageLayout; +using dflash::vision::ImageTokenType; +using dflash::vision::PreprocessConfig; +using dflash::vision::PreprocessError; +using dflash::vision::PreprocessLimits; +using dflash::vision::PreprocessResult; +using dflash::vision::ResizePlan; + +namespace { + +struct FixtureCase { + std::string label; + std::uint32_t input_width = 0; + std::uint32_t input_height = 0; + std::uint32_t resized_width = 0; + std::uint32_t resized_height = 0; + std::uint32_t vit_rows = 0; + std::uint32_t vit_cols = 0; + std::uint32_t aligner_rows = 0; + std::uint32_t aligner_cols = 0; +}; + +void require(bool condition, const std::string & message) { + if (!condition) { + throw std::runtime_error(message); + } +} + +std::vector split_tabs(const std::string & line) { + std::vector fields; + std::size_t begin = 0; + while (true) { + const std::size_t end = line.find('\t', begin); + fields.push_back(line.substr(begin, end == std::string::npos ? end : end - begin)); + if (end == std::string::npos) { + return fields; + } + begin = end + 1; + } +} + +std::uint32_t parse_u32(const std::string & value, const std::string & field) { + std::size_t used = 0; + const unsigned long parsed = std::stoul(value, &used); + if (used != value.size() || parsed > std::numeric_limits::max()) { + throw std::runtime_error("invalid " + field + ": " + value); + } + return static_cast(parsed); +} + +std::vector read_manifest(const fs::path & root) { + std::ifstream input(root / "manifest.tsv"); + require(input.good(), "cannot open fixture manifest: " + (root / "manifest.tsv").string()); + std::string line; + require(static_cast(std::getline(input, line)), "fixture manifest is empty"); + require(line == + "label\tinput_width\tinput_height\tresized_width\tresized_height\tvit_rows\tvit_cols\taligner_rows\taligner_cols", + "fixture manifest header mismatch"); + std::vector cases; + while (std::getline(input, line)) { + if (line.empty()) { + continue; + } + const auto fields = split_tabs(line); + require(fields.size() == 9, "fixture manifest row must have 9 fields"); + require(!fields[0].empty() && + std::all_of(fields[0].begin(), fields[0].end(), [](unsigned char value) { + return (value >= 'a' && value <= 'z') || value == '-'; + }), + "fixture label contains unsupported characters"); + FixtureCase item; + item.label = fields[0]; + item.input_width = parse_u32(fields[1], "input_width"); + item.input_height = parse_u32(fields[2], "input_height"); + item.resized_width = parse_u32(fields[3], "resized_width"); + item.resized_height = parse_u32(fields[4], "resized_height"); + item.vit_rows = parse_u32(fields[5], "vit_rows"); + item.vit_cols = parse_u32(fields[6], "vit_cols"); + item.aligner_rows = parse_u32(fields[7], "aligner_rows"); + item.aligner_cols = parse_u32(fields[8], "aligner_cols"); + cases.push_back(std::move(item)); + } + require(!cases.empty(), "fixture manifest has no cases"); + return cases; +} + +template +std::vector read_binary(const fs::path & path) { + static_assert(std::is_trivially_copyable_v); + std::ifstream input(path, std::ios::binary | std::ios::ate); + require(input.good(), "cannot open fixture: " + path.string()); + const std::streampos end = input.tellg(); + require(end >= 0, "cannot determine fixture size: " + path.string()); + const auto bytes = static_cast(end); + require(bytes % sizeof(T) == 0, "fixture byte size is invalid: " + path.string()); + std::vector result(static_cast(bytes / sizeof(T))); + input.seekg(0); + if (!result.empty()) { + input.read(reinterpret_cast(result.data()), static_cast(bytes)); + require(input.good(), "cannot read fixture: " + path.string()); + } + return result; +} + +template +void require_equal( + const std::vector & actual, + const std::vector & expected, + const std::string & label) { + if (actual.size() != expected.size()) { + std::ostringstream message; + message << label << " size mismatch: actual=" << actual.size() + << " expected=" << expected.size(); + throw std::runtime_error(message.str()); + } + const auto mismatch = std::mismatch(actual.begin(), actual.end(), expected.begin()); + if (mismatch.first != actual.end()) { + const std::size_t offset = static_cast(mismatch.first - actual.begin()); + std::ostringstream message; + message << label << " mismatch at element " << offset + << ": actual=" << static_cast(*mismatch.first) + << " expected=" << static_cast(*mismatch.second); + throw std::runtime_error(message.str()); + } +} + +std::vector layout_types(const ImageLayout & layout) { + std::vector result; + result.reserve(layout.types.size()); + for (const auto type : layout.types) { + result.push_back(static_cast(type)); + } + return result; +} + +void require_plan(const ResizePlan & actual, const FixtureCase & expected) { + std::ostringstream details; + details << "actual resized=" << actual.resized_width << 'x' << actual.resized_height + << " vit=" << actual.vit_rows << 'x' << actual.vit_cols + << " aligner=" << actual.aligner_rows << 'x' << actual.aligner_cols; + require(actual.resized_width == expected.resized_width && + actual.resized_height == expected.resized_height && + actual.vit_rows == expected.vit_rows && actual.vit_cols == expected.vit_cols && + actual.aligner_rows == expected.aligner_rows && + actual.aligner_cols == expected.aligner_cols, + expected.label + " plan mismatch: " + details.str()); +} + +void require_same_result(const PreprocessResult & first, const PreprocessResult & second, + const std::string & label) { + require(static_cast(first) && static_cast(second), + label + " deterministic run failed"); + require(first.image.plan.resized_width == second.image.plan.resized_width && + first.image.plan.resized_height == second.image.plan.resized_height && + first.image.plan.vit_rows == second.image.plan.vit_rows && + first.image.plan.vit_cols == second.image.plan.vit_cols && + first.image.plan.aligner_rows == second.image.plan.aligner_rows && + first.image.plan.aligner_cols == second.image.plan.aligner_cols && + first.image.plan.direct_resize == second.image.plan.direct_resize, + label + " plan is not deterministic"); + require_equal(first.image.resized_rgb, second.image.resized_rgb, + label + " deterministic resized RGB"); + require_equal(first.image.patches_bf16, second.image.patches_bf16, + label + " deterministic patches"); + require_equal(layout_types(first.image.layout), layout_types(second.image.layout), + label + " deterministic layout types"); + require_equal(first.image.layout.permutation, second.image.layout.permutation, + label + " deterministic permutation"); + require(first.image.layout.span.block_begin == second.image.layout.span.block_begin && + first.image.layout.span.visible_begin == second.image.layout.span.visible_begin && + first.image.layout.span.visible_end == second.image.layout.span.visible_end && + first.image.layout.span.block_end == second.image.layout.span.block_end, + label + " span is not deterministic"); +} + +void verify_fixture(const fs::path & root, const FixtureCase & item) { + const fs::path case_dir = root / item.label; + const auto input = read_binary(case_dir / "input.rgb"); + const DecodedRgbView view{item.input_width, item.input_height, input.data(), input.size()}; + const PreprocessResult result = dflash::vision::preprocess_rgb(view, 0); + require(static_cast(result), + item.label + " preprocess failed: " + + dflash::vision::preprocess_error_name(result.status.code) + ": " + + result.status.message); + require_plan(result.image.plan, item); + require_equal(result.image.resized_rgb, + read_binary(case_dir / "resized.rgb"), + item.label + " resized RGB"); + require_equal(result.image.patches_bf16, + read_binary(case_dir / "patches.bf16"), + item.label + " BF16 patches"); + + constexpr std::array starts = {0, 1, 2, 3, 127}; + for (const std::uint64_t start : starts) { + ImageLayout layout; + const auto status = dflash::vision::build_image_layout( + item.aligner_rows, item.aligner_cols, start, layout); + require(static_cast(status), + item.label + " layout failed at start " + std::to_string(start) + ": " + + status.message); + const std::string suffix = std::to_string(start) + ".i64"; + require_equal(layout_types(layout), + read_binary(case_dir / ("types-" + suffix)), + item.label + " types start=" + std::to_string(start)); + require_equal(layout.permutation, + read_binary(case_dir / ("permutation-" + suffix)), + item.label + " permutation start=" + std::to_string(start)); + const std::uint64_t leading = 3 - start % 4; + const std::uint64_t end = start + layout.types.size(); + require(layout.span.block_begin == start && layout.span.visible_begin == start + leading && + layout.span.visible_end == end && layout.span.block_end == end, + item.label + " span mismatch at start " + std::to_string(start)); + } + + const PreprocessResult repeated = dflash::vision::preprocess_rgb(view, 0); + require_same_result(result, repeated, item.label); + std::cout << item.label << " PASS resized=" << item.resized_width << 'x' + << item.resized_height << " vit=" << item.vit_rows << 'x' << item.vit_cols + << " aligner=" << item.aligner_rows << 'x' << item.aligner_cols + << " patch_words=" << result.image.patches_bf16.size() << '\n'; +} + +void expect_error(PreprocessError expected, PreprocessError actual, const std::string & label) { + require(actual == expected, + label + " returned " + dflash::vision::preprocess_error_name(actual) + + ", expected " + dflash::vision::preprocess_error_name(expected)); +} + +void self_test() { + PreprocessConfig bad_config; + bad_config.patch_size = 16; + expect_error(PreprocessError::InvalidConfig, + dflash::vision::validate_config(bad_config).code, + "changed fixed config"); + + expect_error(PreprocessError::InvalidDimensions, + dflash::vision::validate_decoded_dimensions(0, 1).code, + "zero width"); + expect_error(PreprocessError::InputTooLarge, + dflash::vision::validate_decoded_dimensions(65'536, 1).code, + "axis cap"); + expect_error(PreprocessError::InputTooLarge, + dflash::vision::validate_decoded_dimensions(8192, 8193).code, + "pixel cap"); + + const std::array pixel = {0, 127, 255}; + DecodedRgbView wrong_size{1, 1, pixel.data(), 2}; + expect_error(PreprocessError::InputSizeMismatch, + dflash::vision::preprocess_rgb(wrong_size, 0).status.code, + "wrong RGB byte count"); + + PreprocessLimits tiny_output_limit; + tiny_output_limit.max_output_pixels = 1; + ResizePlan plan; + expect_error(PreprocessError::OutputTooLarge, + dflash::vision::plan_image(1, 1, plan, {}, tiny_output_limit).code, + "output cap"); + + ImageLayout layout; + expect_error(PreprocessError::TokenBudgetExceeded, + dflash::vision::build_image_layout(100, 100, 0, layout).code, + "layout budget"); + expect_error(PreprocessError::PositionOverflow, + dflash::vision::build_image_layout( + 2, 3, std::numeric_limits::max() - 5, layout).code, + "absolute span overflow"); + + const auto layout_status = dflash::vision::build_image_layout(2, 3, 0, layout); + require(static_cast(layout_status), "known layout failed"); + const std::vector expected_types = { + 1, 1, 1, 0, 2, 2, 2, 2, 2, 2, 3, 3, 4, + }; + const std::vector expected_permutation = {0, 3, 1, 4, 2, 5}; + require_equal(layout_types(layout), expected_types, "known layout types"); + require_equal(layout.permutation, expected_permutation, "known layout permutation"); + require(layout.span.block_begin == 0 && layout.span.visible_begin == 3 && + layout.span.visible_end == 13 && layout.span.block_end == 13, + "known layout span mismatch"); + std::cout << "self-test PASS\n"; +} + +void usage(const char * program) { + std::cerr << "Usage: " << program << " --self-test | --fixtures DIRECTORY\n"; +} + +} // namespace + +int main(int argc, char ** argv) { + try { + if (argc == 2 && std::string(argv[1]) == "--self-test") { + self_test(); + return 0; + } + if (argc == 3 && std::string(argv[1]) == "--fixtures") { + const fs::path root = argv[2]; + for (const auto & item : read_manifest(root)) { + verify_fixture(root, item); + } + return 0; + } + usage(argv[0]); + return 2; + } catch (const std::exception & error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return 1; + } +} diff --git a/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py b/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py new file mode 100644 index 000000000..f4ecd0a56 --- /dev/null +++ b/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python3 +"""Generate decoded-RGB preprocessing fixtures with the original Python source.""" + +import argparse +import hashlib +import io +import json +import math +from pathlib import Path +import shutil +import sys +from types import SimpleNamespace + +import numpy as np +import torch +from PIL import Image, ImageOps + + +START_POSITIONS = (0, 1, 2, 3, 127) + + +def pattern(width: int, height: int, seed: int) -> bytes: + y, x = np.indices((height, width), dtype=np.uint32) + channels = [] + for channel in range(3): + values = (x * 17 + y * 31 + (x * y) % 251 + channel * 73 + seed) % 256 + channels.append(values.astype(np.uint8)) + return np.stack(channels, axis=2).tobytes() + + +def source_resize(image, model_args, safe_resize): + patch = model_args.vision_patch_size + width, height = image.size + if model_args.vision_max_wh_ratio is not None and width > height * model_args.vision_max_wh_ratio: + width = height * model_args.vision_max_wh_ratio + if 0 < width * height < model_args.vision_min_pixels: + ratio = (model_args.vision_min_pixels / (width * height)) ** 0.5 + width = int(width * ratio) + height = int(height * ratio) + best_width = math.ceil(width / patch) * patch + best_height = math.ceil(height / patch) * patch + llm_h, llm_w, best_height, best_width = safe_resize( + height, + width, + best_height, + best_width, + patch, + model_args.vision_downsample_ratio, + model_args.vision_max_n_token, + ) + if image.width >= model_args.vision_max_wh_ratio * image.height: + resized = image.resize((best_width, best_height)) + else: + resized = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127)) + return resized, best_height // patch, best_width // patch, llm_h, llm_w + + +def save_bytes(path: Path, data: bytes, digests: dict[str, str], root: Path): + path.write_bytes(data) + digests[str(path.relative_to(root))] = hashlib.sha256(data).hexdigest() + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--source", required=True, type=Path, help="parent model directory") + parser.add_argument("--output", required=True, type=Path) + args = parser.parse_args() + + sys.path.insert(0, str(args.source / "inference")) + from image_processor import build_image_block, load_image, safe_resize + + config = json.loads((args.source / "config.json").read_text()) + config["dim"] = config["hidden_size"] + model_args = SimpleNamespace(**config) + torch.set_num_threads(2) + + if args.output.exists(): + shutil.rmtree(args.output) + args.output.mkdir(parents=True) + + cases: list[tuple[str, Image.Image, bytes]] = [] + synthetic = ( + ("tiny", 3, 5), + ("odd-padding", 37, 23), + ("portrait", 41, 113), + ("wide-direct", 257, 31), + ("very-tall", 10, 20000), + ("max-budget", 2048, 354), + ) + for seed, (label, width, height) in enumerate(synthetic, start=1): + raw = pattern(width, height, seed * 19) + image = Image.frombytes("RGB", (width, height), raw) + encoded = io.BytesIO() + image.save(encoded, format="PNG") + cases.append((label, image, encoded.getvalue())) + + for label in ("carrots", "corn"): + encoded = (args.source / "inference/examples/images" / f"{label}.jpeg").read_bytes() + with Image.open(io.BytesIO(encoded)) as opened: + image = opened.convert("RGB") + cases.append((label, image, encoded)) + + manifest_lines = [ + "label\tinput_width\tinput_height\tresized_width\tresized_height\tvit_rows\tvit_cols\taligner_rows\taligner_cols" + ] + digests: dict[str, str] = {} + for label, image, encoded in cases: + case_dir = args.output / label + case_dir.mkdir() + resized, vit_rows, vit_cols, aligner_rows, aligner_cols = source_resize( + image, model_args, safe_resize + ) + patches, source_vit_rows, source_vit_cols, source_aligner_rows, source_aligner_cols = load_image( + {"data": encoded}, model_args + ) + assert (vit_rows, vit_cols, aligner_rows, aligner_cols) == ( + source_vit_rows, + source_vit_cols, + source_aligner_rows, + source_aligner_cols, + ) + + # This checks that the separately materialized resized RGB image produces + # the same BF16 tensor as the original load_image implementation. + rebuilt = torch.from_numpy(np.asarray(resized, dtype=np.float32)).permute(2, 0, 1) / 255 + rebuilt = ((rebuilt - 0.5) / 0.5).to(torch.bfloat16) + rebuilt = ( + rebuilt.reshape(3, vit_rows, model_args.vision_patch_size, vit_cols, model_args.vision_patch_size) + .permute(1, 3, 0, 2, 4) + .reshape(vit_rows * vit_cols, 3, model_args.vision_patch_size, model_args.vision_patch_size) + ) + assert torch.equal(rebuilt, patches) + + save_bytes(case_dir / "input.rgb", image.tobytes(), digests, args.output) + save_bytes(case_dir / "resized.rgb", resized.tobytes(), digests, args.output) + save_bytes( + case_dir / "patches.bf16", + patches.contiguous().view(torch.uint16).cpu().numpy().tobytes(), + digests, + args.output, + ) + for start in START_POSITIONS: + types, permutation = build_image_block(aligner_rows, aligner_cols, start) + save_bytes( + case_dir / f"types-{start}.i64", + types.contiguous().cpu().numpy().tobytes(), + digests, + args.output, + ) + save_bytes( + case_dir / f"permutation-{start}.i64", + permutation.contiguous().cpu().numpy().tobytes(), + digests, + args.output, + ) + manifest_lines.append( + "\t".join( + str(value) + for value in ( + label, + image.width, + image.height, + resized.width, + resized.height, + vit_rows, + vit_cols, + aligner_rows, + aligner_cols, + ) + ) + ) + print( + f"{label}: decoded={image.width}x{image.height} resized={resized.width}x{resized.height} " + f"vit={vit_rows}x{vit_cols} aligner={aligner_rows}x{aligner_cols}", + flush=True, + ) + + (args.output / "manifest.tsv").write_text("\n".join(manifest_lines) + "\n") + (args.output / "sha256.json").write_text(json.dumps(digests, indent=2, sort_keys=True) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 60ce979c05affe474d685ad34bc4143c0e24c631 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:01:11 -0400 Subject: [PATCH 024/123] feat(ds4v): add bounded image routing and raw visibility helpers --- .../src/deepseek4/deepseek4_image_policy.cpp | 50 ++++++- server/tools/ds4v_image_policy/CMakeLists.txt | 2 + server/tools/ds4v_image_policy/probe.cpp | 51 ++++++++ .../ds4v_image_policy/verify_fixtures.py | 123 ++++++++++++++++++ 4 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 server/tools/ds4v_image_policy/probe.cpp create mode 100644 server/tools/ds4v_image_policy/verify_fixtures.py diff --git a/server/src/deepseek4/deepseek4_image_policy.cpp b/server/src/deepseek4/deepseek4_image_policy.cpp index 0b0c5a5a9..eedd57d17 100644 --- a/server/src/deepseek4/deepseek4_image_policy.cpp +++ b/server/src/deepseek4/deepseek4_image_policy.cpp @@ -1,11 +1,51 @@ #include "deepseek4_image_policy.h" +#include +#include namespace dflash::vision { -bool select_image_experts(const float *, const float *, size_t, size_t, - ImageExpertSelection & output, std::string & error, float) { - output = {}; error = "not implemented"; return false; +bool select_image_experts(const float * scores, const float * bias, size_t experts, + size_t topk, ImageExpertSelection & output, std::string & error, + float route_scale) { + output = {}; error.clear(); + if (!scores || !bias || experts==0 || experts>MAX_IMAGE_EXPERTS || topk==0 || topk>experts) { + error="invalid expert arrays/count/topk"; return false; + } + if (!std::isfinite(route_scale) || route_scale<=0) { + error="route scale must be finite and positive"; return false; + } + std::array corrected{}; + std::array order{}; + for (size_t i=0;i(i); + } + std::sort(order.begin(),order.begin()+experts,[&](int32_t a,int32_t b) { + return corrected[a]>corrected[b] || (corrected[a]==corrected[b] && a=image_begin && query=image_begin && key +#include +#include +#include + +using namespace dflash::vision; +template static std::vector read(const char * path,size_t count) { + std::ifstream file(path,std::ios::binary|std::ios::ate); + if (!file || file.tellg()!=std::streamoff(count*sizeof(T))) throw std::runtime_error("input byte size mismatch"); + std::vector data(count); + file.seekg(0); file.read(reinterpret_cast(data.data()),count*sizeof(T)); + if (!file) throw std::runtime_error("input read failed"); + return data; +} +template static void save(const std::string & path,const std::vector & values) { + std::ofstream file(path,std::ios::binary); + file.write(reinterpret_cast(values.data()),values.size()*sizeof(T)); + if (!file) throw std::runtime_error("output write failed"); +} +int main(int argc,char ** argv) { + try { + if (argc==8 && std::string(argv[1])=="route") { + size_t rows=std::stoul(argv[5]),experts=std::stoul(argv[6]),topk=std::stoul(argv[7]); + if (rows>1024 || experts>256 || topk>experts) throw std::runtime_error("probe dimensions out of range"); + auto scores=read(argv[2],rows*experts),bias=read(argv[3],experts); + std::vector indices; + std::vector weights; + for (size_t row=0;row4096) throw std::runtime_error("probe mask too large"); + auto ranges=read(argv[2],count*2); + std::vector mask(count*count); + for (size_t q=0;q= config['vocab_size']) + assert np.array_equal(ids[image_rows], np.arange(config['vocab_size'], config['vocab_size'] + 5)) + expected_ids, expected_weights = read(entry['indices']), read(entry['weights']) + assert np.array_equal(actual_ids[image_rows], expected_ids[image_rows]), ('indices', layer) + difference = float(np.max(np.abs(actual_weights[image_rows] - expected_weights[image_rows]))) + assert np.isfinite(actual_weights).all() and difference <= 1e-6, ('weights', layer, difference) + assert np.max(np.abs(actual_weights[image_rows].sum(1) - 1.5)) <= 1e-6 + assert np.array_equal(actual_ids[image_rows], np.repeat(actual_ids[image_rows[:1]], 5, axis=0)) + hash_difference = None + if 'hash_rows' in entry: + hash_rows = read(entry['hash_rows']) + assert np.array_equal(expected_ids[:3], hash_rows), ('original text hash fixture', layer) + hash_difference = bool(np.all(np.any(hash_rows != actual_ids[image_rows[0]], axis=1))) + assert hash_difference, ('fixture must distinguish learned image selection from text hash', layer) + report['routing'][layer] = {'all_five_image_kinds_exact_indices': True, + 'max_weight_absolute_error': difference, 'image_selection_differs_from_text_hash_rows': hash_difference, + 'scores_sha256': hashlib.sha256(scores_path.read_bytes()).hexdigest()} + print('routing', layer, 'PASS', difference, flush=True) + +for name, entry in manifest['masks'].items(): + ids = read(entry['ids']).reshape(-1) + count = ids.size + ranges = np.full((count, 2), -1, np.int64) + starts = np.flatnonzero(ids == config['vocab_size']) + ends = np.flatnonzero(ids == config['vocab_size'] + 4) + assert len(starts) == len(ends) + for begin, end in zip(starts, ends): + assert begin <= end and (ranges[begin:end+1] == -1).all() + ranges[begin:end+1] = [begin, end+1] + ranges_path = a.output / f'{name}-ranges.i64' + ranges.tofile(ranges_path) + output = a.output / f'{name}-mask.u8' + subprocess.run([str(a.probe), 'mask', str(ranges_path), str(count), str(config['window_size']), str(output)], check=True) + actual = np.fromfile(output, np.uint8).reshape(count, count) + expected = np.zeros((count, count), np.uint8) + raw = read(entry['raw_indices']).reshape(count, -1) + for query, keys in enumerate(raw): + valid = keys[keys >= 0] + assert (valid < count).all() + expected[query, valid] = 1 + assert np.array_equal(actual, expected), ('raw visibility mismatch', name, np.count_nonzero(actual != expected)) + report['masks'][name] = {'all_key_query_pairs_exact': True, 'comparisons': count*count, + 'longer_than_window': bool(np.any(ends-starts+1 > config['window_size'])), + 'native_mask_sha256': hashlib.sha256(output.read_bytes()).hexdigest()} + print('mask', name, 'PASS', count*count, flush=True) + +report['verdict'] = 'PASS' +(a.output / 'verdict.json').write_text(json.dumps(report, indent=2) + '\n') From 0306a3fa806a237a63888e00a48e0b22ef7b13ff Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:01:19 -0400 Subject: [PATCH 025/123] test(ds4v): specify bounded image data URL transport --- server/src/server/image_input.cpp | 19 +++++ server/src/server/image_input.h | 34 ++++++++ server/tools/ds4v_image_input/CMakeLists.txt | 18 ++++ server/tools/ds4v_image_input/test.cpp | 87 ++++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 server/src/server/image_input.cpp create mode 100644 server/src/server/image_input.h create mode 100644 server/tools/ds4v_image_input/CMakeLists.txt create mode 100644 server/tools/ds4v_image_input/test.cpp diff --git a/server/src/server/image_input.cpp b/server/src/server/image_input.cpp new file mode 100644 index 000000000..70c1a2ff3 --- /dev/null +++ b/server/src/server/image_input.cpp @@ -0,0 +1,19 @@ +#include "image_input.h" + +namespace dflash::common { +bool parse_image_data_url(std::string_view, EncodedImage & image, + std::string & error, size_t) { + image = {}; + error = "image data URLs are not implemented"; + return false; +} +bool extract_chat_images(const nlohmann::json &, nlohmann::json & normalized, + std::vector & images, + std::string & error, const ImageInputLimits &) { + normalized = nullptr; + images.clear(); + error = "chat image transport is not implemented"; + return false; +} +void redact_image_urls(nlohmann::json &) {} +} // namespace dflash::common diff --git a/server/src/server/image_input.h b/server/src/server/image_input.h new file mode 100644 index 000000000..18840f2d5 --- /dev/null +++ b/server/src/server/image_input.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace dflash::common { + +inline constexpr char DS4_IMAGE_PLACEHOLDER[] = "<|deepseek_image|>"; + +struct EncodedImage { + std::string mime_type; + std::vector bytes; +}; + +struct ImageInputLimits { + size_t image_bytes = 16 * 1024 * 1024; + size_t request_bytes = 32 * 1024 * 1024; + size_t image_count = 4; +}; + +bool parse_image_data_url(std::string_view url, EncodedImage & image, + std::string & error, size_t max_bytes = 16 * 1024 * 1024); +bool extract_chat_images(const nlohmann::json & messages, + nlohmann::json & normalized, + std::vector & images, + std::string & error, + const ImageInputLimits & limits = {}); +void redact_image_urls(nlohmann::json & value); + +} // namespace dflash::common diff --git a/server/tools/ds4v_image_input/CMakeLists.txt b/server/tools/ds4v_image_input/CMakeLists.txt new file mode 100644 index 000000000..9d2be91f7 --- /dev/null +++ b/server/tools/ds4v_image_input/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4v_image_input LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +find_package(nlohmann_json CONFIG QUIET) +if(NOT nlohmann_json_FOUND) + include(FetchContent) + FetchContent_Declare(nlohmann_json + URL https://codeload.github.com/nlohmann/json/tar.gz/9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03) + FetchContent_MakeAvailable(nlohmann_json) +endif() +add_library(ds4v_image_input STATIC ../../src/server/image_input.cpp) +target_include_directories(ds4v_image_input PUBLIC ../../src) +target_link_libraries(ds4v_image_input PUBLIC nlohmann_json::nlohmann_json) +add_executable(ds4v_image_input_test test.cpp) +target_link_libraries(ds4v_image_input_test PRIVATE ds4v_image_input) +enable_testing() +add_test(NAME ds4v_image_input COMMAND ds4v_image_input_test) diff --git a/server/tools/ds4v_image_input/test.cpp b/server/tools/ds4v_image_input/test.cpp new file mode 100644 index 000000000..1c50fbd8e --- /dev/null +++ b/server/tools/ds4v_image_input/test.cpp @@ -0,0 +1,87 @@ +#include "server/image_input.h" + +#include +#include + +using namespace dflash::common; +using json = nlohmann::json; + +static void check(bool condition, const char * message) { + if (!condition) throw std::runtime_error(message); +} +static json image_part(const std::string & url) { + return {{"type", "image_url"}, {"image_url", {{"url", url}}}}; +} +static json text_part(const std::string & text) { + return {{"type", "text"}, {"text", text}}; +} + +int main() { + try { + const std::string jpeg = "data:image/jpeg;base64,/9j/"; + const std::string png = "data:image/png;base64,iVBORw0KGgo="; + EncodedImage image; + std::string error; + check(parse_image_data_url(jpeg, image, error), "valid JPEG transport rejected"); + check(image.mime_type == "image/jpeg" && image.bytes == std::vector({255,216,255}), "JPEG bytes differ"); + check(parse_image_data_url(png, image, error), "valid PNG transport rejected"); + check(image.mime_type == "image/png" && image.bytes == std::vector({137,80,78,71,13,10,26,10}), "PNG bytes differ"); + for (const auto & bad : std::vector{ + "https://example.com/a.png", "file:///etc/passwd", "data:image/gif;base64,R0lG", + "data:image/png,iVBORw0KGgo=", "data:image/png;base64,", "data:image/png;base64,AAA", + "data:image/png;base64,AAAA====", "data:image/png;base64,AA=A", "data:image/png;base64,AA!A", + "data:image/png;base64,iVBORw0KGgo=\n", "data:image/png;base64,iVBORw0KGgp=", + "data:image/png;base64,AB==", "data:image/png;base64,/9j/"}) { + check(!parse_image_data_url(bad, image, error), "invalid data URL accepted"); + check(image.bytes.empty() && image.mime_type.empty(), "failed parse retained image"); + check(!error.empty() && error.find(bad) == std::string::npos, "parse error leaks URL"); + } + check(!parse_image_data_url(png, image, error, 7), "image byte cap ignored"); + check(parse_image_data_url(png, image, error, 8), "exact byte cap rejected"); + + json messages = json::array({{{"role", "user"}, {"content", json::array({ + text_part("before"), image_part(jpeg), text_part("between"), image_part(png), text_part("after")})}}}); + json normalized; + std::vector images; + check(extract_chat_images(messages, normalized, images, error), "ordered images rejected"); + check(images.size() == 2 && images[0].mime_type == "image/jpeg" && images[1].mime_type == "image/png", "image order differs"); + std::string text; + for (const auto & part : normalized[0]["content"]) text += part.at("text").get(); + check(text == "before" + std::string(DS4_IMAGE_PLACEHOLDER) + "between" + DS4_IMAGE_PLACEHOLDER + "after", "text/image placement differs"); + check(messages[0]["content"][1]["image_url"]["url"] == jpeg, "input JSON mutated"); + check(normalized.dump().find("base64") == std::string::npos, "normalized messages retain bytes"); + + const json plain = json::array({{{"role", "user"}, {"content", "hello"}}, + {{"role", "assistant"}, {"content", "hi"}, {"tool_calls", json::array()}}}); + check(extract_chat_images(plain, normalized, images, error) && normalized == plain && images.empty(), "text-only request changed"); + + for (const auto & bad : std::vector{ + json::array({{{"role", "user"}, {"content", DS4_IMAGE_PLACEHOLDER}}}), + json::array({{{"role", "user"}, {"content", json::array({text_part("<|deepseek_"), text_part("image|>")})}}}), + json::array({{{"role", "assistant"}, {"reasoning_content", DS4_IMAGE_PLACEHOLDER}, {"content", "hi"}}}), + json::array({{{"role", "assistant"}, {"content", json::array({image_part(png)})}}}), + json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}, {"image_url", 42}}})}}}), + json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}, {"image_url", {{"url", 42}}}}})}}}), + json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}}})}}})}) { + check(!extract_chat_images(bad, normalized, images, error), "invalid image message accepted"); + check(images.empty() && normalized.is_null(), "failed extraction retains partial state"); + } + ImageInputLimits limits; + limits.image_count = 1; + check(!extract_chat_images(messages, normalized, images, error, limits), "image count cap ignored"); + limits.image_count = 4; + limits.request_bytes = 10; + check(!extract_chat_images(messages, normalized, images, error, limits), "aggregate byte cap ignored"); + limits.request_bytes = 11; + check(extract_chat_images(messages, normalized, images, error, limits), "exact aggregate cap rejected"); + + json status = {{"messages", messages}, {"raw_body", {{"nested", image_part(png)}}}}; + redact_image_urls(status); + check(status.dump().find("base64") == std::string::npos, "status leaks image data URL"); + check(status["messages"][0]["content"][0]["text"] == "before", "redaction alters ordinary text"); + std::cout << "PASS: strict bounded data URLs, ordered content, placeholder rejection, transactional failures, redaction\n"; + } catch (const std::exception & e) { + std::cerr << "FAIL: " << e.what() << '\n'; + return 1; + } +} From e09f8dafe3a87b5e775a4c4d1d6f5c5ff600aaea Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:03:22 -0400 Subject: [PATCH 026/123] feat(ds4v): parse bounded image data URLs and preserve content order --- server/src/server/image_input.cpp | 167 ++++++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 9 deletions(-) diff --git a/server/src/server/image_input.cpp b/server/src/server/image_input.cpp index 70c1a2ff3..8aadb3898 100644 --- a/server/src/server/image_input.cpp +++ b/server/src/server/image_input.cpp @@ -1,19 +1,168 @@ #include "image_input.h" +#include +#include +#include + namespace dflash::common { -bool parse_image_data_url(std::string_view, EncodedImage & image, - std::string & error, size_t) { +namespace { +int base64_value(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} +bool reserved_placeholder(std::string_view text) { + return text.find(DS4_IMAGE_PLACEHOLDER) != std::string_view::npos; +} +void require(bool valid, const char * message) { + if (!valid) throw std::invalid_argument(message); +} +} + +bool parse_image_data_url(std::string_view url, EncodedImage & image, + std::string & error, size_t max_bytes) { image = {}; - error = "image data URLs are not implemented"; - return false; + error.clear(); + try { + EncodedImage parsed; + constexpr std::string_view jpeg = "data:image/jpeg;base64,"; + constexpr std::string_view png = "data:image/png;base64,"; + if (url.substr(0, jpeg.size()) == jpeg) { + parsed.mime_type = "image/jpeg"; + url.remove_prefix(jpeg.size()); + } else if (url.substr(0, png.size()) == png) { + parsed.mime_type = "image/png"; + url.remove_prefix(png.size()); + } else { + throw std::invalid_argument("image_url must be a JPEG or PNG base64 data URL"); + } + require(!url.empty() && url.size() % 4 == 0, "invalid image base64 length"); + size_t padding = url.back() == '=' ? 1 : 0; + if (url[url.size() - 2] == '=') ++padding; + const size_t decoded_bytes = url.size() / 4 * 3 - padding; + require(decoded_bytes > 0 && decoded_bytes <= max_bytes, "image exceeds encoded byte limit"); + for (size_t i = 0; i < url.size() - padding; ++i) { + require(base64_value(url[i]) >= 0, "invalid image base64 alphabet or padding"); + } + for (size_t i = url.size() - padding; i < url.size(); ++i) { + require(url[i] == '=', "invalid image base64 padding"); + } + if (padding == 1) require((base64_value(url[url.size() - 2]) & 3) == 0, "noncanonical image base64 padding"); + if (padding == 2) require((base64_value(url[url.size() - 3]) & 15) == 0, "noncanonical image base64 padding"); + parsed.bytes.reserve(decoded_bytes); + for (size_t i = 0; i < url.size(); i += 4) { + uint32_t value = 0; + for (size_t j = 0; j < 4; ++j) { + value = value << 6 | uint32_t(url[i+j] == '=' ? 0 : base64_value(url[i+j])); + } + for (int shift : {16, 8, 0}) { + if (parsed.bytes.size() < decoded_bytes) parsed.bytes.push_back(uint8_t(value >> shift)); + } + } + constexpr std::array jpeg_signature{255, 216, 255}; + constexpr std::array png_signature{137, 80, 78, 71, 13, 10, 26, 10}; + if (parsed.mime_type == "image/jpeg") { + require(parsed.bytes.size() >= jpeg_signature.size() && + std::equal(jpeg_signature.begin(), jpeg_signature.end(), parsed.bytes.begin()), + "image bytes do not match JPEG media type"); + } else { + require(parsed.bytes.size() >= png_signature.size() && + std::equal(png_signature.begin(), png_signature.end(), parsed.bytes.begin()), + "image bytes do not match PNG media type"); + } + image = std::move(parsed); + return true; + } catch (const std::exception & e) { + error = e.what(); + return false; + } } -bool extract_chat_images(const nlohmann::json &, nlohmann::json & normalized, + +bool extract_chat_images(const nlohmann::json & messages, nlohmann::json & normalized, std::vector & images, - std::string & error, const ImageInputLimits &) { + std::string & error, const ImageInputLimits & limits) { normalized = nullptr; images.clear(); - error = "chat image transport is not implemented"; - return false; + error.clear(); + try { + require(messages.is_array(), "image messages must be an array"); + nlohmann::json result = messages; + std::vector collected; + size_t total_bytes = 0; + for (auto & message : result) { + require(message.is_object(), "each image message must be an object"); + if (message.contains("reasoning_content") && message["reasoning_content"].is_string()) { + require(!reserved_placeholder(message["reasoning_content"].get_ref()), + "text contains the reserved image placeholder"); + } + if (!message.contains("content")) continue; + auto & content = message["content"]; + if (content.is_string()) { + require(!reserved_placeholder(content.get_ref()), + "text contains the reserved image placeholder"); + continue; + } + if (!content.is_array()) continue; + std::string text_segment; + for (auto & part : content) { + require(part.is_object(), "each content part must be an object"); + const auto type = part.value("type", std::string()); + if (type == "text" || type == "input_text" || type == "output_text") { + text_segment += part.value("text", std::string()); + continue; + } + require(type != "image" && type != "input_image", "use image_url content parts for images"); + if (type != "image_url") continue; + require(!reserved_placeholder(text_segment), "text contains the reserved image placeholder"); + text_segment.clear(); + require(message.value("role", std::string("user")) == "user", "images are supported only in user messages"); + require(collected.size() < limits.image_count, "too many images in request"); + require(part.contains("image_url") && part["image_url"].is_object() && + part["image_url"].contains("url") && part["image_url"]["url"].is_string(), + "image_url must contain a string url"); + const auto & descriptor = part["image_url"]; + if (descriptor.contains("detail")) { + require(descriptor["detail"].is_string(), "image detail must be auto, low, or high"); + const auto detail = descriptor["detail"].get(); + require(detail == "auto" || detail == "low" || detail == "high", "image detail must be auto, low, or high"); + } + require(total_bytes <= limits.request_bytes, "images exceed request byte limit"); + EncodedImage decoded; + std::string decode_error; + const size_t remaining = std::min(limits.image_bytes, limits.request_bytes - total_bytes); + if (!parse_image_data_url(descriptor["url"].get_ref(), decoded, decode_error, remaining)) { + error = std::move(decode_error); + return false; + } + total_bytes += decoded.bytes.size(); + collected.push_back(std::move(decoded)); + part = {{"type", "text"}, {"text", DS4_IMAGE_PLACEHOLDER}}; + } + require(!reserved_placeholder(text_segment), "text contains the reserved image placeholder"); + } + normalized = std::move(result); + images = std::move(collected); + return true; + } catch (const nlohmann::json::exception &) { + error = "invalid image content field type"; + return false; + } catch (const std::exception & e) { + error = e.what(); + return false; + } +} + +void redact_image_urls(nlohmann::json & value) { + if (value.is_object()) { + for (auto & item : value.items()) { + if (item.key() == "image_url") item.value() = "[image omitted]"; + else redact_image_urls(item.value()); + } + } else if (value.is_array()) { + for (auto & item : value) redact_image_urls(item); + } } -void redact_image_urls(nlohmann::json &) {} } // namespace dflash::common From 5da9272ece0083152f6b56beb4351c2fe660a317 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:03:28 -0400 Subject: [PATCH 027/123] docs(ds4v): record image policy contracts and CPU proof --- server/src/deepseek4/deepseek4_image_policy.h | 1 + server/tools/ds4v_image_policy/README.md | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 server/tools/ds4v_image_policy/README.md diff --git a/server/src/deepseek4/deepseek4_image_policy.h b/server/src/deepseek4/deepseek4_image_policy.h index 2bb45f3c5..213e639e3 100644 --- a/server/src/deepseek4/deepseek4_image_policy.h +++ b/server/src/deepseek4/deepseek4_image_policy.h @@ -15,6 +15,7 @@ struct ImageExpertSelection { }; // Scores are unbiased sqrt(softplus(logits)); bias affects selection only. +// Input arrays contain experts readable floats and do not alias output. // Equal corrected scores select lower expert indices first. Torch topk does // not specify equal-score ordering, so tie parity is not part of this contract. bool select_image_experts(const float * scores, const float * bias, size_t experts, diff --git a/server/tools/ds4v_image_policy/README.md b/server/tools/ds4v_image_policy/README.md new file mode 100644 index 000000000..f9c2ae807 --- /dev/null +++ b/server/tools/ds4v_image_policy/README.md @@ -0,0 +1,68 @@ +# DS4V image selection and raw visibility policy + +This standalone C++17 unit is independent of the native tower. It adds no decoder, +loader, graph, image transport, or server wiring. + +`select_image_experts` consumes finite, nonnegative F32 unbiased scores from +sqrt(softplus(router logits)) and finite image biases. It ranks score+bias, then +normalizes the selected **unbiased** scores in F32 and applies route scale 1.5. +The result owns bounded arrays for at most 256 experts. Invalid counts, pointers, +nonfinite values, overflowed corrected scores/sums, and zero selected sums fail +with a cleared result and an error. Input pointers must address `experts` readable +floats and must not alias the output. No router matmul or token/hash dispatch is +performed here. Equal corrected scores select lower indices first; this is an +explicit deterministic rule, not a claim of equal-score ordering parity with +Torch or every GGML kernel. + +`raw_key_visible` tests one absolute query/key pair. An image range is +[IMAGE_START, IMAGE_END+1), excluding leading compression padding. Queries in that +range see the union of the complete range and ordinary causal sliding-window +positions. Queries outside it retain ordinary visibility. Pass (-1,-1) when no +range applies. Nonnegative query/key, positive window, and an absent or ordered +nonnegative range are required; invalid arguments return false with `visible` +cleared. The helper uses differences rather than query+1, including at INT64_MAX. +It owns no image block, allocates no mask, and changes no compressed-row policy. +The eventual request boundary must supply validated sequence/image spans; this +scalar predicate has no sequence length or source image-token budget to validate. + +## Verification on soulf + +Tests were committed first at e30c1b0. The remote Release build succeeded and +CTest failed with `valid selection rejected` before implementation. At 60ce979, +the same tests passed, covering unbiased weighting, selection bias, deterministic +ties, malformed contracts, causal/image boundaries, and near-INT64_MAX positions. +All build and fixture execution took place on soulf with at most two threads. + +From the isolated `~/lucebox-ds4v-policy` checkout: + +```sh +cmake -S server/tools/ds4v_image_policy -B /tmp/ds4v-image-policy-build -DCMAKE_BUILD_TYPE=Release +cmake --build /tmp/ds4v-image-policy-build -j2 +ctest --test-dir /tmp/ds4v-image-policy-build --output-on-failure +OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 ~/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python server/tools/ds4v_image_policy/verify_fixtures.py /tmp/ds4v-image-policy-build/ds4v_image_policy_probe ~/lucebox-ds4v-mix-fix/artifacts/routing-mask-reference ~/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored artifacts/image-policy/fixtures +``` + +The fixture verifier checks every original fixture hash and the parent model.py +hash before use. It executes only the exact source `linear` function and the +score-producing AST statements from `Gate.forward`, using the saved original +F32 input/router fixtures. New scores and native outputs go to a separate +evidence directory; neither source fixtures nor the Python environment changes. +Production code does not depend on Python, Torch, NumPy, or GGML. + +All 120 selected image expert indices match exactly across layers 0, 2, 3, 42 +and all five image token kinds. Maximum absolute weight errors are 2.9802322e-08, +5.9604645e-08, 0, and 0 respectively, against the unchanged 1e-6 limit. First +three-layer fixtures demonstrate learned image selection differs from their +original text hash rows. This proves calling the image policy for all five kinds; +it does not prove integrated hash bypass. + +Raw visibility matches all 433,562 query/key booleans exactly: 72,361 for the +one-image fixture and 361,201 for two images. This includes leading padding, +text before/between/after images, future-image isolation, and an image span longer +than the sliding window. The native probe writes full masks only for qualification; +the production predicate remains scalar. + +Evidence: `~/lucebox-ds4v-policy/artifacts/image-policy/{red.log,green.log,fixtures.log,fixtures/verdict.json}`. +Verdict: **PASS for this unintegrated CPU policy unit.** GPU graph execution, +bias_vl loading, text routing preservation in the integrated backend, HTTP image +behavior, and full-model output remain outside this unit. From 4acc20d0965911d02ab3d9a14ded0e7ec2eea2d1 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:05:39 -0400 Subject: [PATCH 028/123] docs(ds4v): record image transport contract and proof --- server/tools/ds4v_image_input/README.md | 43 +++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 server/tools/ds4v_image_input/README.md diff --git a/server/tools/ds4v_image_input/README.md b/server/tools/ds4v_image_input/README.md new file mode 100644 index 000000000..9cba0c063 --- /dev/null +++ b/server/tools/ds4v_image_input/README.md @@ -0,0 +1,43 @@ +# DS4V image transport unit + +This is an unintegrated parser and log-redaction component. It does not decode +JPEG/PNG pixels, invoke a model, fetch URLs, or add an HTTP endpoint. + +`parse_image_data_url` accepts canonical padded base64 data URLs with media type +`image/jpeg` or `image/png`. It rejects malformed alphabet/padding, nonzero unused +padding bits, empty payloads, incorrect media signatures, and oversized payloads +before publishing an output. JPEG/PNG structural validity belongs to the native +decoder; transport tests intentionally include signature-only inputs. + +`extract_chat_images` retains the order of user text and image parts and substitutes +the parent's `<|deepseek_image|>` placeholder for each image. It returns owned +JPEG/PNG bytes separately. Defaults are16MiB per encoded image,32MiB aggregate, +and four images. These limits count image bytes after base64 decoding, before +pixel decoding. Output is cleared on failure and input JSON is unchanged. +Literal image placeholders in text/reasoning are rejected, including placeholders +split across adjacent text parts. Images in other message roles are rejected. +The standard `detail` values are accepted; source preprocessing uses its fixed +model recipe for all three values. + +Only base64 data URLs are supported in this first unit. Remote HTTP(S), filesystem +paths, other media types, and other APIs' image part schemas fail explicitly. +Text-only message arrays preserve their JSON representation. + +`redact_image_urls` replaces image_url fields before a caller serializes status +or diagnostic JSON. Request integration must invoke it before dumping messages +or raw bodies. This helper alone does not prove that a live server is redacted. + +The pure unit was tested on soulf: RED at0306a3f (`valid JPEG transport rejected`), +GREEN ate09f8da. Tests cover canonical JPEG/PNG bytes, malformed base64, per-image +and aggregate/count limits, exact limit boundaries, content order, input immutability, +placeholder injection, malformed descriptors, failure cleanup, and nested redaction. + +```sh +cmake -S server/tools/ds4v_image_input -B /tmp/ds4v-transport-build -DCMAKE_BUILD_TYPE=Release +cmake --build /tmp/ds4v-transport-build -j2 +ctest --test-dir /tmp/ds4v-transport-build --output-on-failure +``` + +The standalone CMake target uses the same pinned nlohmann/json commit as the server. +It needs no GPU SDK or GGML. All builds and tests ran on soulf in +`~/lucebox-ds4v-transport`; evidence is in `artifacts/transport/` there. From 1fa0a2ed478817ef4f767e37618fb8a47b1dda37 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:07:55 -0400 Subject: [PATCH 029/123] feat(ds4v): add bounded JPEG and PNG decode gate --- .../src/deepseek4/deepseek4_vision_decode.cpp | 239 ++++++++++++++++++ .../src/deepseek4/deepseek4_vision_decode.h | 62 +++++ .../ds4v_preprocess_probe/CMakeLists.txt | 33 +++ server/tools/ds4v_preprocess_probe/README.md | 6 +- .../ds4v_preprocess_probe.cpp | 54 ++++ .../generate_reference_fixtures.py | 1 + 6 files changed, 392 insertions(+), 3 deletions(-) create mode 100644 server/src/deepseek4/deepseek4_vision_decode.cpp create mode 100644 server/src/deepseek4/deepseek4_vision_decode.h diff --git a/server/src/deepseek4/deepseek4_vision_decode.cpp b/server/src/deepseek4/deepseek4_vision_decode.cpp new file mode 100644 index 000000000..7ffb67158 --- /dev/null +++ b/server/src/deepseek4/deepseek4_vision_decode.cpp @@ -0,0 +1,239 @@ +#include "deepseek4_vision_decode.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace dflash::vision { +namespace { + +DecodeResult fail(DecodeError code, std::string message) { + DecodeResult result; + result.status = {code, std::move(message)}; + return result; +} + +DecodeStatus validate_encoded(const EncodedImageView & encoded, const DecodeLimits & limits) { + if (encoded.data == nullptr || encoded.size == 0) { + return {DecodeError::EmptyInput, "encoded image is empty"}; + } + if (limits.max_encoded_bytes == 0 || encoded.size > limits.max_encoded_bytes) { + return {DecodeError::EncodedTooLarge, "encoded image exceeds the 16 MiB limit"}; + } + return {}; +} + +DecodeStatus validate_decoded( + std::uint32_t width, + std::uint32_t height, + const DecodeLimits & limits, + std::size_t & output_bytes) { + const auto status = validate_decoded_dimensions(width, height, limits.decoded); + if (!status) { + return {DecodeError::DecodedTooLarge, status.message}; + } + const std::uint64_t pixels = static_cast(width) * height; + if (pixels > std::numeric_limits::max() / 3) { + return {DecodeError::DecodedTooLarge, "decoded RGB byte count overflows size_t"}; + } + output_bytes = static_cast(pixels * 3); + return {}; +} + +struct JpegErrorManager { + jpeg_error_mgr base; + std::jmp_buf jump; + char message[JMSG_LENGTH_MAX] = {}; + int warnings = 0; +}; + +extern "C" void jpeg_error_exit(j_common_ptr common) { + auto * error = reinterpret_cast(common->err); + error->base.format_message(common, error->message); + std::longjmp(error->jump, 1); +} + +extern "C" void jpeg_emit_message(j_common_ptr common, int level) { + auto * error = reinterpret_cast(common->err); + if (level < 0) { + ++error->warnings; + error->base.format_message(common, error->message); + } +} + +DecodeResult decode_jpeg(const EncodedImageView & encoded, const DecodeLimits & limits) { + DecodeResult result; + jpeg_decompress_struct decoder{}; + JpegErrorManager error{}; + volatile bool decoder_created = false; + std::uint8_t * volatile output = nullptr; + std::size_t output_bytes = 0; + + decoder.err = jpeg_std_error(&error.base); + error.base.error_exit = jpeg_error_exit; + error.base.emit_message = jpeg_emit_message; + if (setjmp(error.jump) != 0) { + if (decoder_created) { + jpeg_destroy_decompress(&decoder); + } + std::free(output); + return fail(DecodeError::MalformedImage, + error.message[0] == '\0' ? "malformed JPEG" : error.message); + } + + jpeg_create_decompress(&decoder); + decoder_created = true; + jpeg_mem_src(&decoder, encoded.data, static_cast(encoded.size)); + if (jpeg_read_header(&decoder, TRUE) != JPEG_HEADER_OK) { + jpeg_destroy_decompress(&decoder); + return fail(DecodeError::MalformedImage, "JPEG header is incomplete"); + } + decoder.out_color_space = JCS_RGB; + jpeg_calc_output_dimensions(&decoder); + if (decoder.output_width > std::numeric_limits::max() || + decoder.output_height > std::numeric_limits::max()) { + jpeg_destroy_decompress(&decoder); + return fail(DecodeError::DecodedTooLarge, "JPEG dimensions are out of range"); + } + if (const auto status = validate_decoded( + static_cast(decoder.output_width), + static_cast(decoder.output_height), + limits, + output_bytes); + !status) { + jpeg_destroy_decompress(&decoder); + result.status = status; + return result; + } + if (!jpeg_start_decompress(&decoder) || decoder.output_components != 3) { + jpeg_destroy_decompress(&decoder); + return fail(DecodeError::MalformedImage, "JPEG cannot be converted to RGB"); + } + + output = static_cast(std::malloc(output_bytes)); + if (output == nullptr) { + jpeg_destroy_decompress(&decoder); + return fail(DecodeError::AllocationFailed, "cannot allocate decoded JPEG RGB buffer"); + } + const std::size_t stride = static_cast(decoder.output_width) * 3; + while (decoder.output_scanline < decoder.output_height) { + JSAMPROW row = output + + static_cast(decoder.output_scanline) * stride; + if (jpeg_read_scanlines(&decoder, &row, 1) != 1) { + jpeg_destroy_decompress(&decoder); + std::free(output); + return fail(DecodeError::MalformedImage, "JPEG scanline data is incomplete"); + } + } + if (!jpeg_finish_decompress(&decoder) || error.warnings != 0) { + jpeg_destroy_decompress(&decoder); + std::free(output); + return fail(DecodeError::MalformedImage, + error.message[0] == '\0' ? "JPEG has decoding warnings" : error.message); + } + + result.image.width = static_cast(decoder.output_width); + result.image.height = static_cast(decoder.output_height); + jpeg_destroy_decompress(&decoder); + decoder_created = false; + try { + const auto * begin = output; + result.image.pixels.assign(begin, begin + output_bytes); + } catch (...) { + std::free(output); + return fail(DecodeError::AllocationFailed, "cannot own decoded JPEG RGB buffer"); + } + std::free(output); + result.status = {}; + return result; +} + +DecodeResult decode_png(const EncodedImageView & encoded, const DecodeLimits & limits) { + LodePNGState state; + lodepng_state_init(&state); + unsigned width = 0; + unsigned height = 0; + const unsigned inspect_error = + lodepng_inspect(&width, &height, &state, encoded.data, encoded.size); + lodepng_state_cleanup(&state); + if (inspect_error != 0) { + return fail(DecodeError::MalformedImage, + std::string("malformed PNG: ") + lodepng_error_text(inspect_error)); + } + + std::size_t output_bytes = 0; + if (const auto status = validate_decoded(width, height, limits, output_bytes); !status) { + DecodeResult result; + result.status = status; + return result; + } + unsigned char * output = nullptr; + unsigned decoded_width = 0; + unsigned decoded_height = 0; + const unsigned decode_error = lodepng_decode24( + &output, &decoded_width, &decoded_height, encoded.data, encoded.size); + if (decode_error != 0) { + std::free(output); + return fail(DecodeError::MalformedImage, + std::string("malformed PNG: ") + lodepng_error_text(decode_error)); + } + if (decoded_width != width || decoded_height != height) { + std::free(output); + return fail(DecodeError::MalformedImage, "PNG dimensions changed during decode"); + } + + DecodeResult result; + result.image.width = width; + result.image.height = height; + try { + result.image.pixels.assign(output, output + output_bytes); + } catch (...) { + std::free(output); + return fail(DecodeError::AllocationFailed, "cannot own decoded PNG RGB buffer"); + } + std::free(output); + result.status = {}; + return result; +} + +} // namespace + +DecodeResult decode_image(const EncodedImageView & encoded, const DecodeLimits & limits) { + if (const auto status = validate_encoded(encoded, limits); !status) { + DecodeResult result; + result.status = status; + return result; + } + constexpr std::uint8_t png_signature[] = {137, 80, 78, 71, 13, 10, 26, 10}; + if (encoded.size >= sizeof(png_signature) && + std::memcmp(encoded.data, png_signature, sizeof(png_signature)) == 0) { + return decode_png(encoded, limits); + } + if (encoded.size >= 3 && encoded.data[0] == 0xFF && encoded.data[1] == 0xD8 && + encoded.data[2] == 0xFF) { + return decode_jpeg(encoded, limits); + } + return fail(DecodeError::UnsupportedFormat, "encoded image is neither JPEG nor PNG"); +} + +const char * decode_error_name(DecodeError error) { + switch (error) { + case DecodeError::None: return "none"; + case DecodeError::EmptyInput: return "empty_input"; + case DecodeError::EncodedTooLarge: return "encoded_too_large"; + case DecodeError::UnsupportedFormat: return "unsupported_format"; + case DecodeError::MalformedImage: return "malformed_image"; + case DecodeError::DecodedTooLarge: return "decoded_too_large"; + case DecodeError::AllocationFailed: return "allocation_failed"; + } + return "unknown"; +} + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_vision_decode.h b/server/src/deepseek4/deepseek4_vision_decode.h new file mode 100644 index 000000000..4688669d2 --- /dev/null +++ b/server/src/deepseek4/deepseek4_vision_decode.h @@ -0,0 +1,62 @@ +#pragma once + +#include "deepseek4_vision_preprocess.h" + +#include +#include +#include +#include + +namespace dflash::vision { + +struct EncodedImageView { + const std::uint8_t * data = nullptr; + std::size_t size = 0; +}; + +struct DecodeLimits { + std::size_t max_encoded_bytes = 16ULL * 1024ULL * 1024ULL; + PreprocessLimits decoded; +}; + +struct DecodedRgb { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::vector pixels; + + DecodedRgbView view() const { + return {width, height, pixels.data(), pixels.size()}; + } +}; + +enum class DecodeError { + None = 0, + EmptyInput, + EncodedTooLarge, + UnsupportedFormat, + MalformedImage, + DecodedTooLarge, + AllocationFailed, +}; + +struct DecodeStatus { + DecodeError code = DecodeError::None; + std::string message; + + explicit operator bool() const { return code == DecodeError::None; } +}; + +struct DecodeResult { + DecodeStatus status; + DecodedRgb image; + + explicit operator bool() const { return static_cast(status); } +}; + +DecodeResult decode_image( + const EncodedImageView & encoded, + const DecodeLimits & limits = {}); + +const char * decode_error_name(DecodeError error); + +} // namespace dflash::vision diff --git a/server/tools/ds4v_preprocess_probe/CMakeLists.txt b/server/tools/ds4v_preprocess_probe/CMakeLists.txt index 27e245176..5951a09f5 100644 --- a/server/tools/ds4v_preprocess_probe/CMakeLists.txt +++ b/server/tools/ds4v_preprocess_probe/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.20) project(ds4v_preprocess_probe LANGUAGES CXX) +option(DS4V_PREPROCESS_WITH_CODECS "Build pinned JPEG/PNG decoder gate" ON) + add_executable(ds4v_preprocess_probe ds4v_preprocess_probe.cpp ../../src/deepseek4/deepseek4_vision_preprocess.cpp) @@ -10,5 +12,36 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") target_compile_options(ds4v_preprocess_probe PRIVATE -Wall -Wextra -Wpedantic -Werror) endif() +if(DS4V_PREPROCESS_WITH_CODECS) + include(FetchContent) + + set(ENABLE_SHARED OFF CACHE BOOL "" FORCE) + set(ENABLE_STATIC ON CACHE BOOL "" FORCE) + set(WITH_TOOLS OFF CACHE BOOL "" FORCE) + set(WITH_TESTS OFF CACHE BOOL "" FORCE) + set(WITH_SIMD OFF CACHE BOOL "" FORCE) + FetchContent_Declare(libjpeg_turbo + URL https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.1.4.1/libjpeg-turbo-3.1.4.1.tar.gz + URL_HASH SHA256=ecae8008e2cc9ade2f2c1bb9d5e6d4fb73e7c433866a056bd82980741571a022 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + FetchContent_MakeAvailable(libjpeg_turbo) + + FetchContent_Declare(lodepng + URL https://github.com/lvandeve/lodepng/archive/ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a.tar.gz + URL_HASH SHA256=c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + FetchContent_MakeAvailable(lodepng) + add_library(ds4v_lodepng STATIC ${lodepng_SOURCE_DIR}/lodepng.cpp) + target_include_directories(ds4v_lodepng PUBLIC ${lodepng_SOURCE_DIR}) + + target_sources(ds4v_preprocess_probe PRIVATE + ../../src/deepseek4/deepseek4_vision_decode.cpp) + target_include_directories(ds4v_preprocess_probe PRIVATE + ${libjpeg_turbo_SOURCE_DIR} + ${libjpeg_turbo_BINARY_DIR}) + target_compile_definitions(ds4v_preprocess_probe PRIVATE DS4V_PREPROCESS_WITH_CODECS=1) + target_link_libraries(ds4v_preprocess_probe PRIVATE jpeg-static ds4v_lodepng) +endif() + enable_testing() add_test(NAME ds4v_preprocess_self_test COMMAND ds4v_preprocess_probe --self-test) diff --git a/server/tools/ds4v_preprocess_probe/README.md b/server/tools/ds4v_preprocess_probe/README.md index 20787da3a..8dcbe8d5e 100644 --- a/server/tools/ds4v_preprocess_probe/README.md +++ b/server/tools/ds4v_preprocess_probe/README.md @@ -10,7 +10,7 @@ python generate_reference_fixtures.py \ --output /tmp/ds4v-preprocess-fixtures ``` -Build and run the probe: +Build and run the probe. Its default codec gate downloads the pinned source archives for libjpeg-turbo 3.1.4.1 and LodePNG commit `ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a`: ```sh cmake -S . -B build -DCMAKE_BUILD_TYPE=Release @@ -19,8 +19,8 @@ ctest --test-dir build --output-on-failure ./build/ds4v_preprocess_probe --fixtures /tmp/ds4v-preprocess-fixtures ``` -The fixture check compares resized RGB bytes, BF16 patch words, dimensions, all five grounded start-position layouts, permutations, spans, and a second deterministic run. The built-in self-test covers invalid fixed config, invalid and oversized decoded dimensions without allocating them, wrong RGB byte counts, output bounds, layout budget overflow, and absolute-position overflow. +The fixture check compares JPEG/PNG decoded RGB, resized RGB bytes, BF16 patch words, dimensions, all five grounded start-position layouts, permutations, spans, and second deterministic decode and preprocessing runs. The built-in self-test covers malformed and truncated JPEG/PNG, unsupported formats, encoded and decoded bounds, invalid fixed config, invalid decoded dimensions, wrong RGB byte counts, output bounds, layout budget overflow, and absolute-position overflow. -JPEG and PNG decoding are a separate gate. This target accepts already decoded interleaved RGB bytes and has no image-codec or Python runtime dependency. +The production boundary remains split: `decode_image` owns bounded JPEG/PNG byte decoding, while `preprocess_rgb` accepts already decoded interleaved RGB and has no image-codec or Python runtime dependency. Configure with `-DDS4V_PREPROCESS_WITH_CODECS=OFF` to build and test the RGB unit without downloading codec dependencies. The resampler follows Pillow 12.3.0 `src/libImaging/Resample.c`, including signed 22-bit fixed-point bicubic coefficients and the tall-image vertical-first path. diff --git a/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp b/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp index 8153ffa8c..b93d9ccc4 100644 --- a/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp +++ b/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp @@ -1,4 +1,7 @@ #include "deepseek4_vision_preprocess.h" +#ifdef DS4V_PREPROCESS_WITH_CODECS +#include "deepseek4_vision_decode.h" +#endif #include #include @@ -22,6 +25,11 @@ using dflash::vision::PreprocessError; using dflash::vision::PreprocessLimits; using dflash::vision::PreprocessResult; using dflash::vision::ResizePlan; +#ifdef DS4V_PREPROCESS_WITH_CODECS +using dflash::vision::DecodeError; +using dflash::vision::DecodeLimits; +using dflash::vision::EncodedImageView; +#endif namespace { @@ -193,6 +201,28 @@ void require_same_result(const PreprocessResult & first, const PreprocessResult void verify_fixture(const fs::path & root, const FixtureCase & item) { const fs::path case_dir = root / item.label; const auto input = read_binary(case_dir / "input.rgb"); +#ifdef DS4V_PREPROCESS_WITH_CODECS + const auto encoded = read_binary(case_dir / "encoded.bin"); + const auto decoded = dflash::vision::decode_image({encoded.data(), encoded.size()}); + require(static_cast(decoded), + item.label + " decode failed: " + + dflash::vision::decode_error_name(decoded.status.code) + ": " + + decoded.status.message); + require(decoded.image.width == item.input_width && decoded.image.height == item.input_height, + item.label + " decoded dimensions mismatch"); + require_equal(decoded.image.pixels, input, item.label + " decoded RGB"); + const auto decoded_again = dflash::vision::decode_image({encoded.data(), encoded.size()}); + require(static_cast(decoded_again), item.label + " repeated decode failed"); + require_equal(decoded_again.image.pixels, decoded.image.pixels, + item.label + " deterministic decode"); + + DecodeLimits one_pixel; + one_pixel.decoded.max_decoded_pixels = 1; + const auto bounded = dflash::vision::decode_image( + {encoded.data(), encoded.size()}, one_pixel); + require(bounded.status.code == DecodeError::DecodedTooLarge && bounded.image.pixels.empty(), + item.label + " decoded pixel cap did not fail before output allocation"); +#endif const DecodedRgbView view{item.input_width, item.input_height, input.data(), input.size()}; const PreprocessResult result = dflash::vision::preprocess_rgb(view, 0); require(static_cast(result), @@ -293,6 +323,30 @@ void self_test() { require(layout.span.block_begin == 0 && layout.span.visible_begin == 3 && layout.span.visible_end == 13 && layout.span.block_end == 13, "known layout span mismatch"); +#ifdef DS4V_PREPROCESS_WITH_CODECS + const std::array unsupported = {'G', 'I', 'F', '8', '9', 'a'}; + require(dflash::vision::decode_image({nullptr, 0}).status.code == DecodeError::EmptyInput, + "empty encoded image was accepted"); + require(dflash::vision::decode_image({unsupported.data(), unsupported.size()}).status.code == + DecodeError::UnsupportedFormat, + "unsupported encoded format was accepted"); + const std::array truncated_jpeg = {0xFF, 0xD8, 0xFF, 0xD9}; + require(dflash::vision::decode_image( + {truncated_jpeg.data(), truncated_jpeg.size()}).status.code == + DecodeError::MalformedImage, + "truncated JPEG was accepted"); + const std::array truncated_png = {137, 80, 78, 71, 13, 10, 26, 10}; + require(dflash::vision::decode_image( + {truncated_png.data(), truncated_png.size()}).status.code == + DecodeError::MalformedImage, + "truncated PNG was accepted"); + const std::uint8_t byte = 0; + DecodeLimits encoded_limit; + require(dflash::vision::decode_image( + {&byte, encoded_limit.max_encoded_bytes + 1}, encoded_limit).status.code == + DecodeError::EncodedTooLarge, + "oversized encoded input was inspected"); +#endif std::cout << "self-test PASS\n"; } diff --git a/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py b/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py index f4ecd0a56..d84b45b63 100644 --- a/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py +++ b/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py @@ -132,6 +132,7 @@ def main() -> int: assert torch.equal(rebuilt, patches) save_bytes(case_dir / "input.rgb", image.tobytes(), digests, args.output) + save_bytes(case_dir / "encoded.bin", encoded, digests, args.output) save_bytes(case_dir / "resized.rgb", resized.tobytes(), digests, args.output) save_bytes( case_dir / "patches.bf16", From 127bb61b1dd80d4b0d908542423933c0385ff1b0 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:07:59 -0400 Subject: [PATCH 030/123] build(ds4v): prepare explicit HIP tower qualification probe --- server/tools/ds4v_vision/CMakeLists.txt | 6 +++++- server/tools/ds4v_vision/README.md | 21 +++++++++++++++++++ server/tools/ds4v_vision/probe.cpp | 27 +++++++++++++++++++------ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index b6a2f1277..08c2efc71 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -2,8 +2,9 @@ cmake_minimum_required(VERSION 3.21) project(ds4v_vision LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) +option(DS4V_VISION_HIP "Build the optional HIP qualification probe" OFF) set(GGML_CUDA OFF CACHE BOOL "" FORCE) -set(GGML_HIP OFF CACHE BOOL "" FORCE) +set(GGML_HIP ${DS4V_VISION_HIP} CACHE BOOL "" FORCE) set(GGML_METAL OFF CACHE BOOL "" FORCE) set(GGML_VULKAN OFF CACHE BOOL "" FORCE) set(GGML_BLAS OFF CACHE BOOL "" FORCE) @@ -15,6 +16,9 @@ target_include_directories(ds4v_vision PUBLIC ../../src) target_link_libraries(ds4v_vision PUBLIC ggml) add_executable(ds4v_vision_probe probe.cpp) target_link_libraries(ds4v_vision_probe PRIVATE ds4v_vision) +if(DS4V_VISION_HIP) + target_compile_definitions(ds4v_vision_probe PRIVATE DS4V_VISION_HIP) +endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) enable_testing() diff --git a/server/tools/ds4v_vision/README.md b/server/tools/ds4v_vision/README.md index 5ad1525cb..47a65a390 100644 --- a/server/tools/ds4v_vision/README.md +++ b/server/tools/ds4v_vision/README.md @@ -128,3 +128,24 @@ bytes; corn scratch 77,774,592 bytes and 3.30829 s encode; carrots scratch load. The configured 2 GiB graph scratch cap is distinct from total process RSS. Largest permitted grids are bounded analytically and by allocation measurement; only the original 782/2562-patch grids have full numerical reference qualification. + +## Selected-base follow-up + +The full N-layout budget regression fails at5845ed5 and passes at5bf705e. +The largest grid permitted by this budget (6x561=3366patches) runs with finite +outputs and bitwise observer invariance. It is an allocation boundary fixture, +not a source resize-aspect fixture. Scratch is822488832bytes without diagnostics +and891424512bytes with them; encode times26.74/27.14seconds, peak RSS1881796KiB. +Failure/reload checks precede each successful encode; scratch release follows it. + +An optional HIP build can prepare the same probe for the later GPU window: + +```sh +ROCM_PATH=/opt/rocm-7.2.4 cmake -S server/tools/ds4v_vision -B /tmp/ds4v-runtime-hip-build -DCMAKE_BUILD_TYPE=Release -DDS4V_VISION_HIP=ON '-DCMAKE_HIP_ARCHITECTURES=gfx1100;gfx1151' -DCMAKE_HIP_COMPILER=/opt/rocm-7.2.4/lib/llvm/bin/clang++ +cmake --build /tmp/ds4v-runtime-hip-build -j2 +``` + +Append `hip:0` or `hip:1` to an encode/load-only probe command to request that +device explicitly. It fails if unavailable and never falls back to CPU. Building +the target is not GPU qualification. Do not run it on GPU before the private text +load proof and the operator's GPU window permit it. diff --git a/server/tools/ds4v_vision/probe.cpp b/server/tools/ds4v_vision/probe.cpp index 3a10dcf64..025c05ac4 100644 --- a/server/tools/ds4v_vision/probe.cpp +++ b/server/tools/ds4v_vision/probe.cpp @@ -1,5 +1,8 @@ #include "deepseek4/deepseek4_vision.h" #include "ggml-cpu.h" +#ifdef DS4V_VISION_HIP +#include "ggml-cuda.h" +#endif #include #include #include @@ -21,17 +24,29 @@ static void save(const std::string & path,const std::vector & values) { if(!f) throw std::runtime_error("output write failed: "+path); } int main(int argc,char ** argv) { - if(argc!=8 && argc!=5) { - std::cerr<<"usage: ds4v_vision_probe mmproj patches.f32 height width output-dir label stages(0|1)\n" - <<" ds4v_vision_probe mmproj --load-only dimension vocabulary\n"; return 2; + const bool load_only=argc>=3 && std::string(argv[2])=="--load-only"; + if((load_only && argc!=5 && argc!=6) || (!load_only && argc!=8 && argc!=9)) { + std::cerr<<"usage: ds4v_vision_probe mmproj patches.f32 height width output-dir label stages(0|1) [cpu|hip:0|hip:1]\n" + <<" ds4v_vision_probe mmproj --load-only dimension vocabulary [cpu|hip:0|hip:1]\n"; return 2; } - auto backend=ggml_backend_cpu_init(); - ggml_backend_cpu_set_n_threads(backend,2); + const std::string device=(load_only?argc==6:argc==9)?argv[argc-1]:"cpu"; + ggml_backend_t backend=nullptr; + if(device=="cpu") { + backend=ggml_backend_cpu_init(); + if(backend) ggml_backend_cpu_set_n_threads(backend,2); + } +#ifdef DS4V_VISION_HIP + else if(device=="hip:0" || device=="hip:1") { + const int index=device.back()-'0'; + if(index Date: Fri, 4 Sep 2026 19:08:41 -0400 Subject: [PATCH 031/123] test(ds4v): bound image message nesting before JSON copy --- server/tools/ds4v_image_input/test.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/tools/ds4v_image_input/test.cpp b/server/tools/ds4v_image_input/test.cpp index 1c50fbd8e..8cb41860a 100644 --- a/server/tools/ds4v_image_input/test.cpp +++ b/server/tools/ds4v_image_input/test.cpp @@ -75,6 +75,18 @@ int main() { limits.request_bytes = 11; check(extract_chat_images(messages, normalized, images, error, limits), "exact aggregate cap rejected"); + json deep = plain; + json * nested = &deep[0]["metadata"]; + for (int i = 0; i < 72; ++i) { + *nested = json::object(); + nested = &(*nested)["nested"]; + } + *nested = image_part(png); + check(!extract_chat_images(deep, normalized, images, error), "deep metadata accepted before copy"); + check(error == "image message nesting exceeds 64 levels", "unexpected depth rejection"); + redact_image_urls(deep); + check((*nested)["image_url"] == "[image omitted]", "deep redaction failed"); + json status = {{"messages", messages}, {"raw_body", {{"nested", image_part(png)}}}}; redact_image_urls(status); check(status.dump().find("base64") == std::string::npos, "status leaks image data URL"); From 60af19f5f1a7faf4860973c53d58d81bbe64dfbc Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:08:48 -0400 Subject: [PATCH 032/123] fix(ds4v): isolate libjpeg probe build --- .../ds4v_preprocess_probe/CMakeLists.txt | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/server/tools/ds4v_preprocess_probe/CMakeLists.txt b/server/tools/ds4v_preprocess_probe/CMakeLists.txt index 5951a09f5..81be1778d 100644 --- a/server/tools/ds4v_preprocess_probe/CMakeLists.txt +++ b/server/tools/ds4v_preprocess_probe/CMakeLists.txt @@ -13,18 +13,31 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") endif() if(DS4V_PREPROCESS_WITH_CODECS) + include(ExternalProject) include(FetchContent) - set(ENABLE_SHARED OFF CACHE BOOL "" FORCE) - set(ENABLE_STATIC ON CACHE BOOL "" FORCE) - set(WITH_TOOLS OFF CACHE BOOL "" FORCE) - set(WITH_TESTS OFF CACHE BOOL "" FORCE) - set(WITH_SIMD OFF CACHE BOOL "" FORCE) - FetchContent_Declare(libjpeg_turbo + set(DS4V_JPEG_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/libjpeg-turbo-prefix) + file(MAKE_DIRECTORY ${DS4V_JPEG_PREFIX}/include) + ExternalProject_Add(libjpeg_turbo_external URL https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.1.4.1/libjpeg-turbo-3.1.4.1.tar.gz URL_HASH SHA256=ecae8008e2cc9ade2f2c1bb9d5e6d4fb73e7c433866a056bd82980741571a022 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE) - FetchContent_MakeAvailable(libjpeg_turbo) + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + CMAKE_ARGS + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX=${DS4V_JPEG_PREFIX} + -DCMAKE_INSTALL_LIBDIR=lib + -DENABLE_SHARED=OFF + -DENABLE_STATIC=ON + -DWITH_TOOLS=OFF + -DWITH_TESTS=OFF + -DWITH_SIMD=OFF + BUILD_COMMAND ${CMAKE_COMMAND} --build --parallel 2 + BUILD_BYPRODUCTS ${DS4V_JPEG_PREFIX}/lib/libjpeg.a) + add_library(ds4v_libjpeg STATIC IMPORTED GLOBAL) + set_target_properties(ds4v_libjpeg PROPERTIES + IMPORTED_LOCATION ${DS4V_JPEG_PREFIX}/lib/libjpeg.a + INTERFACE_INCLUDE_DIRECTORIES ${DS4V_JPEG_PREFIX}/include) + add_dependencies(ds4v_libjpeg libjpeg_turbo_external) FetchContent_Declare(lodepng URL https://github.com/lvandeve/lodepng/archive/ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a.tar.gz @@ -36,11 +49,8 @@ if(DS4V_PREPROCESS_WITH_CODECS) target_sources(ds4v_preprocess_probe PRIVATE ../../src/deepseek4/deepseek4_vision_decode.cpp) - target_include_directories(ds4v_preprocess_probe PRIVATE - ${libjpeg_turbo_SOURCE_DIR} - ${libjpeg_turbo_BINARY_DIR}) target_compile_definitions(ds4v_preprocess_probe PRIVATE DS4V_PREPROCESS_WITH_CODECS=1) - target_link_libraries(ds4v_preprocess_probe PRIVATE jpeg-static ds4v_lodepng) + target_link_libraries(ds4v_preprocess_probe PRIVATE ds4v_libjpeg ds4v_lodepng) endif() enable_testing() From 25f6105c2a3520542fa1df4bfdc2f679a9a64646 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:10:06 -0400 Subject: [PATCH 033/123] fix(ds4v): validate message depth and placeholders before copying --- server/src/server/image_input.cpp | 41 ++++++++++++++++--------- server/tools/ds4v_image_input/README.md | 7 +++-- server/tools/ds4v_image_input/test.cpp | 3 ++ 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/server/src/server/image_input.cpp b/server/src/server/image_input.cpp index 8aadb3898..3e0a2044f 100644 --- a/server/src/server/image_input.cpp +++ b/server/src/server/image_input.cpp @@ -20,6 +20,20 @@ bool reserved_placeholder(std::string_view text) { void require(bool valid, const char * message) { if (!valid) throw std::invalid_argument(message); } +void validate_message_structure(const nlohmann::json & messages) { + std::vector> pending{{&messages, 0}}; + while (!pending.empty()) { + const auto [value, depth] = pending.back(); + pending.pop_back(); + require(depth <= 64, "image message nesting exceeds 64 levels"); + if (value->is_string()) { + require(!reserved_placeholder(value->get_ref()), + "text contains the reserved image placeholder"); + } else if (value->is_structured()) { + for (const auto & child : *value) pending.emplace_back(&child, depth + 1); + } + } +} } bool parse_image_data_url(std::string_view url, EncodedImage & image, @@ -89,22 +103,14 @@ bool extract_chat_images(const nlohmann::json & messages, nlohmann::json & norma error.clear(); try { require(messages.is_array(), "image messages must be an array"); + validate_message_structure(messages); nlohmann::json result = messages; std::vector collected; size_t total_bytes = 0; for (auto & message : result) { require(message.is_object(), "each image message must be an object"); - if (message.contains("reasoning_content") && message["reasoning_content"].is_string()) { - require(!reserved_placeholder(message["reasoning_content"].get_ref()), - "text contains the reserved image placeholder"); - } if (!message.contains("content")) continue; auto & content = message["content"]; - if (content.is_string()) { - require(!reserved_placeholder(content.get_ref()), - "text contains the reserved image placeholder"); - continue; - } if (!content.is_array()) continue; std::string text_segment; for (auto & part : content) { @@ -156,13 +162,18 @@ bool extract_chat_images(const nlohmann::json & messages, nlohmann::json & norma } void redact_image_urls(nlohmann::json & value) { - if (value.is_object()) { - for (auto & item : value.items()) { - if (item.key() == "image_url") item.value() = "[image omitted]"; - else redact_image_urls(item.value()); + std::vector pending{&value}; + while (!pending.empty()) { + auto * current = pending.back(); + pending.pop_back(); + if (current->is_object()) { + for (auto & item : current->items()) { + if (item.key() == "image_url") item.value() = "[image omitted]"; + else pending.push_back(&item.value()); + } + } else if (current->is_array()) { + for (auto & item : *current) pending.push_back(&item); } - } else if (value.is_array()) { - for (auto & item : value) redact_image_urls(item); } } } // namespace dflash::common diff --git a/server/tools/ds4v_image_input/README.md b/server/tools/ds4v_image_input/README.md index 9cba0c063..4163dc22d 100644 --- a/server/tools/ds4v_image_input/README.md +++ b/server/tools/ds4v_image_input/README.md @@ -14,8 +14,9 @@ the parent's `<|deepseek_image|>` placeholder for each image. It returns own JPEG/PNG bytes separately. Defaults are16MiB per encoded image,32MiB aggregate, and four images. These limits count image bytes after base64 decoding, before pixel decoding. Output is cleared on failure and input JSON is unchanged. -Literal image placeholders in text/reasoning are rejected, including placeholders -split across adjacent text parts. Images in other message roles are rejected. +Literal image placeholders in all message string values are rejected, including +tool-call fields and placeholders split across adjacent text parts. Message nesting +is limited to64levels by an iterative walk before copying JSON. Images in other message roles are rejected. The standard `detail` values are accepted; source preprocessing uses its fixed model recipe for all three values. @@ -23,7 +24,7 @@ Only base64 data URLs are supported in this first unit. Remote HTTP(S), filesyst paths, other media types, and other APIs' image part schemas fail explicitly. Text-only message arrays preserve their JSON representation. -`redact_image_urls` replaces image_url fields before a caller serializes status +`redact_image_urls` iteratively replaces image_url fields before a caller serializes status or diagnostic JSON. Request integration must invoke it before dumping messages or raw bodies. This helper alone does not prove that a live server is redacted. diff --git a/server/tools/ds4v_image_input/test.cpp b/server/tools/ds4v_image_input/test.cpp index 8cb41860a..eb84d2433 100644 --- a/server/tools/ds4v_image_input/test.cpp +++ b/server/tools/ds4v_image_input/test.cpp @@ -59,6 +59,9 @@ int main() { json::array({{{"role", "user"}, {"content", DS4_IMAGE_PLACEHOLDER}}}), json::array({{{"role", "user"}, {"content", json::array({text_part("<|deepseek_"), text_part("image|>")})}}}), json::array({{{"role", "assistant"}, {"reasoning_content", DS4_IMAGE_PLACEHOLDER}, {"content", "hi"}}}), + json::array({{{"type", "function_call_output"}, {"output", DS4_IMAGE_PLACEHOLDER}}}), + json::array({{{"type", "function_call"}, {"arguments", DS4_IMAGE_PLACEHOLDER}}}), + json::array({{{"role", "assistant"}, {"tool_calls", json::array({{{"function", {{"arguments", DS4_IMAGE_PLACEHOLDER}}}}})}}}), json::array({{{"role", "assistant"}, {"content", json::array({image_part(png)})}}}), json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}, {"image_url", 42}}})}}}), json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}, {"image_url", {{"url", 42}}}}})}}}), From 724e4ef7f600d47b4cf065f5d39ec7b0ee626ad0 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:12:04 -0400 Subject: [PATCH 034/123] chore(ds4v): keep codec probe dependency focused --- server/src/deepseek4/deepseek4_vision_decode.cpp | 2 +- server/tools/ds4v_preprocess_probe/CMakeLists.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/deepseek4/deepseek4_vision_decode.cpp b/server/src/deepseek4/deepseek4_vision_decode.cpp index 7ffb67158..1ba55fcd1 100644 --- a/server/src/deepseek4/deepseek4_vision_decode.cpp +++ b/server/src/deepseek4/deepseek4_vision_decode.cpp @@ -25,7 +25,7 @@ DecodeStatus validate_encoded(const EncodedImageView & encoded, const DecodeLimi return {DecodeError::EmptyInput, "encoded image is empty"}; } if (limits.max_encoded_bytes == 0 || encoded.size > limits.max_encoded_bytes) { - return {DecodeError::EncodedTooLarge, "encoded image exceeds the 16 MiB limit"}; + return {DecodeError::EncodedTooLarge, "encoded image exceeds the configured byte limit"}; } return {}; } diff --git a/server/tools/ds4v_preprocess_probe/CMakeLists.txt b/server/tools/ds4v_preprocess_probe/CMakeLists.txt index 81be1778d..97082afcf 100644 --- a/server/tools/ds4v_preprocess_probe/CMakeLists.txt +++ b/server/tools/ds4v_preprocess_probe/CMakeLists.txt @@ -31,6 +31,7 @@ if(DS4V_PREPROCESS_WITH_CODECS) -DWITH_TOOLS=OFF -DWITH_TESTS=OFF -DWITH_SIMD=OFF + -DWITH_TURBOJPEG=OFF BUILD_COMMAND ${CMAKE_COMMAND} --build --parallel 2 BUILD_BYPRODUCTS ${DS4V_JPEG_PREFIX}/lib/libjpeg.a) add_library(ds4v_libjpeg STATIC IMPORTED GLOBAL) From 4bf727077cf007352997798edc52f32c1f887023 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:12:59 -0400 Subject: [PATCH 035/123] fix(ds4v): reuse server HIP compatibility definitions in probe --- server/tools/ds4v_vision/CMakeLists.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 08c2efc71..4a1173889 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -11,6 +11,16 @@ set(GGML_BLAS OFF CACHE BOOL "" FORCE) set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) add_subdirectory(../../deps/llama.cpp/ggml ggml) +if(DS4V_VISION_HIP) + target_compile_definitions(ggml-hip PRIVATE + cublasSgemmStridedBatched=hipblasSgemmStridedBatched + cudaStreamCaptureStatus=hipStreamCaptureStatus + cudaStreamCaptureStatusNone=hipStreamCaptureStatusNone + cudaStreamIsCapturing=hipStreamIsCapturing) + target_include_directories(ggml-hip BEFORE PRIVATE ../../src/hip_compat) + get_filename_component(DS4V_HIP_RUNTIME_DIR "${hip_DIR}/../.." ABSOLUTE) + target_link_options(ggml-hip PRIVATE "-L${DS4V_HIP_RUNTIME_DIR}") +endif() add_library(ds4v_vision STATIC ../../src/deepseek4/deepseek4_vision.cpp) target_include_directories(ds4v_vision PUBLIC ../../src) target_link_libraries(ds4v_vision PUBLIC ggml) From 6a2b38170219201af0712a42443900eb54a3dde8 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:13:20 -0400 Subject: [PATCH 036/123] test(ds4v): cover RGB conversion and EXIF handling --- server/tools/ds4v_preprocess_probe/README.md | 2 +- .../generate_reference_fixtures.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/server/tools/ds4v_preprocess_probe/README.md b/server/tools/ds4v_preprocess_probe/README.md index 8dcbe8d5e..f7d869db1 100644 --- a/server/tools/ds4v_preprocess_probe/README.md +++ b/server/tools/ds4v_preprocess_probe/README.md @@ -19,7 +19,7 @@ ctest --test-dir build --output-on-failure ./build/ds4v_preprocess_probe --fixtures /tmp/ds4v-preprocess-fixtures ``` -The fixture check compares JPEG/PNG decoded RGB, resized RGB bytes, BF16 patch words, dimensions, all five grounded start-position layouts, permutations, spans, and second deterministic decode and preprocessing runs. The built-in self-test covers malformed and truncated JPEG/PNG, unsupported formats, encoded and decoded bounds, invalid fixed config, invalid decoded dimensions, wrong RGB byte counts, output bounds, layout budget overflow, and absolute-position overflow. +The fixture check compares JPEG/PNG decoded RGB, including PNG alpha removal and a JPEG EXIF orientation that must not be transposed, resized RGB bytes, BF16 patch words, dimensions, all five grounded start-position layouts, permutations, spans, and second deterministic decode and preprocessing runs. The built-in self-test covers malformed and truncated JPEG/PNG, unsupported formats, encoded and decoded bounds, invalid fixed config, invalid decoded dimensions, wrong RGB byte counts, output bounds, layout budget overflow, and absolute-position overflow. The production boundary remains split: `decode_image` owns bounded JPEG/PNG byte decoding, while `preprocess_rgb` accepts already decoded interleaved RGB and has no image-codec or Python runtime dependency. Configure with `-DDS4V_PREPROCESS_WITH_CODECS=OFF` to build and test the RGB unit without downloading codec dependencies. diff --git a/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py b/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py index d84b45b63..1de0161aa 100644 --- a/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py +++ b/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py @@ -94,6 +94,25 @@ def main() -> int: image.save(encoded, format="PNG") cases.append((label, image, encoded.getvalue())) + rgba_rgb = np.frombuffer(pattern(29, 17, 211), dtype=np.uint8).reshape(17, 29, 3) + alpha = ((np.indices((17, 29), dtype=np.uint16).sum(axis=0) * 23) % 256).astype(np.uint8) + rgba = Image.fromarray(np.dstack((rgba_rgb, alpha))) + encoded_rgba = io.BytesIO() + rgba.save(encoded_rgba, format="PNG") + with Image.open(io.BytesIO(encoded_rgba.getvalue())) as opened: + expected_rgba_rgb = opened.convert("RGB") + cases.append(("png-rgba", expected_rgba_rgb, encoded_rgba.getvalue())) + + jpeg_source = Image.frombytes("RGB", (19, 11), pattern(19, 11, 233)) + exif = Image.Exif() + exif[274] = 6 + encoded_jpeg = io.BytesIO() + jpeg_source.save(encoded_jpeg, format="JPEG", quality=91, exif=exif) + with Image.open(io.BytesIO(encoded_jpeg.getvalue())) as opened: + expected_jpeg_rgb = opened.convert("RGB") + assert expected_jpeg_rgb.size == (19, 11), "source unexpectedly transposed EXIF orientation" + cases.append(("jpeg-exif", expected_jpeg_rgb, encoded_jpeg.getvalue())) + for label in ("carrots", "corn"): encoded = (args.source / "inference/examples/images" / f"{label}.jpeg").read_bytes() with Image.open(io.BytesIO(encoded)) as opened: From a28abfd5b551b7c90847f541c1456c596e5fad4d Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:36:25 -0400 Subject: [PATCH 037/123] test(ds4v): capture decoder review regressions --- server/tools/ds4v_preprocess_probe/README.md | 2 + .../THIRD_PARTY_NOTICES.md | 67 +++++++++ .../ds4v_preprocess_probe.cpp | 140 ++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md diff --git a/server/tools/ds4v_preprocess_probe/README.md b/server/tools/ds4v_preprocess_probe/README.md index f7d869db1..2759ffb8b 100644 --- a/server/tools/ds4v_preprocess_probe/README.md +++ b/server/tools/ds4v_preprocess_probe/README.md @@ -24,3 +24,5 @@ The fixture check compares JPEG/PNG decoded RGB, including PNG alpha removal and The production boundary remains split: `decode_image` owns bounded JPEG/PNG byte decoding, while `preprocess_rgb` accepts already decoded interleaved RGB and has no image-codec or Python runtime dependency. Configure with `-DDS4V_PREPROCESS_WITH_CODECS=OFF` to build and test the RGB unit without downloading codec dependencies. The resampler follows Pillow 12.3.0 `src/libImaging/Resample.c`, including signed 22-bit fixed-point bicubic coefficients and the tall-image vertical-first path. + +See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for the Pillow-derived resampler notice and the notices for statically linked codec dependencies. diff --git a/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md b/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..8a4023798 --- /dev/null +++ b/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md @@ -0,0 +1,67 @@ +# Third-party notices + +## Pillow 12.3.0 + +The resize implementation follows Pillow 12.3.0 `src/libImaging/Resample.c`. + +The Python Imaging Library (PIL) is + + Copyright © 1997-2011 by Secret Labs AB + Copyright © 1995-2011 by Fredrik Lundh and contributors + +Pillow is the friendly PIL fork. It is + + Copyright © 2010 by Jeffrey 'Alex' Clark and contributors + +Like PIL, Pillow is licensed under the open source MIT-CMU License: + +By obtaining, using, and/or copying this software and/or its associated +documentation, you agree that you have read, understood, and will comply +with the following terms and conditions: + +Permission to use, copy, modify and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appears in all copies, and that +both that copyright notice and this permission notice appear in supporting +documentation, and that the name of Secret Labs AB or the author not be +used in advertising or publicity pertaining to distribution of the software +without specific, written prior permission. + +SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. +IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, +INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + +## LodePNG + +Copyright (c) 2005-2018 Lode Vandevenne + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. + +## libjpeg-turbo 3.1.4.1 + +The probe statically links the libjpeg API library and disables the TurboJPEG API library. + +This software is based in part on the work of the Independent JPEG Group. + +The downloaded source archive retains its complete `LICENSE.md` and `README.ijg` notices. diff --git a/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp b/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp index b93d9ccc4..a428d5b85 100644 --- a/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp +++ b/server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp @@ -1,11 +1,15 @@ #include "deepseek4_vision_preprocess.h" #ifdef DS4V_PREPROCESS_WITH_CODECS #include "deepseek4_vision_decode.h" +#include +#include +#include #endif #include #include #include +#include #include #include #include @@ -273,6 +277,141 @@ void expect_error(PreprocessError expected, PreprocessError actual, const std::s ", expected " + dflash::vision::preprocess_error_name(expected)); } +#ifdef DS4V_PREPROCESS_WITH_CODECS +void append_u32(std::vector & output, std::uint32_t value) { + output.push_back(static_cast(value >> 24)); + output.push_back(static_cast(value >> 16)); + output.push_back(static_cast(value >> 8)); + output.push_back(static_cast(value)); +} + +void append_png_chunk( + std::vector & output, + const std::array & type, + const std::vector & data) { + append_u32(output, static_cast(data.size())); + const std::size_t crc_begin = output.size(); + output.insert(output.end(), type.begin(), type.end()); + output.insert(output.end(), data.begin(), data.end()); + append_u32(output, lodepng_crc32(output.data() + crc_begin, 4 + data.size())); +} + +std::vector excessive_idat_png() { + std::vector inflated(4096, 0); + unsigned char * compressed = nullptr; + std::size_t compressed_size = 0; + const unsigned error = lodepng_zlib_compress( + &compressed, + &compressed_size, + inflated.data(), + inflated.size(), + &lodepng_default_compress_settings); + require(error == 0, "cannot create excessive-IDAT regression PNG"); + + std::vector result = {137, 80, 78, 71, 13, 10, 26, 10}; + const std::vector ihdr = { + 0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0, + }; + append_png_chunk(result, {'I', 'H', 'D', 'R'}, ihdr); + const std::vector compressed_bytes( + compressed, compressed + compressed_size); + append_png_chunk(result, {'I', 'D', 'A', 'T'}, compressed_bytes); + append_png_chunk(result, {'I', 'E', 'N', 'D'}, {}); + std::free(compressed); + return result; +} + +std::vector grey16_png() { + constexpr std::array values = { + 0, 1, 254, 255, 256, 257, 1024, 65535, + }; + std::vector pixels; + pixels.reserve(values.size() * 2 * 2); + for (int row = 0; row < 2; ++row) { + for (const std::uint16_t value : values) { + pixels.push_back(static_cast(value >> 8)); + pixels.push_back(static_cast(value)); + } + } + unsigned char * encoded = nullptr; + std::size_t encoded_size = 0; + const unsigned error = lodepng_encode_memory( + &encoded, &encoded_size, pixels.data(), 8, 2, LCT_GREY, 16); + require(error == 0, "cannot create GREY16 regression PNG"); + std::vector result(encoded, encoded + encoded_size); + std::free(encoded); + return result; +} + +std::vector cmyk_jpeg() { + jpeg_compress_struct encoder{}; + jpeg_error_mgr error{}; + encoder.err = jpeg_std_error(&error); + jpeg_create_compress(&encoder); + unsigned char * encoded = nullptr; + unsigned long encoded_size = 0; + jpeg_mem_dest(&encoder, &encoded, &encoded_size); + encoder.image_width = 2; + encoder.image_height = 1; + encoder.input_components = 4; + encoder.in_color_space = JCS_CMYK; + jpeg_set_defaults(&encoder); + jpeg_start_compress(&encoder, TRUE); + std::array pixels = {0, 64, 128, 16, 255, 192, 128, 32}; + JSAMPROW row = pixels.data(); + require(jpeg_write_scanlines(&encoder, &row, 1) == 1, + "cannot create CMYK regression JPEG"); + jpeg_finish_compress(&encoder); + std::vector result(encoded, encoded + encoded_size); + jpeg_destroy_compress(&encoder); + std::free(encoded); + return result; +} + +void decoder_regression_tests() { + std::vector failures; + const auto excessive = excessive_idat_png(); + const auto excessive_result = + dflash::vision::decode_image({excessive.data(), excessive.size()}); + if (excessive_result.status.code != DecodeError::MalformedImage || + excessive_result.status.message.find("IDAT exceeds decoded geometry bound") == + std::string::npos) { + failures.emplace_back("excess IDAT did not report the bounded-inflate outcome"); + } + + const auto grey = grey16_png(); + const auto grey_result = dflash::vision::decode_image({grey.data(), grey.size()}); + constexpr std::array expected_values = { + 0, 1, 254, 255, 255, 255, 255, 255, + }; + std::vector expected_rgb; + expected_rgb.reserve(expected_values.size() * 2 * 3); + for (int row = 0; row < 2; ++row) { + for (const std::uint8_t value : expected_values) { + expected_rgb.insert(expected_rgb.end(), 3, value); + } + } + if (!grey_result || grey_result.image.pixels != expected_rgb) { + failures.emplace_back("GREY16 did not match Pillow I;16 to RGB clamping"); + } + + const auto cmyk = cmyk_jpeg(); + const auto cmyk_result = dflash::vision::decode_image({cmyk.data(), cmyk.size()}); + if (cmyk_result.status.code != DecodeError::UnsupportedFormat) { + failures.emplace_back("CMYK JPEG was not classified as unsupported_format"); + } + + if (!failures.empty()) { + std::ostringstream message; + message << "decoder regression failures:"; + for (const auto & failure : failures) { + message << "\n- " << failure; + } + throw std::runtime_error(message.str()); + } +} +#endif + void self_test() { PreprocessConfig bad_config; bad_config.patch_size = 16; @@ -346,6 +485,7 @@ void self_test() { {&byte, encoded_limit.max_encoded_bytes + 1}, encoded_limit).status.code == DecodeError::EncodedTooLarge, "oversized encoded input was inspected"); + decoder_regression_tests(); #endif std::cout << "self-test PASS\n"; } From c24e82373eb5ab7aa0904fabe556921f9fdf52e0 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:38:57 -0400 Subject: [PATCH 038/123] test(ds4v): specify transactional image prompt preparation --- .../src/deepseek4/deepseek4_image_prompt.cpp | 10 +++ server/src/deepseek4/deepseek4_image_prompt.h | 44 +++++++++ server/tools/ds4v_image_prompt/CMakeLists.txt | 14 +++ server/tools/ds4v_image_prompt/test.cpp | 89 +++++++++++++++++++ 4 files changed, 157 insertions(+) create mode 100644 server/src/deepseek4/deepseek4_image_prompt.cpp create mode 100644 server/src/deepseek4/deepseek4_image_prompt.h create mode 100644 server/tools/ds4v_image_prompt/CMakeLists.txt create mode 100644 server/tools/ds4v_image_prompt/test.cpp diff --git a/server/src/deepseek4/deepseek4_image_prompt.cpp b/server/src/deepseek4/deepseek4_image_prompt.cpp new file mode 100644 index 000000000..8217da5f4 --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_prompt.cpp @@ -0,0 +1,10 @@ +#include "deepseek4_image_prompt.h" +namespace dflash::vision { +PreparedImagePrompt prepare_image_prompt(const std::vector &, + const std::vector &,const ImagePromptLimits &,const ImageTokenizerContract &) { + PreparedImagePrompt result; + result.error=ImagePromptError::InvalidContract; + result.message="not implemented"; + return result; +} +} diff --git a/server/src/deepseek4/deepseek4_image_prompt.h b/server/src/deepseek4/deepseek4_image_prompt.h new file mode 100644 index 000000000..881731e1d --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_prompt.h @@ -0,0 +1,44 @@ +#pragma once +#include "deepseek4_vision_preprocess.h" + +namespace dflash::vision { + +struct ImagePatchInput { + ResizePlan plan; + std::vector patches_bf16; +}; +struct PromptImage { + ImagePatchInput input; + ImageLayout layout; +}; +struct ImageTokenizerContract { + std::uint32_t vocabulary = 129280; + std::int32_t marker = 129264; +}; +struct ImagePromptLimits { + std::uint64_t context_capacity = 131072; + std::uint64_t output_reserve = 4096; + std::uint64_t max_expanded_tokens = 131072; +}; +enum class ImagePromptError { + None, InvalidContract, InvalidLimits, InvalidToken, ImageCount, + MarkerCount, InvalidPlan, InvalidPatches, TokenLimit, ContextOverflow, +}; +struct PreparedImagePrompt { + ImagePromptError error = ImagePromptError::None; + std::string message; + std::vector tokens; + std::vector images; + explicit operator bool() const { return error == ImagePromptError::None; } +}; + +// Consumes final rendered token IDs and already-preprocessed image inputs. +// Success owns independent copies of patches and layouts. Failure owns no +// partial tokens/images. No decoding, model execution, or cache policy occurs. +PreparedImagePrompt prepare_image_prompt( + const std::vector & rendered_tokens, + const std::vector & images, + const ImagePromptLimits & limits = {}, + const ImageTokenizerContract & tokenizer = {}); + +} // namespace dflash::vision diff --git a/server/tools/ds4v_image_prompt/CMakeLists.txt b/server/tools/ds4v_image_prompt/CMakeLists.txt new file mode 100644 index 000000000..16edca944 --- /dev/null +++ b/server/tools/ds4v_image_prompt/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.20) +project(ds4v_image_prompt LANGUAGES CXX) +add_library(ds4v_image_prompt STATIC + ../../src/deepseek4/deepseek4_image_prompt.cpp + ../../src/deepseek4/deepseek4_vision_preprocess.cpp) +target_include_directories(ds4v_image_prompt PUBLIC ../../src/deepseek4) +target_compile_features(ds4v_image_prompt PUBLIC cxx_std_17) +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(ds4v_image_prompt PRIVATE -Wall -Wextra -Wpedantic -Werror) +endif() +add_executable(test_ds4v_image_prompt test.cpp) +target_link_libraries(test_ds4v_image_prompt PRIVATE ds4v_image_prompt) +enable_testing() +add_test(NAME ds4v_image_prompt COMMAND test_ds4v_image_prompt) diff --git a/server/tools/ds4v_image_prompt/test.cpp b/server/tools/ds4v_image_prompt/test.cpp new file mode 100644 index 000000000..6b2b97f30 --- /dev/null +++ b/server/tools/ds4v_image_prompt/test.cpp @@ -0,0 +1,89 @@ +#include "deepseek4_image_prompt.h" +#include +#include +#include + +using namespace dflash::vision; +constexpr int32_t marker=129264,vocab=129280; +static void check(bool value,const char * why) { if (!value) throw std::runtime_error(why); } +static ImagePatchInput small_image(uint16_t pixel=0) { + ImagePatchInput image; + image.plan={42,42,3,3,1,1,false}; + image.patches_bf16.assign(9*588,pixel); + return image; +} +static void rejected(const PreparedImagePrompt & result,ImagePromptError expected) { + check(!result && result.error==expected,"wrong failure category"); + check(result.tokens.empty() && result.images.empty() && !result.message.empty(),"nontransactional failure"); +} +int main() { + try { + auto image=small_image(); + auto text=prepare_image_prompt({1,2,42},{ }); + check(bool(text) && text.tokens==std::vector({1,2,42}) && text.images.empty(),"text path"); + const std::vector body={ImageTokenType::Start,ImageTokenType::Image, + ImageTokenType::Pad,ImageTokenType::Newline,ImageTokenType::Pad,ImageTokenType::End}; + for (uint64_t start=0;start<4;++start) { + std::vector input(start,42); input.push_back(marker); input.push_back(7); + auto result=prepare_image_prompt(input,{image}); + check(bool(result) && result.images.size()==1,"single image"); + const auto & layout=result.images[0].layout; + std::vector expected(3-start,ImageTokenType::Pad); + expected.insert(expected.end(),body.begin(),body.end()); + check(layout.types==expected && layout.permutation==std::vector({0}),"source grounded tiny layout"); + check(layout.span.block_begin==start && layout.span.visible_begin==3 && + layout.span.visible_end==9 && layout.span.block_end==9,"span residues"); + check(result.tokens.size()==10 && result.tokens.back()==7,"expanded count/text tail"); + for (size_t i=0;i(expected[i]),"generated IDs"); + } + auto pair=prepare_image_prompt({marker,marker},{image,small_image(0x3f80)}); + check(bool(pair) && pair.tokens.size()==17 && pair.images[1].layout.span.block_begin==9 && + pair.images[1].layout.span.visible_begin==11 && pair.images[1].layout.span.block_end==17,"consecutive expanded positions"); + auto spaced=prepare_image_prompt({marker,55,marker,66},{image,image}); + check(bool(spaced) && spaced.images[1].layout.span.block_begin==10 && spaced.tokens[9]==55 && + spaced.tokens.back()==66,"text between images"); + auto copy=pair; + pair.images[0].input.patches_bf16[0]=0x3f80; + check(copy.images[0].input.patches_bf16[0]==0,"copy owns patches"); + auto moved=std::move(copy); + check(moved.images[1].input.patches_bf16[0]==0x3f80 && moved.tokens.size()==17,"move lifetime"); + auto independent=prepare_image_prompt({marker},{image}); + image.patches_bf16[0]=0xbf80; + check(independent.images[0].input.patches_bf16[0]==0,"input lifetime independence"); + rejected(prepare_image_prompt({marker},{}),ImagePromptError::MarkerCount); + rejected(prepare_image_prompt({1},{image}),ImagePromptError::MarkerCount); + rejected(prepare_image_prompt({marker,marker},{image}),ImagePromptError::MarkerCount); + rejected(prepare_image_prompt({-1},{}),ImagePromptError::InvalidToken); + for (int kind=0;kind<5;++kind) rejected(prepare_image_prompt({vocab+kind},{}),ImagePromptError::InvalidToken); + rejected(prepare_image_prompt({marker},{image},{},{129279,marker}),ImagePromptError::InvalidContract); + rejected(prepare_image_prompt({marker},{image},{},{vocab,marker-1}),ImagePromptError::InvalidContract); + rejected(prepare_image_prompt(std::vector(5,marker),std::vector(5,image)),ImagePromptError::ImageCount); + auto bad=image; bad.plan.resized_width++; + rejected(prepare_image_prompt({marker,marker},{image,bad}),ImagePromptError::InvalidPlan); + bad=image; bad.plan.aligner_rows=2; + rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPlan); + bad=image; bad.plan.vit_rows=0; + rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPlan); + bad=image; bad.plan={1008,672,48,72,16,24,false}; + rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPlan); + bad=image; bad.patches_bf16.pop_back(); + rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPatches); + for (uint16_t word : {uint16_t(0x3f81),uint16_t(0xbf81),uint16_t(0x7f80),uint16_t(0x7fc1)}) { + bad=image; bad.patches_bf16[0]=word; + rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPatches); + } + check(bool(prepare_image_prompt({marker},{image},{10,1,9})),"exact context fit"); + rejected(prepare_image_prompt({marker},{image},{9,1,9}),ImagePromptError::ContextOverflow); + rejected(prepare_image_prompt({marker},{image},{10,1,8}),ImagePromptError::TokenLimit); + rejected(prepare_image_prompt({1},{},{0,0,1}),ImagePromptError::InvalidLimits); + rejected(prepare_image_prompt({1},{},{1,2,1}),ImagePromptError::InvalidLimits); + rejected(prepare_image_prompt({1},{},{2147483648ULL,0,1}),ImagePromptError::InvalidLimits); + rejected(prepare_image_prompt({1},{},{2147483647,0,2147483647}),ImagePromptError::InvalidLimits); + rejected(prepare_image_prompt({1},{},{2147483647,2147483647,1}),ImagePromptError::ContextOverflow); + check(bool(prepare_image_prompt({1},{},{2147483647,2147483646,1})),"wide exact arithmetic"); + rejected(prepare_image_prompt({1},{},{1,std::numeric_limits::max(),1}),ImagePromptError::InvalidLimits); + std::cout<<"PASS text, cardinality, source layout residues, ordering, ownership, negative contracts, checked context\n"; + return 0; + } catch (const std::exception & e) { std::cerr<<"FAIL: "< Date: Fri, 4 Sep 2026 19:39:09 -0400 Subject: [PATCH 039/123] fix(ds4v): bound PNG inflate and match source decoding --- .../src/deepseek4/deepseek4_vision_decode.cpp | 141 +++++++++++++++++- server/tools/ds4v_preprocess_probe/README.md | 2 + 2 files changed, 139 insertions(+), 4 deletions(-) diff --git a/server/src/deepseek4/deepseek4_vision_decode.cpp b/server/src/deepseek4/deepseek4_vision_decode.cpp index 1ba55fcd1..b16920a9d 100644 --- a/server/src/deepseek4/deepseek4_vision_decode.cpp +++ b/server/src/deepseek4/deepseek4_vision_decode.cpp @@ -4,6 +4,8 @@ #include #include +#include +#include #include #include #include @@ -47,6 +49,82 @@ DecodeStatus validate_decoded( return {}; } +bool checked_add_size(std::size_t left, std::size_t right, std::size_t & result) { + if (right > std::numeric_limits::max() - left) { + return false; + } + result = left + right; + return true; +} + +bool checked_mul_size(std::size_t left, std::size_t right, std::size_t & result) { + if (left != 0 && right > std::numeric_limits::max() / left) { + return false; + } + result = left * right; + return true; +} + +bool png_filtered_size( + std::uint32_t width, + std::uint32_t height, + std::uint32_t bits_per_pixel, + std::size_t & result) { + std::size_t whole_bytes = 0; + if (!checked_mul_size(width / 8, bits_per_pixel, whole_bytes)) { + return false; + } + const std::size_t partial_bytes = + ((width & 7U) * static_cast(bits_per_pixel) + 7) / 8; + std::size_t row_bytes = 0; + if (!checked_add_size(whole_bytes, partial_bytes, row_bytes) || + !checked_add_size(row_bytes, 1, row_bytes)) { + return false; + } + return checked_mul_size(row_bytes, height, result); +} + +bool png_idat_bound( + std::uint32_t width, + std::uint32_t height, + const LodePNGColorMode & color, + std::uint32_t interlace_method, + std::size_t & result) { + const std::uint32_t bits_per_pixel = lodepng_get_bpp(&color); + if (bits_per_pixel == 0) { + return false; + } + if (interlace_method == 0) { + return png_filtered_size(width, height, bits_per_pixel, result); + } + if (interlace_method != 1) { + return false; + } + + constexpr std::array x_start = {0, 4, 0, 2, 0, 1, 0}; + constexpr std::array y_start = {0, 0, 4, 0, 2, 0, 1}; + constexpr std::array x_step = {8, 8, 4, 4, 2, 2, 1}; + constexpr std::array y_step = {8, 8, 8, 4, 4, 2, 2}; + result = 0; + for (std::size_t pass = 0; pass < x_start.size(); ++pass) { + const std::uint32_t pass_width = width <= x_start[pass] + ? 0 + : (width - x_start[pass] + x_step[pass] - 1) / x_step[pass]; + const std::uint32_t pass_height = height <= y_start[pass] + ? 0 + : (height - y_start[pass] + y_step[pass] - 1) / y_step[pass]; + if (pass_width == 0 || pass_height == 0) { + continue; + } + std::size_t pass_size = 0; + if (!png_filtered_size(pass_width, pass_height, bits_per_pixel, pass_size) || + !checked_add_size(result, pass_size, result)) { + return false; + } + } + return result != 0; +} + struct JpegErrorManager { jpeg_error_mgr base; std::jmp_buf jump; @@ -95,6 +173,11 @@ DecodeResult decode_jpeg(const EncodedImageView & encoded, const DecodeLimits & jpeg_destroy_decompress(&decoder); return fail(DecodeError::MalformedImage, "JPEG header is incomplete"); } + if (decoder.jpeg_color_space == JCS_CMYK || decoder.jpeg_color_space == JCS_YCCK) { + jpeg_destroy_decompress(&decoder); + return fail(DecodeError::UnsupportedFormat, + "CMYK and YCCK JPEG images are not supported"); + } decoder.out_color_space = JCS_RGB; jpeg_calc_output_dimensions(&decoder); if (decoder.output_width > std::numeric_limits::max() || @@ -158,29 +241,65 @@ DecodeResult decode_jpeg(const EncodedImageView & encoded, const DecodeLimits & DecodeResult decode_png(const EncodedImageView & encoded, const DecodeLimits & limits) { LodePNGState state; lodepng_state_init(&state); + state.decoder.ignore_crc = 0; + state.decoder.ignore_critical = 0; + state.decoder.ignore_end = 0; + state.decoder.zlibsettings.ignore_adler32 = 0; + state.decoder.zlibsettings.ignore_nlen = 0; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + state.decoder.read_text_chunks = 0; + state.decoder.remember_unknown_chunks = 0; + state.decoder.max_icc_size = 16ULL * 1024ULL * 1024ULL; +#endif unsigned width = 0; unsigned height = 0; const unsigned inspect_error = lodepng_inspect(&width, &height, &state, encoded.data, encoded.size); - lodepng_state_cleanup(&state); if (inspect_error != 0) { + lodepng_state_cleanup(&state); return fail(DecodeError::MalformedImage, std::string("malformed PNG: ") + lodepng_error_text(inspect_error)); } std::size_t output_bytes = 0; if (const auto status = validate_decoded(width, height, limits, output_bytes); !status) { + lodepng_state_cleanup(&state); DecodeResult result; result.status = status; return result; } + std::size_t idat_bound = 0; + if (!png_idat_bound( + width, + height, + state.info_png.color, + state.info_png.interlace_method, + idat_bound)) { + lodepng_state_cleanup(&state); + return fail(DecodeError::MalformedImage, "PNG filtered scanline size is invalid"); + } + state.decoder.zlibsettings.max_output_size = idat_bound; + + const bool grey16 = + state.info_png.color.colortype == LCT_GREY && state.info_png.color.bitdepth == 16; + state.info_raw.colortype = grey16 ? LCT_GREY : LCT_RGB; + state.info_raw.bitdepth = grey16 ? 16 : 8; + state.decoder.color_convert = grey16 ? 0 : 1; unsigned char * output = nullptr; unsigned decoded_width = 0; unsigned decoded_height = 0; - const unsigned decode_error = lodepng_decode24( - &output, &decoded_width, &decoded_height, encoded.data, encoded.size); + const unsigned decode_error = lodepng_decode( + &output, &decoded_width, &decoded_height, &state, encoded.data, encoded.size); + lodepng_state_cleanup(&state); if (decode_error != 0) { std::free(output); + if (decode_error == 109) { + return fail(DecodeError::MalformedImage, + "PNG IDAT exceeds decoded geometry bound"); + } + if (decode_error == 83) { + return fail(DecodeError::AllocationFailed, "cannot allocate PNG decode buffer"); + } return fail(DecodeError::MalformedImage, std::string("malformed PNG: ") + lodepng_error_text(decode_error)); } @@ -193,7 +312,21 @@ DecodeResult decode_png(const EncodedImageView & encoded, const DecodeLimits & l result.image.width = width; result.image.height = height; try { - result.image.pixels.assign(output, output + output_bytes); + if (grey16) { + const std::size_t sample_count = static_cast(width) * height; + result.image.pixels.resize(output_bytes); + for (std::size_t sample = 0; sample < sample_count; ++sample) { + const std::uint16_t value = + static_cast(output[sample * 2]) << 8 | + output[sample * 2 + 1]; + const auto channel = static_cast(std::min(value, 255)); + result.image.pixels[sample * 3] = channel; + result.image.pixels[sample * 3 + 1] = channel; + result.image.pixels[sample * 3 + 2] = channel; + } + } else { + result.image.pixels.assign(output, output + output_bytes); + } } catch (...) { std::free(output); return fail(DecodeError::AllocationFailed, "cannot own decoded PNG RGB buffer"); diff --git a/server/tools/ds4v_preprocess_probe/README.md b/server/tools/ds4v_preprocess_probe/README.md index 2759ffb8b..abb7e3ec1 100644 --- a/server/tools/ds4v_preprocess_probe/README.md +++ b/server/tools/ds4v_preprocess_probe/README.md @@ -23,6 +23,8 @@ The fixture check compares JPEG/PNG decoded RGB, including PNG alpha removal and The production boundary remains split: `decode_image` owns bounded JPEG/PNG byte decoding, while `preprocess_rgb` accepts already decoded interleaved RGB and has no image-codec or Python runtime dependency. Configure with `-DDS4V_PREPROCESS_WITH_CODECS=OFF` to build and test the RGB unit without downloading codec dependencies. +PNG decoding sets the IDAT inflation limit to the checked filtered scanline size derived from the validated IHDR color depth and interlace layout. LodePNG may retain capacity up to roughly 1.5 times that limit plus one 65,535-byte uncompressed DEFLATE block before reporting the limit. Text and unknown-chunk storage stay disabled; CRC, Adler-32, and DEFLATE length checks stay enabled; ICC decompression retains LodePNG's 16 MiB cap. GREY16 samples follow Pillow's `I;16` RGB conversion by clamping values above 255. CMYK and YCCK JPEG inputs return `unsupported_format`. + The resampler follows Pillow 12.3.0 `src/libImaging/Resample.c`, including signed 22-bit fixed-point bicubic coefficients and the tall-image vertical-first path. See [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for the Pillow-derived resampler notice and the notices for statically linked codec dependencies. From a2489a460d6d6229e46caa62f30d4178d16cc435 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:41:48 -0400 Subject: [PATCH 040/123] feat(ds4v): prepare owned image prompts with checked expansion --- .../src/deepseek4/deepseek4_image_prompt.cpp | 97 ++++++++++++++++++- server/src/deepseek4/deepseek4_image_prompt.h | 3 +- server/tools/ds4v_image_prompt/CMakeLists.txt | 2 + server/tools/ds4v_image_prompt/fixtures.cpp | 73 ++++++++++++++ .../ds4v_image_prompt/verify_fixtures.py | 29 ++++++ 5 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 server/tools/ds4v_image_prompt/fixtures.cpp create mode 100644 server/tools/ds4v_image_prompt/verify_fixtures.py diff --git a/server/src/deepseek4/deepseek4_image_prompt.cpp b/server/src/deepseek4/deepseek4_image_prompt.cpp index 8217da5f4..82ac1d95b 100644 --- a/server/src/deepseek4/deepseek4_image_prompt.cpp +++ b/server/src/deepseek4/deepseek4_image_prompt.cpp @@ -1,10 +1,99 @@ #include "deepseek4_image_prompt.h" +#include +#include +#include + namespace dflash::vision { -PreparedImagePrompt prepare_image_prompt(const std::vector &, - const std::vector &,const ImagePromptLimits &,const ImageTokenizerContract &) { +namespace { +PreparedImagePrompt fail(ImagePromptError error,const std::string & message) { PreparedImagePrompt result; - result.error=ImagePromptError::InvalidContract; - result.message="not implemented"; + result.error=error; + result.message=message; return result; } } +PreparedImagePrompt prepare_image_prompt(const std::vector & tokens, + const std::vector & images,const ImagePromptLimits & limits, + const ImageTokenizerContract & tokenizer) { + if (tokenizer.vocabulary!=129280 || tokenizer.marker!=129264) + return fail(ImagePromptError::InvalidContract,"unsupported image tokenizer contract"); + if (limits.context_capacity==0 || limits.context_capacity>std::numeric_limits::max() || + limits.output_reserve>limits.context_capacity || limits.max_expanded_tokens==0 || + limits.max_expanded_tokens>MAX_PREPARED_PROMPT_TOKENS) + return fail(ImagePromptError::InvalidLimits,"invalid context, output reserve, or prompt bound"); + if (images.size()>4) return fail(ImagePromptError::ImageCount,"at most four images are supported"); + if (tokens.size()>limits.max_expanded_tokens) + return fail(ImagePromptError::TokenLimit,"rendered tokens exceed prompt bound"); + size_t markers=0; + for (int32_t token:tokens) { + if (token<0 || static_cast(token)>=tokenizer.vocabulary) + return fail(ImagePromptError::InvalidToken,"rendered token outside text vocabulary"); + if (token==tokenizer.marker) ++markers; + } + if (markers!=images.size()) return fail(ImagePromptError::MarkerCount,"final image marker count differs from image count"); + + try { + for (size_t i=0;i0x3f80U) + return fail(ImagePromptError::InvalidPatches,where+"patch value outside finite normalized range"); + } + } + std::vector layouts; + layouts.reserve(images.size()); + uint64_t position=0; + for (int32_t token:tokens) { + uint64_t added=1; + if (token==tokenizer.marker) { + const auto & plan=images[layouts.size()].plan; + ImageLayout layout; + if (!build_image_layout(plan.aligner_rows,plan.aligner_cols,position,layout)) + return fail(ImagePromptError::InvalidPlan,"image layout could not be built"); + added=layout.types.size(); + layouts.push_back(std::move(layout)); + } + if (added>limits.max_expanded_tokens-position) + return fail(ImagePromptError::TokenLimit,"expanded tokens exceed prompt bound"); + position+=added; + } + if (position>limits.context_capacity-limits.output_reserve) + return fail(ImagePromptError::ContextOverflow,"expanded prompt and output reserve exceed context capacity"); + + PreparedImagePrompt result; + result.tokens.reserve(static_cast(position)); + result.images.reserve(images.size()); + size_t image_index=0; + for (int32_t token:tokens) { + if (token!=tokenizer.marker) { result.tokens.push_back(token); continue; } + auto & layout=layouts[image_index]; + for (ImageTokenType kind:layout.types) + result.tokens.push_back(static_cast(tokenizer.vocabulary)+static_cast(kind)); + result.images.push_back({images[image_index],std::move(layout)}); + ++image_index; + } + return result; + } catch (const std::bad_alloc &) { + return fail(ImagePromptError::AllocationFailed,"image prompt allocation failed"); + } +} +} diff --git a/server/src/deepseek4/deepseek4_image_prompt.h b/server/src/deepseek4/deepseek4_image_prompt.h index 881731e1d..7edb1ed9c 100644 --- a/server/src/deepseek4/deepseek4_image_prompt.h +++ b/server/src/deepseek4/deepseek4_image_prompt.h @@ -20,9 +20,10 @@ struct ImagePromptLimits { std::uint64_t output_reserve = 4096; std::uint64_t max_expanded_tokens = 131072; }; +constexpr std::uint64_t MAX_PREPARED_PROMPT_TOKENS = 1048576; enum class ImagePromptError { None, InvalidContract, InvalidLimits, InvalidToken, ImageCount, - MarkerCount, InvalidPlan, InvalidPatches, TokenLimit, ContextOverflow, + MarkerCount, InvalidPlan, InvalidPatches, TokenLimit, ContextOverflow, AllocationFailed, }; struct PreparedImagePrompt { ImagePromptError error = ImagePromptError::None; diff --git a/server/tools/ds4v_image_prompt/CMakeLists.txt b/server/tools/ds4v_image_prompt/CMakeLists.txt index 16edca944..abec9a621 100644 --- a/server/tools/ds4v_image_prompt/CMakeLists.txt +++ b/server/tools/ds4v_image_prompt/CMakeLists.txt @@ -10,5 +10,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") endif() add_executable(test_ds4v_image_prompt test.cpp) target_link_libraries(test_ds4v_image_prompt PRIVATE ds4v_image_prompt) +add_executable(ds4v_image_prompt_fixtures fixtures.cpp) +target_link_libraries(ds4v_image_prompt_fixtures PRIVATE ds4v_image_prompt) enable_testing() add_test(NAME ds4v_image_prompt COMMAND test_ds4v_image_prompt) diff --git a/server/tools/ds4v_image_prompt/fixtures.cpp b/server/tools/ds4v_image_prompt/fixtures.cpp new file mode 100644 index 000000000..5ecb1e7a2 --- /dev/null +++ b/server/tools/ds4v_image_prompt/fixtures.cpp @@ -0,0 +1,73 @@ +#include "deepseek4_image_prompt.h" +#include +#include +#include +#include +#include + +using namespace dflash::vision; +namespace fs=std::filesystem; +static void check(bool value,const char * why) { if (!value) throw std::runtime_error(why); } +template static std::vector read(const fs::path & path) { + std::ifstream stream(path,std::ios::binary|std::ios::ate); + check(bool(stream),"fixture open failed"); + const auto size=stream.tellg(); + check(size>=0 && size<16*1024*1024 && size%sizeof(T)==0,"fixture byte size invalid"); + std::vector values(static_cast(size)/sizeof(T)); + stream.seekg(0); stream.read(reinterpret_cast(values.data()),size); + check(bool(stream),"fixture read failed"); + return values; +} +static ImagePatchInput image(const fs::path & root,const std::string & label) { + ImagePatchInput value; + value.plan=label=="corn" ? ResizePlan{476,322,23,34,8,12,false} : ResizePlan{854,588,42,61,14,21,false}; + value.patches_bf16=read(root/label/"patches.bf16"); + return value; +} +static void compare(const PreparedImagePrompt & result,size_t index,const ImagePatchInput & input, + const fs::path & root,const std::string & label,uint64_t start,int fixture_start) { + const auto types=read(root/label/("types-"+std::to_string(fixture_start)+".i64")); + const auto permutation=read(root/label/("permutation-"+std::to_string(fixture_start)+".i64")); + check(bool(result) && result.images.size()>index,"fixture preparation failed"); + const auto & item=result.images[index]; + check(item.input.patches_bf16==input.patches_bf16,"patch bytes changed"); + check(item.layout.types.size()==types.size() && item.layout.permutation==permutation,"source layout size/permutation mismatch"); + for (size_t i=0;i(item.layout.types[i])==types[i],"source layout kind mismatch"); + check(result.tokens[start+i]==129280+types[i],"source expanded token mismatch"); + } + const auto first=std::find(types.begin(),types.end(),int64_t(ImageTokenType::Start)); + const auto last=std::find(types.begin(),types.end(),int64_t(ImageTokenType::End)); + check(first!=types.end() && last!=types.end(),"bad source sentinel fixture"); + check(item.layout.span.block_begin==start && item.layout.span.block_end==start+types.size() && + item.layout.span.visible_begin==start+static_cast(first-types.begin()) && + item.layout.span.visible_end==start+static_cast(last-types.begin())+1,"source absolute spans mismatch"); +} +int main(int argc,char ** argv) { + try { + check(argc==2,"usage: fixture_probe fixture_directory"); + const fs::path root=argv[1]; + for (const std::string label:{"corn","carrots"}) { + const auto source=image(root,label); + for (int start:{0,1,2,3,127}) { + std::vector tokens(start,42); + tokens.push_back(129264); tokens.push_back(77); + auto result=prepare_image_prompt(tokens,{source}); + compare(result,0,source,root,label,start,start); + check(result.tokens.back()==77,"text suffix changed"); + for (int i=0;i(root/first/"types-0.i64"); + const auto a=image(root,first),b=image(root,second); + auto result=prepare_image_prompt({129264,129264},{a,b}); + compare(result,0,a,root,first,0,0); + compare(result,1,b,root,second,first_types.size(),static_cast(first_types.size()%4)); + std::cout< Date: Fri, 4 Sep 2026 19:43:36 -0400 Subject: [PATCH 041/123] test(ds4v): prove prompt ownership and document CPU qualification --- server/tools/ds4v_image_prompt/README.md | 79 ++++++++++++++++++++++++ server/tools/ds4v_image_prompt/test.cpp | 8 +++ 2 files changed, 87 insertions(+) create mode 100644 server/tools/ds4v_image_prompt/README.md diff --git a/server/tools/ds4v_image_prompt/README.md b/server/tools/ds4v_image_prompt/README.md new file mode 100644 index 000000000..db9b4290c --- /dev/null +++ b/server/tools/ds4v_image_prompt/README.md @@ -0,0 +1,79 @@ +# Pure DS4V image prompt preparation + +`prepare_image_prompt` accepts final rendered/tokenized text and ordered +`ImagePatchInput` values containing an existing `ResizePlan` and raw BF16 patches. +It returns one owning value with expanded token IDs and `PromptImage` records, +each binding copied plan/patches to the existing core `ImageLayout`/`TokenSpan`. +There is no RGB retention, caller-supplied layout, second span representation, +model payload abstraction, decoder, tower, HTTP, backend, or cache integration. + +The operation enforces the fixed vocabulary 129280 and marker 129264 contract. +It checks final marker cardinality even with zero images, rejects negative or +external-vocabulary input tokens, and accepts at most four images. No-image, +no-marker requests keep their token vector unchanged and have no image records. +The caller remains responsible for obtaining these final tokens from the verified +native tokenizer and for transport schema checks before this operation. + +Plans must have consistent positive patch/aligner grids and resized dimensions. +The accepted core's layout builder at position zero verifies the complete source +image budget with all three possible leading pads reserved. Patch word count must +match the grid, and every raw BF16 value must be finite and in [-1,1]. This checks +the normalized tensor contract, not image provenance or whether each value came +from a particular byte-valued source pixel. The unit does not rerun resizing. + +Each final layout is then generated at the current expanded position, so prior +images affect later offsets and leading padding correctly. All five image kinds +become vocabulary+kind IDs. The core's whole-block bounds include leading pads; +its narrower visibility bounds start at IMAGE_START and include IMAGE_END. + +Limits accept context capacities through INT_MAX, a nonnegative output reserve +not exceeding that capacity, and a positive expanded-token limit capped at +1,048,576 (default 131,072). The hard ceiling bounds memory independently of a +misconfigured context limit. Size checks use uint64 and subtraction before token +allocation or narrowing. Exact context fit passes; compression grants no +exception. All inputs/layouts/counts are validated before the final result is +published. Errors carry bounded category/index text and no partial tokens or +image records. Default value copies own independent patch storage; move ownership +survives destruction of the original transport/input object. + +## soulf CPU verification + +The tests and a compiling stub were committed first as c24e823. The isolated +remote Release target built and CTest failed with `FAIL: text path`. Implementation +a2489a4 passed the same tests and original source fixtures. The final revision adds +explicit equal-layout separate-request isolation and additional bound cases. + +Run from the isolated `~/lucebox-ds4v-prompt` checkout: + +```sh +cmake -S server/tools/ds4v_image_prompt -B /tmp/ds4v-image-prompt-build -DCMAKE_BUILD_TYPE=Release +cmake --build /tmp/ds4v-image-prompt-build -j2 +ctest --test-dir /tmp/ds4v-image-prompt-build --output-on-failure +python3 server/tools/ds4v_image_prompt/verify_fixtures.py /tmp/ds4v-image-prompt-build/ds4v_image_prompt_fixtures ~/ds4v-work/ds4v-preprocess-fixtures-final artifacts/image-prompt/fixture-verdict.json +``` + +This source-only CMake target compiles the new helper and the accepted +`deepseek4_vision_preprocess.cpp` core. It has no FetchContent, codec, GGML, GPU, +or Python package dependency. The fixture wrapper uses Python's standard library +and checks all 28 saved carrots/corn fixture hashes before the native probe. +The source fixtures are reused read-only; no codec or unrelated RGB tests rerun. + +Tests cover every start residue; text before/between/after images; consecutive +images; all cardinality errors including zero-image marker injection; all five +forged external image IDs; malformed plans/patches; the source full-layout budget; +finite normalized BF16 bounds; image counts; exact-fit/context-overflow and +near-INT_MAX limits; transactional second-image failure; independent copies, +moves, and separate same-layout image storage. + +Ten original-image cases compare exact types, permutation, absolute spans, +expanded image IDs, unchanged text, and original BF16 patch bytes: carrots and +corn at starts 0, 1, 2, 3, 127. Both consecutive-image orders also pass, using +source fixture lengths to independently establish second starts 109 and 313. +No expected layout is calculated with the implementation under test. + +Evidence lives in `~/lucebox-ds4v-prompt/artifacts/image-prompt` with RED/GREEN +logs, fixture logs/verdict, exact final source commit and binary hashes. This is +**unintegrated CPU preparation proof**. The final integration owner must preserve +the result through requests/retries, prevent later token rewrites, add backend +materialization and execution guards, and implement image-safe usage/cache/status +handling. Native tower numerical qualification remains open. diff --git a/server/tools/ds4v_image_prompt/test.cpp b/server/tools/ds4v_image_prompt/test.cpp index 6b2b97f30..933f7618b 100644 --- a/server/tools/ds4v_image_prompt/test.cpp +++ b/server/tools/ds4v_image_prompt/test.cpp @@ -51,6 +51,11 @@ int main() { auto independent=prepare_image_prompt({marker},{image}); image.patches_bf16[0]=0xbf80; check(independent.images[0].input.patches_bf16[0]==0,"input lifetime independence"); + auto other_request=prepare_image_prompt({marker},{small_image(0x3f80)}); + check(independent.tokens==other_request.tokens,"equal layout fixture"); + independent.images[0].input.patches_bf16[0]=0xbf80; + check(other_request.images[0].input.patches_bf16[0]==0x3f80,"distinct requests own image storage"); + check(bool(prepare_image_prompt(std::vector(4,marker),std::vector(4,image))),"four-image boundary"); rejected(prepare_image_prompt({marker},{}),ImagePromptError::MarkerCount); rejected(prepare_image_prompt({1},{image}),ImagePromptError::MarkerCount); rejected(prepare_image_prompt({marker,marker},{image}),ImagePromptError::MarkerCount); @@ -65,6 +70,8 @@ int main() { rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPlan); bad=image; bad.plan.vit_rows=0; rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPlan); + bad=image; bad.plan.vit_rows=std::numeric_limits::max(); + rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPlan); bad=image; bad.plan={1008,672,48,72,16,24,false}; rejected(prepare_image_prompt({marker},{bad}),ImagePromptError::InvalidPlan); bad=image; bad.patches_bf16.pop_back(); @@ -78,6 +85,7 @@ int main() { rejected(prepare_image_prompt({marker},{image},{10,1,8}),ImagePromptError::TokenLimit); rejected(prepare_image_prompt({1},{},{0,0,1}),ImagePromptError::InvalidLimits); rejected(prepare_image_prompt({1},{},{1,2,1}),ImagePromptError::InvalidLimits); + rejected(prepare_image_prompt({1},{},{1,0,0}),ImagePromptError::InvalidLimits); rejected(prepare_image_prompt({1},{},{2147483648ULL,0,1}),ImagePromptError::InvalidLimits); rejected(prepare_image_prompt({1},{},{2147483647,0,2147483647}),ImagePromptError::InvalidLimits); rejected(prepare_image_prompt({1},{},{2147483647,2147483647,1}),ImagePromptError::ContextOverflow); From edb3b0e15d3e73b5408d8fbb13532fe40fc9fefb Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:44:15 -0400 Subject: [PATCH 042/123] docs(ds4v): correct LodePNG notice --- server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md b/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md index 8a4023798..33bf7ed2d 100644 --- a/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md +++ b/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md @@ -37,7 +37,9 @@ PERFORMANCE OF THIS SOFTWARE. ## LodePNG -Copyright (c) 2005-2018 Lode Vandevenne +LodePNG version 20260119 + +Copyright (c) 2005-2026 Lode Vandevenne This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages From 1096fabad3bdcd86650ac1f0b02320f150e6514f Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:53:53 -0400 Subject: [PATCH 043/123] test(ds4v): specify CPU image preparation composition --- .../tools/ds4v_image_prepare/CMakeLists.txt | 46 ++++++ server/tools/ds4v_image_prepare/compose.cpp | 7 + server/tools/ds4v_image_prepare/compose.h | 17 +++ server/tools/ds4v_image_prepare/test.cpp | 134 ++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 server/tools/ds4v_image_prepare/CMakeLists.txt create mode 100644 server/tools/ds4v_image_prepare/compose.cpp create mode 100644 server/tools/ds4v_image_prepare/compose.h create mode 100644 server/tools/ds4v_image_prepare/test.cpp diff --git a/server/tools/ds4v_image_prepare/CMakeLists.txt b/server/tools/ds4v_image_prepare/CMakeLists.txt new file mode 100644 index 000000000..5a4efebb0 --- /dev/null +++ b/server/tools/ds4v_image_prepare/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4v_image_prepare LANGUAGES C CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +foreach(backend CPU CUDA HIP METAL VULKAN SYCL OPENCL CANN MUSA RPC BLAS WEBGPU HEXAGON ZENDNN) + set(GGML_${backend} OFF CACHE BOOL "" FORCE) +endforeach() +set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +add_subdirectory(../../deps/llama.cpp/ggml ggml EXCLUDE_FROM_ALL) + +find_package(nlohmann_json CONFIG QUIET) +if(NOT nlohmann_json_FOUND) + include(FetchContent) + FetchContent_Declare(json + URL https://codeload.github.com/nlohmann/json/tar.gz/9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 + URL_HASH SHA256=0dbc5e40a01ff142e7e68c03e85247a4dcede2f592d12d3677dee3664d17975a + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + FetchContent_MakeAvailable(json) +endif() + +# Reuse the accepted target's pinned codec downloads and two-job build rule. +set(DS4V_PREPROCESS_WITH_CODECS ON CACHE BOOL "" FORCE) +add_subdirectory(../ds4v_preprocess_probe codecs EXCLUDE_FROM_ALL) +set(DS4V_JINJA ../../deps/llama.cpp/common/jinja) +add_executable(ds4v_image_prepare + test.cpp compose.cpp + ../../src/server/image_input.cpp + ../../src/server/chat_template.cpp + ../../src/server/tokenizer.cpp + ../../src/deepseek4/deepseek4_vision_decode.cpp + ../../src/deepseek4/deepseek4_vision_preprocess.cpp + ../../src/deepseek4/deepseek4_image_prompt.cpp + ${DS4V_JINJA}/lexer.cpp ${DS4V_JINJA}/parser.cpp + ${DS4V_JINJA}/runtime.cpp ${DS4V_JINJA}/value.cpp + ${DS4V_JINJA}/string.cpp ${DS4V_JINJA}/caps.cpp) +target_include_directories(ds4v_image_prepare PRIVATE ../../src ../../deps/llama.cpp/common) +target_link_libraries(ds4v_image_prepare PRIVATE ggml-base nlohmann_json::nlohmann_json ds4v_libjpeg ds4v_lodepng) +enable_testing() +set(DS4V_TOKENIZER_GGUF "" CACHE FILEPATH "Converter smoke GGUF; metadata only") +set(DS4V_SOURCE_FIXTURES "" CACHE PATH "Immutable preprocessing fixtures") +if(DS4V_TOKENIZER_GGUF AND DS4V_SOURCE_FIXTURES) + add_test(NAME ds4v_image_prepare_composition COMMAND ds4v_image_prepare ${DS4V_TOKENIZER_GGUF} ${DS4V_SOURCE_FIXTURES}) +endif() diff --git a/server/tools/ds4v_image_prepare/compose.cpp b/server/tools/ds4v_image_prepare/compose.cpp new file mode 100644 index 000000000..fb8694e4f --- /dev/null +++ b/server/tools/ds4v_image_prepare/compose.cpp @@ -0,0 +1,7 @@ +#include "compose.h" +Composition compose(const nlohmann::json &,dflash::common::Tokenizer &, + const dflash::vision::ImagePromptLimits &,const std::string &,const std::string &) { + Composition result; + result.error="not implemented"; + return result; +} diff --git a/server/tools/ds4v_image_prepare/compose.h b/server/tools/ds4v_image_prepare/compose.h new file mode 100644 index 000000000..5fbfa3d44 --- /dev/null +++ b/server/tools/ds4v_image_prepare/compose.h @@ -0,0 +1,17 @@ +#pragma once +#include "deepseek4/deepseek4_image_prompt.h" +#include "deepseek4/deepseek4_vision_decode.h" +#include "server/image_input.h" +#include "server/tokenizer.h" + +// Qualification-only adapter. This is not HttpServer normalization or serving. +struct Composition { + std::string error; + dflash::vision::PreparedImagePrompt prepared; + std::vector decoded; + std::vector rendered_tokens; + explicit operator bool() const { return error.empty(); } +}; +Composition compose(const nlohmann::json & messages, dflash::common::Tokenizer & tokenizer, + const dflash::vision::ImagePromptLimits & limits = {}, + const std::string & tools = "", const std::string & jinja = ""); diff --git a/server/tools/ds4v_image_prepare/test.cpp b/server/tools/ds4v_image_prepare/test.cpp new file mode 100644 index 000000000..074f69d38 --- /dev/null +++ b/server/tools/ds4v_image_prepare/test.cpp @@ -0,0 +1,134 @@ +#include "compose.h" +#include "server/chat_template.h" +#include "lodepng.h" +#include +#include +#include +#include +#include +#include + +using namespace dflash::common; +using namespace dflash::vision; +using json=nlohmann::json; +namespace fs=std::filesystem; +static void check(bool ok,const char * why) { if (!ok) throw std::runtime_error(why); } +template static std::vector read(const fs::path & path) { + std::ifstream file(path,std::ios::binary|std::ios::ate); + check(bool(file),"fixture open failed"); + auto bytes=file.tellg(); + check(bytes>=0 && bytes<32*1024*1024 && bytes%sizeof(T)==0,"fixture size invalid"); + std::vector value(static_cast(bytes)/sizeof(T)); + file.seekg(0); file.read(reinterpret_cast(value.data()),bytes); + check(bool(file),"fixture read failed"); return value; +} +static std::string data_url(const std::vector & bytes,const char * mime="image/jpeg") { + constexpr char chars[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + std::string result=std::string("data:")+mime+";base64,"; + for (size_t i=0;i>18)&63]; result+=chars[(word>>12)&63]; + result+=i+1>6)&63]:'='; + result+=i+2 & urls) { + json parts=json::array({{{"type","text"},{"text","Describe these: "}}}); + for (const auto & url:urls) { + parts.push_back({{"type","image_url"},{"image_url",{{"url",url}}}}); + parts.push_back({{"type","text"},{"text"," then "}}); + } + parts.push_back({{"type","text"},{"text","Explain the difference."}}); + return json::array({{{"role","system"},{"content","Be concise."}},{{"role","user"},{"content",parts}}}); +} +static void compare(const Composition & result,size_t index,const fs::path & root,const std::string & label) { + const auto & item=result.prepared.images[index]; + check(result.decoded[index].pixels==read(root/label/"input.rgb"),"decoded RGB differs from source"); + check(item.input.patches_bf16==read(root/label/"patches.bf16"),"BF16 patches differ from source"); + const auto start=item.layout.span.block_begin; + const int residue=static_cast(start%4); + const auto types=read(root/label/("types-"+std::to_string(residue)+".i64")); + check(item.layout.types.size()==types.size(),"type count mismatch"); + check(item.layout.permutation==read(root/label/("permutation-"+std::to_string(residue)+".i64")),"permutation mismatch"); + for (size_t i=0;i(item.layout.types[i])==types[i],"source type mismatch"); + check(result.prepared.tokens[start+i]==129280+types[i],"generated ID mismatch"); + } + const auto first=std::find(types.begin(),types.end(),int64_t(0)); + const auto last=std::find(types.begin(),types.end(),int64_t(4)); + check(first!=types.end() && last!=types.end(),"source sentinels absent"); + check(item.layout.span.visible_begin==start+static_cast(first-types.begin()) && + item.layout.span.visible_end==start+static_cast(last-types.begin())+1 && + item.layout.span.block_end==start+types.size(),"source span mismatch"); +} +static void failure(const Composition & result,const char * category) { + check(!result && result.error.find(category)!=std::string::npos,"wrong failure category"); + check(result.prepared.tokens.empty() && result.prepared.images.empty() && result.decoded.empty(),"partial composition escaped"); +} +static std::string solid_png(uint8_t pixel) { + std::vector rgb(19*11*3,pixel); + unsigned char * bytes=nullptr; size_t size=0; + check(lodepng_encode24(&bytes,&size,rgb.data(),19,11)==0,"synthetic PNG encode failed"); + std::vector data(bytes,bytes+size); std::free(bytes); + return data_url(data,"image/png"); +} +int main(int argc,char ** argv) { + try { + check(argc==3,"usage: composition_probe tokenizer_gguf fixtures"); + Tokenizer tokenizer; check(tokenizer.load_from_gguf(argv[1]),"tokenizer load failed"); + check(tokenizer.vocab_size()==129280 && tokenizer.token_to_id(DS4_IMAGE_PLACEHOLDER)==129264,"tokenizer contract"); + const auto text_messages=json::array({{{"role","user"},{"content","Hello."}}}); + auto text=compose(text_messages,tokenizer); + check(bool(text),"text composition failed"); + const auto expected=tokenizer.encode(render_chat_template({{"user","Hello."}},ChatFormat::DEEPSEEK4)); + check(text.prepared.tokens==expected && text.prepared.images.empty(),"text tokens changed"); + const fs::path root=argv[2]; + const auto corn=data_url(read(root/"corn/encoded.bin")); + const auto carrots=data_url(read(root/"carrots/encoded.bin")); + for (bool reverse:{false,true}) { + const std::string first=reverse?"carrots":"corn",second=reverse?"corn":"carrots"; + const auto input=messages(reverse?std::vector{carrots,corn}:std::vector{corn,carrots}); + auto result=compose(input,tokenizer); + check(bool(result) && result.prepared.images.size()==2,"real image composition failed"); + compare(result,0,root,first); compare(result,1,root,second); + size_t position=0,index=0; + for (int32_t token:result.rendered_tokens) { + if (token==129264) { + check(result.prepared.images[index].layout.span.block_begin==position,"ordered expanded image position"); + position+=result.prepared.images[index++].layout.types.size(); + } else check(result.prepared.tokens[position++]==token,"surrounding text changed"); + } + check(position==result.prepared.tokens.size() && index==2,"expanded coverage"); + const uint64_t total=result.prepared.tokens.size(); + check(bool(compose(input,tokenizer,{total+1,1,total})),"exact fit rejected"); + failure(compose(input,tokenizer,{total,1,total}),"prompt"); + std::cout<(root/"corn/encoded.bin"); malformed.resize(3); + failure(compose(messages({corn,data_url(malformed)}),tokenizer),"decode image 1"); + json redacted=messages({corn,carrots}); redact_image_urls(redacted); + const auto logged=redacted.dump(); + check(logged.find("base64,")==std::string::npos && logged.find(corn.substr(0,80))==std::string::npos && + logged.find("[image omitted]")!=std::string::npos,"redaction before dump failed"); + auto black=compose(messages({solid_png(0)}),tokenizer); + auto white=compose(messages({solid_png(255)}),tokenizer); + check(bool(black) && bool(white) && black.prepared.tokens==white.prepared.tokens,"same-layout PNG tokens"); + check(black.prepared.images[0].input.patches_bf16!=white.prepared.images[0].input.patches_bf16,"different pixels collapsed"); + const auto white_first=white.prepared.images[0].input.patches_bf16[0]; + black.prepared.images[0].input.patches_bf16[0]=0; + check(white.prepared.images[0].input.patches_bf16[0]==white_first,"image storage shared"); + std::cout<<"PASS text preservation, Jinja/cardinality, zero-image tool key, malformed second image, exact context, redaction, distinct PNG storage\n"; + return 0; + } catch (const std::exception & e) { std::cerr<<"FAIL: "< Date: Fri, 4 Sep 2026 19:55:23 -0400 Subject: [PATCH 044/123] test(ds4v): link existing Jinja unicode helper in composition probe --- server/tools/ds4v_image_prepare/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/tools/ds4v_image_prepare/CMakeLists.txt b/server/tools/ds4v_image_prepare/CMakeLists.txt index 5a4efebb0..1440580c9 100644 --- a/server/tools/ds4v_image_prepare/CMakeLists.txt +++ b/server/tools/ds4v_image_prepare/CMakeLists.txt @@ -35,7 +35,8 @@ add_executable(ds4v_image_prepare ../../src/deepseek4/deepseek4_image_prompt.cpp ${DS4V_JINJA}/lexer.cpp ${DS4V_JINJA}/parser.cpp ${DS4V_JINJA}/runtime.cpp ${DS4V_JINJA}/value.cpp - ${DS4V_JINJA}/string.cpp ${DS4V_JINJA}/caps.cpp) + ${DS4V_JINJA}/string.cpp ${DS4V_JINJA}/caps.cpp + ../../deps/llama.cpp/common/unicode.cpp) target_include_directories(ds4v_image_prepare PRIVATE ../../src ../../deps/llama.cpp/common) target_link_libraries(ds4v_image_prepare PRIVATE ggml-base nlohmann_json::nlohmann_json ds4v_libjpeg ds4v_lodepng) enable_testing() From 15a10c50a06680b037baa55e88e29aec89c63945 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:56:48 -0400 Subject: [PATCH 045/123] feat(ds4v): compose accepted image preparation in CPU probe --- server/tools/ds4v_image_prepare/compose.cpp | 66 ++++++++++++++++++++- server/tools/ds4v_image_prepare/verify.py | 36 +++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 server/tools/ds4v_image_prepare/verify.py diff --git a/server/tools/ds4v_image_prepare/compose.cpp b/server/tools/ds4v_image_prepare/compose.cpp index fb8694e4f..064af1eb5 100644 --- a/server/tools/ds4v_image_prepare/compose.cpp +++ b/server/tools/ds4v_image_prepare/compose.cpp @@ -1,7 +1,67 @@ #include "compose.h" -Composition compose(const nlohmann::json &,dflash::common::Tokenizer &, - const dflash::vision::ImagePromptLimits &,const std::string &,const std::string &) { +#include "server/chat_template.h" +#include +#include + +using namespace dflash::common; +using namespace dflash::vision; +namespace { +Composition fail(const std::string & error) { Composition result; - result.error="not implemented"; + result.error=error; return result; } +std::vector probe_text_adapter(const nlohmann::json & normalized) { + std::vector result; + for (const auto & message:normalized) { + ChatMessage item; + item.role=message.value("role",std::string("user")); + const auto & content=message.at("content"); + if (content.is_string()) item.content=content.get(); + else if (content.is_array()) { + for (const auto & part:content) { + const auto type=part.value("type",std::string()); + if (type!="text" && type!="input_text" && type!="output_text") + throw std::runtime_error("unsupported probe part"); + item.content+=part.at("text").get(); + } + } else throw std::runtime_error("unsupported probe content"); + result.push_back(std::move(item)); + } + return result; +} +} +Composition compose(const nlohmann::json & messages,Tokenizer & tokenizer, + const ImagePromptLimits & limits,const std::string & tools,const std::string & jinja) { + try { + nlohmann::json normalized; + std::vector encoded; + std::string error; + if (!extract_chat_images(messages,normalized,encoded,error)) return fail("transport: "+error); + const auto chat=probe_text_adapter(normalized); + const auto rendered=jinja.empty() + ? render_chat_template(chat,ChatFormat::DEEPSEEK4,true,false,tools) + : render_chat_template_jinja(jinja,chat,"","",true,false,tools); + auto tokens=tokenizer.encode(rendered); + if (std::count(tokens.begin(),tokens.end(),129264)!=static_cast(encoded.size())) + return fail("cardinality: final image marker count differs from image count"); + Composition result; + std::vector patches; + for (size_t i=0;i(tokenizer.vocab_size()),tokenizer.token_to_id(DS4_IMAGE_PLACEHOLDER)}); + if (!prepared) return fail("prompt: "+prepared.message); + result.prepared=std::move(prepared); + result.rendered_tokens=std::move(tokens); + return result; + } catch (const std::exception &) { + return fail("probe adapter or rendering failure"); + } +} diff --git a/server/tools/ds4v_image_prepare/verify.py b/server/tools/ds4v_image_prepare/verify.py new file mode 100644 index 000000000..8694c9617 --- /dev/null +++ b/server/tools/ds4v_image_prepare/verify.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Hash source fixtures before executing the CPU-only composition probe.""" +import argparse +import hashlib +import json +from pathlib import Path +import subprocess + +p=argparse.ArgumentParser() +p.add_argument('binary',type=Path) +p.add_argument('tokenizer_gguf',type=Path) +p.add_argument('fixtures',type=Path) +p.add_argument('verdict',type=Path) +a=p.parse_args() + +def digest(path): + h=hashlib.sha256() + with path.open('rb') as f: + for block in iter(lambda:f.read(1024*1024),b''): + h.update(block) + return h.hexdigest() + +expected=json.loads((a.fixtures/'sha256.json').read_text()) +verified={} +for name,sha in expected.items(): + if name.split('/')[0] in ('corn','carrots'): + assert digest(a.fixtures/name)==sha,name + verified[name]=sha +assert len(verified)==28 +run=subprocess.run([str(a.binary),str(a.tokenizer_gguf),str(a.fixtures)],capture_output=True,text=True) +print(run.stdout,end='') +print(run.stderr,end='') +assert run.returncode==0,run.returncode +a.verdict.write_text(json.dumps({'verdict':'PASS','source_fixture_hashes':verified, + 'binary_sha256':digest(a.binary),'tokenizer_smoke_gguf_sha256':digest(a.tokenizer_gguf), + 'scope':'CPU transport/decode/preprocess/render/tokenize/prompt composition with explicit probe adapter; no HTTP, backend, or tower'},indent=2)+'\n') From 0065158e5784722630bc4dbda98c6e4c228145ee Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 19:59:11 -0400 Subject: [PATCH 046/123] test(ds4v): freeze bounded CPU preparation composition proof --- .../tools/ds4v_image_prepare/CMakeLists.txt | 2 +- server/tools/ds4v_image_prepare/README.md | 83 +++++++++++++++++++ server/tools/ds4v_image_prepare/test.cpp | 11 +++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 server/tools/ds4v_image_prepare/README.md diff --git a/server/tools/ds4v_image_prepare/CMakeLists.txt b/server/tools/ds4v_image_prepare/CMakeLists.txt index 1440580c9..bdae0c65a 100644 --- a/server/tools/ds4v_image_prepare/CMakeLists.txt +++ b/server/tools/ds4v_image_prepare/CMakeLists.txt @@ -3,7 +3,7 @@ project(ds4v_image_prepare LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) -foreach(backend CPU CUDA HIP METAL VULKAN SYCL OPENCL CANN MUSA RPC BLAS WEBGPU HEXAGON ZENDNN) +foreach(backend CPU CUDA HIP METAL VULKAN SYCL OPENCL CANN MUSA RPC BLAS WEBGPU HEXAGON ZENDNN ZDNN OPENVINO VIRTGPU VIRTGPU_BACKEND ACCELERATE OPENMP) set(GGML_${backend} OFF CACHE BOOL "" FORCE) endforeach() set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE) diff --git a/server/tools/ds4v_image_prepare/README.md b/server/tools/ds4v_image_prepare/README.md new file mode 100644 index 000000000..3a3bb6ce0 --- /dev/null +++ b/server/tools/ds4v_image_prepare/README.md @@ -0,0 +1,83 @@ +# CPU image preparation composition probe + +This qualification-only probe composes accepted data-URL extraction, JPEG/PNG +decode, source RGB preprocessing, the existing built-in DeepSeek4 renderer and +native tokenizer, and owned prompt expansion. It also executes the existing +Jinja renderer for marker-preserving, dropped-marker, and repeated-marker +controls. The tokenizer loads metadata from the converter smoke GGUF; no model +weights, GGML backend, HTTP server, or vision tower are initialized. + +`compose.cpp` contains an explicit tiny adapter from normalized text parts to +ChatMessage. It supports only the simple roles/string/text-part fixtures here. +It is **not HttpServer::normalize_chat_messages**, and does not implement its +ToolMemory replay, Responses or Anthropic conversion, request copies, queueing, +compression, cache, snapshot, usage serialization, or generation behavior. +Production component implementations and APIs are unchanged. + +The adapter counts markers after real rendering/tokenization, including zero-image +requests, before decoding. It constructs its result transactionally. Transport, +decode, preprocess, and expansion failures discard accumulated records and return +bounded category/index messages. Decoded RGB is retained only for this probe's +byte comparisons; the production prompt payload still contains plan/patches/layout. + +## Tests and limits of proof + +- Real source carrots/corn JPEG data URLs in both orders, with text before, + between, and after them. Decoded RGB, BF16 patches, shapes, layout kinds, + permutation, generated IDs, and absolute spans match immutable source fixtures. + All ordinary rendered tokens remain unchanged around expanded blocks. +- Both orders expand to 436 tokens in the fixture's built-in DS4 prompt. The first + block starts at 9; the second starts at 119 for corn then carrots, and 323 for + carrots then corn. These positions arise from actual rendering/tokenization. +- Exact expanded context fit succeeds and one-token overflow fails, using the + image-specific preparation helper. Existing text compression admission is not + executed or qualified here. +- Jinja preserves an image marker successfully, while dropped/repeated markers + fail. A Jinja-injected marker and a tool-schema object key containing the marker + fail with zero extracted images. This tests the final token boundary that an + earlier string-value scan can miss. +- A valid first JPEG followed by a truncated second JPEG fails transactionally. + Redaction occurs before JSON serialization and removes all data URLs. +- Two separately encoded 19x11 solid PNG requests have identical expanded IDs + but different patch bytes and independent mutable storage. They are synthetic + test inputs generated in memory with the pinned LodePNG encoder, not saved + user artifacts. +- Ordinary text follows the real built-in renderer/tokenizer unchanged. + +The fixture wrapper verifies all 28 original carrots/corn fixture hashes before +execution. Logs contain counts, shapes, bounds and hashes, never encoded images +or full prompt data. No source fixture or Python environment is modified. + +The initial test/stub commit was 1096fab. Linking the existing Jinja engine also +requires its existing common/unicode.cpp; the harness wiring correction a5aa8a3 +then built and recorded the intended RED `text composition failed`. The minimal +adapter at 15a10c5 passed the unchanged interaction cases. Final checks also +assert/report source dimensions and explicitly disable additional GGML providers. + +## Reproduce on soulf only + +From isolated `~/lucebox-ds4v-cpu`: + +```sh +cmake -S server/tools/ds4v_image_prepare -B /tmp/ds4v-image-prepare-build -DCMAKE_BUILD_TYPE=Release -DDS4V_TOKENIZER_GGUF=$HOME/lucebox-ds4v-mix-fix/artifacts/fitter-fix/smoke.gguf -DDS4V_SOURCE_FIXTURES=$HOME/ds4v-work/ds4v-preprocess-fixtures-final +cmake --build /tmp/ds4v-image-prepare-build --target ds4v_image_prepare -j2 +python3 server/tools/ds4v_image_prepare/verify.py /tmp/ds4v-image-prepare-build/ds4v_image_prepare ~/lucebox-ds4v-mix-fix/artifacts/fitter-fix/smoke.gguf ~/ds4v-work/ds4v-preprocess-fixtures-final artifacts/cpu-composition/verdict.json +``` + +The wrapper is the authoritative hash-checking invocation. CTest is also +registered as `ds4v_image_prepare_composition` when both fixture paths are set. +Builds use two jobs; the native composition execution is single-threaded. Only +ggml-base/gguf is linked, with compute backends disabled. The accepted preprocessing +CMake target supplies the pinned libjpeg-turbo and LodePNG dependencies and its +two-job external build. JSON fallback uses the root server's pinned 9cca280a +archive with its SHA256. No server root build is configured. + +Retain [the existing preprocessing third-party notices](../ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md) +for Pillow/libjpeg-turbo/LodePNG, and the vendored llama.cpp license. This target +reuses those implementations and pins rather than copying codec code. + +Evidence: `~/lucebox-ds4v-cpu/artifacts/cpu-composition` contains RED/GREEN logs, +final source commit, source/binary hashes, dependency/backend configuration, and +fixture verdict. PASS means **CPU preparation composition with the explicit probe +adapter**, not integrated HTTP image input, image routing/attention execution, +vision semantics, tower parity, or paired-GPU performance. diff --git a/server/tools/ds4v_image_prepare/test.cpp b/server/tools/ds4v_image_prepare/test.cpp index 074f69d38..1c33a895b 100644 --- a/server/tools/ds4v_image_prepare/test.cpp +++ b/server/tools/ds4v_image_prepare/test.cpp @@ -46,6 +46,12 @@ static void compare(const Composition & result,size_t index,const fs::path & roo const auto & item=result.prepared.images[index]; check(result.decoded[index].pixels==read(root/label/"input.rgb"),"decoded RGB differs from source"); check(item.input.patches_bf16==read(root/label/"patches.bf16"),"BF16 patches differ from source"); + check(result.decoded[index].width==(label=="corn"?450U:1024U) && + result.decoded[index].height==(label=="corn"?308U:701U),"source decoded dimensions"); + check(item.input.plan.vit_rows==(label=="corn"?23U:42U) && + item.input.plan.vit_cols==(label=="corn"?34U:61U) && + item.input.plan.aligner_rows==(label=="corn"?8U:14U) && + item.input.plan.aligner_cols==(label=="corn"?12U:21U),"source patch/aligner dimensions"); const auto start=item.layout.span.block_begin; const int residue=static_cast(start%4); const auto types=read(root/label/("types-"+std::to_string(residue)+".i64")); @@ -61,6 +67,11 @@ static void compare(const Composition & result,size_t index,const fs::path & roo check(item.layout.span.visible_begin==start+static_cast(first-types.begin()) && item.layout.span.visible_end==start+static_cast(last-types.begin())+1 && item.layout.span.block_end==start+types.size(),"source span mismatch"); + std::cout< Date: Fri, 4 Sep 2026 21:34:37 -0400 Subject: [PATCH 047/123] test(ds4v): isolate biased linear BF16 boundary regression --- server/src/deepseek4/deepseek4_vision.cpp | 11 +- server/src/deepseek4/deepseek4_vision.h | 1 + server/tools/ds4v_vision/CMakeLists.txt | 27 +++- server/tools/ds4v_vision/linear_rounding.cpp | 132 +++++++++++++++++++ 4 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 server/tools/ds4v_vision/linear_rounding.cpp diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 0873b2930..fdf06cffb 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -108,6 +108,12 @@ std::vector read(Tensor * t) { } } namespace detail { +Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias) { + auto y=ggml_mul_mat(c,weight,input); + ggml_mul_mat_set_prec(y,GGML_PREC_F32); + if(bias) y=ggml_add(c,y,ggml_cast(c,bias,GGML_TYPE_F32)); + return rounded(c,y); +} void rotary_tables(PatchGrid grid,std::vector & cosine,std::vector & sine) { const int n=grid.height*grid.width; cosine.resize(size_t(n)*32); sine.resize(size_t(n)*32); @@ -164,10 +170,7 @@ struct VisionRuntime::Impl { } Tensor * weight(const std::string & name) { return ggml_get_tensor(weights,name.c_str()); } Tensor * linear(ggml_context * c,Tensor * x,const std::string & name,bool bias=true) { - auto y=ggml_mul_mat(c,weight(name+".weight"),x); - ggml_mul_mat_set_prec(y,GGML_PREC_F32); - if(bias) y=ggml_add(c,y,ggml_cast(c,weight(name+".bias"),GGML_TYPE_F32)); - return rounded(c,y); + return detail::linear(c,weight(name+".weight"),x,bias ? weight(name+".bias") : nullptr); } Tensor * norm(ggml_context * c,Tensor * x,const std::string & name) { return rounded(c,ggml_mul(c,ggml_rms_norm(c,x,config.rms_epsilon), diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index 82e7af9a3..0ef5bbcf7 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -53,6 +53,7 @@ class VisionRuntime { // The same geometry primitives used by the runtime and standalone unit tests. namespace detail { +ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias); void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine); ggml_tensor * rotate(ggml_context *, ggml_tensor *, ggml_tensor * cosine, ggml_tensor * sine); ggml_tensor * attention(ggml_context *, ggml_tensor * q, ggml_tensor * k, ggml_tensor * v); diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 4a1173889..5ae9acc4a 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -3,6 +3,7 @@ project(ds4v_vision LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) option(DS4V_VISION_HIP "Build the optional HIP qualification probe" OFF) +set(DS4V_VISION_PREBUILT_GGML "" CACHE PATH "Reuse an immutable GGML build for isolated runtime qualification") set(GGML_CUDA OFF CACHE BOOL "" FORCE) set(GGML_HIP ${DS4V_VISION_HIP} CACHE BOOL "" FORCE) set(GGML_METAL OFF CACHE BOOL "" FORCE) @@ -10,8 +11,27 @@ set(GGML_VULKAN OFF CACHE BOOL "" FORCE) set(GGML_BLAS OFF CACHE BOOL "" FORCE) set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -add_subdirectory(../../deps/llama.cpp/ggml ggml) -if(DS4V_VISION_HIP) +if(DS4V_VISION_PREBUILT_GGML) + add_library(ggml INTERFACE) + target_include_directories(ggml INTERFACE ../../deps/llama.cpp/ggml/include) + foreach(component base cpu) + set(library "${DS4V_VISION_PREBUILT_GGML}/ggml/src/libggml-${component}.so.0") + if(NOT EXISTS "${library}") + message(FATAL_ERROR "Missing prebuilt library: ${library}") + endif() + target_link_libraries(ggml INTERFACE "${library}") + endforeach() + if(DS4V_VISION_HIP) + set(library "${DS4V_VISION_PREBUILT_GGML}/ggml/src/ggml-hip/libggml-hip.so.0") + if(NOT EXISTS "${library}") + message(FATAL_ERROR "Missing prebuilt HIP library: ${library}") + endif() + target_link_libraries(ggml INTERFACE "${library}") + endif() +else() + add_subdirectory(../../deps/llama.cpp/ggml ggml) +endif() +if(DS4V_VISION_HIP AND NOT DS4V_VISION_PREBUILT_GGML) target_compile_definitions(ggml-hip PRIVATE cublasSgemmStridedBatched=hipblasSgemmStridedBatched cudaStreamCaptureStatus=hipStreamCaptureStatus @@ -26,8 +46,11 @@ target_include_directories(ds4v_vision PUBLIC ../../src) target_link_libraries(ds4v_vision PUBLIC ggml) add_executable(ds4v_vision_probe probe.cpp) target_link_libraries(ds4v_vision_probe PRIVATE ds4v_vision) +add_executable(ds4v_linear_rounding linear_rounding.cpp) +target_link_libraries(ds4v_linear_rounding PRIVATE ds4v_vision) if(DS4V_VISION_HIP) target_compile_definitions(ds4v_vision_probe PRIVATE DS4V_VISION_HIP) + target_compile_definitions(ds4v_linear_rounding PRIVATE DS4V_VISION_HIP) endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) diff --git a/server/tools/ds4v_vision/linear_rounding.cpp b/server/tools/ds4v_vision/linear_rounding.cpp new file mode 100644 index 000000000..10f6f1258 --- /dev/null +++ b/server/tools/ds4v_vision/linear_rounding.cpp @@ -0,0 +1,132 @@ +#include "deepseek4/deepseek4_vision.h" +#include "ggml.h" +#include "ggml-alloc.h" +#include "ggml-cpu.h" +#ifdef DS4V_VISION_HIP +#include "ggml-cuda.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +static void check(bool ok,const char * message) { if(!ok) throw std::runtime_error(message); } +static uint32_t bits(float value) { uint32_t b; std::memcpy(&b,&value,4); return b; } +static float from_bits(uint32_t b) { float value; std::memcpy(&value,&b,4); return value; } +// Independent nearest-even BF16 oracle. The fixture is finite and every F64 +// dot/sum is exactly representable in F32, so accumulation order cannot affect it. +static float round_bf16(double exact) { + float value=static_cast(exact); + check(double(value)==exact,"oracle value is not exactly representable in F32"); + uint32_t b=bits(value), high=b>>16, low=b&65535; + if(low>32768 || (low==32768 && (high&1))) ++high; + return from_bits(high<<16); +} +static std::vector pack(const std::vector & values) { + std::vector out; + for(float value:values) { check((bits(value)&65535)==0,"fixture input is not exact BF16"); out.push_back({uint16_t(bits(value)>>16)}); } + return out; +} +static void save(const std::filesystem::path & path,const std::vector & values) { + std::ofstream file(path,std::ios::binary); file.write(reinterpret_cast(values.data()),values.size()*4); + check(bool(file),"output write failed"); +} +static std::vector read(ggml_tensor * tensor) { + std::vector values(ggml_nelements(tensor)); ggml_backend_tensor_get(tensor,values.data(),0,values.size()*4); return values; +} +struct Graph { + ggml_context * context=ggml_init({1024*1024,nullptr,true}); + ggml_gallocr_t allocator; + explicit Graph(ggml_backend_t backend):allocator(ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend))) { + check(context && allocator,"graph creation failed"); + } + ~Graph() { ggml_gallocr_free(allocator); ggml_free(context); } +}; +int main(int argc,char ** argv) { + if(argc!=3) { std::cerr<<"usage: ds4v_linear_rounding cpu|hip:0 NEW_OUTPUT_DIR\n"; return 2; } + ggml_backend_t backend=nullptr; + const std::string device=argv[1]; + if(device=="cpu") { backend=ggml_backend_cpu_init(); if(backend) ggml_backend_cpu_set_n_threads(backend,2); } +#ifdef DS4V_VISION_HIP + else if(device=="hip:0" && ggml_backend_cuda_get_device_count()>0) backend=ggml_backend_cuda_init(0); +#endif + if(!backend) { std::cerr<<"requested backend unavailable\n"; return 1; } + int status=1; + try { + const std::filesystem::path directory=argv[2]; + check(!std::filesystem::exists(directory),"output directory already exists"); + std::filesystem::create_directories(directory); + std::cout<<"backend="<=256,"fixture fails to distinguish intermediate rounding"); + check(expected[m]==1.0078125f && premature[m]==1.f,"positive tie oracle changed"); + check(expected[m+1]==-1.0078125f && premature[m+1]==-1.f,"negative tie oracle changed"); + Graph owner(backend); auto c=owner.context; + auto w=ggml_new_tensor_2d(c,GGML_TYPE_BF16,k,m); + auto x=ggml_new_tensor_2d(c,GGML_TYPE_F32,k,n); + auto b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,m); + for(auto t:{w,x,b}) ggml_set_input(t); + auto raw_dot=ggml_mul_mat(c,w,x); ggml_mul_mat_set_prec(raw_dot,GGML_PREC_F32); + auto actual=dflash::vision::detail::linear(c,w,x,b); + auto unbiased=dflash::vision::detail::linear(c,w,x,nullptr); + auto graph=ggml_new_graph(c); + for(auto t:{raw_dot,actual,unbiased}) { ggml_set_output(t); ggml_build_forward_expand(graph,t); } + size_t required=0; ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&required); + check(required<16*1024*1024,"tiny graph scratch unexpectedly large"); + for(int i=0;i *>>{ + {"weights.f32",&weights},{"inputs.f32",&inputs},{"bias.f32",&bias},{"expected.f32",&expected}, + {"premature.f32",&premature},{"actual.f32",&values},{"raw-dot.f32",&dots},{"exact-dot.f32",&dot}, + {"unbiased.f32",&no_bias}}) save(directory/item.first,*item.second); + std::cout<<"shape="< Date: Fri, 4 Sep 2026 21:36:39 -0400 Subject: [PATCH 048/123] test(ds4v): allocate probe scratch after size preflight --- server/tools/ds4v_vision/linear_rounding.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/tools/ds4v_vision/linear_rounding.cpp b/server/tools/ds4v_vision/linear_rounding.cpp index 10f6f1258..5698609dd 100644 --- a/server/tools/ds4v_vision/linear_rounding.cpp +++ b/server/tools/ds4v_vision/linear_rounding.cpp @@ -101,6 +101,7 @@ int main(int argc,char ** argv) { size_t required=0; ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&required); check(required<16*1024*1024,"tiny graph scratch unexpectedly large"); for(int i=0;i Date: Fri, 4 Sep 2026 21:37:22 -0400 Subject: [PATCH 049/123] fix(ds4v): retain biased linear products until final BF16 round --- server/src/deepseek4/deepseek4_vision.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index fdf06cffb..1fd77a4fd 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -109,6 +109,10 @@ std::vector read(Tensor * t) { } namespace detail { Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias) { + // Source biased linears round after the bias. The HIP BF16 BLAS path can + // round its product to BF16 even with GGML_PREC_F32; an F32 weight operand + // preserves the product until the explicit final boundary below. + if(bias) weight=ggml_cast(c,weight,GGML_TYPE_F32); auto y=ggml_mul_mat(c,weight,input); ggml_mul_mat_set_prec(y,GGML_PREC_F32); if(bias) y=ggml_add(c,y,ggml_cast(c,bias,GGML_TYPE_F32)); From be8b0f1b07f1a3a034ce1d7333fd0d3402754c60 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 21:50:31 -0400 Subject: [PATCH 050/123] fix(ds4v): scope biased product workaround to GPU backends --- server/src/deepseek4/deepseek4_vision.cpp | 10 +++++++--- server/src/deepseek4/deepseek4_vision.h | 3 ++- server/tools/ds4v_vision/linear_rounding.cpp | 21 ++++++++++++++------ 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 1fd77a4fd..5372321f6 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -108,11 +108,12 @@ std::vector read(Tensor * t) { } } namespace detail { -Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias) { +Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias,bool preserve_biased_product) { // Source biased linears round after the bias. The HIP BF16 BLAS path can // round its product to BF16 even with GGML_PREC_F32; an F32 weight operand // preserves the product until the explicit final boundary below. - if(bias) weight=ggml_cast(c,weight,GGML_TYPE_F32); + // The CPU path already retains an F32 product; keep its existing reduction. + if(bias && preserve_biased_product) weight=ggml_cast(c,weight,GGML_TYPE_F32); auto y=ggml_mul_mat(c,weight,input); ggml_mul_mat_set_prec(y,GGML_PREC_F32); if(bias) y=ggml_add(c,y,ggml_cast(c,bias,GGML_TYPE_F32)); @@ -174,7 +175,10 @@ struct VisionRuntime::Impl { } Tensor * weight(const std::string & name) { return ggml_get_tensor(weights,name.c_str()); } Tensor * linear(ggml_context * c,Tensor * x,const std::string & name,bool bias=true) { - return detail::linear(c,weight(name+".weight"),x,bias ? weight(name+".bias") : nullptr); + const auto device=ggml_backend_get_device(backend); + const bool preserve=device && (ggml_backend_dev_type(device)==GGML_BACKEND_DEVICE_TYPE_GPU || + ggml_backend_dev_type(device)==GGML_BACKEND_DEVICE_TYPE_IGPU); + return detail::linear(c,weight(name+".weight"),x,bias ? weight(name+".bias") : nullptr,preserve); } Tensor * norm(ggml_context * c,Tensor * x,const std::string & name) { return rounded(c,ggml_mul(c,ggml_rms_norm(c,x,config.rms_epsilon), diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index 0ef5bbcf7..9f262dbb7 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -53,7 +53,8 @@ class VisionRuntime { // The same geometry primitives used by the runtime and standalone unit tests. namespace detail { -ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias); +ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias, + bool preserve_biased_product); void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine); ggml_tensor * rotate(ggml_context *, ggml_tensor *, ggml_tensor * cosine, ggml_tensor * sine); ggml_tensor * attention(ggml_context *, ggml_tensor * q, ggml_tensor * k, ggml_tensor * v); diff --git a/server/tools/ds4v_vision/linear_rounding.cpp b/server/tools/ds4v_vision/linear_rounding.cpp index 5698609dd..e9304d1a1 100644 --- a/server/tools/ds4v_vision/linear_rounding.cpp +++ b/server/tools/ds4v_vision/linear_rounding.cpp @@ -61,6 +61,10 @@ int main(int argc,char ** argv) { check(!std::filesystem::exists(directory),"output directory already exists"); std::filesystem::create_directories(directory); std::cout<<"backend="< *>>{ {"weights.f32",&weights},{"inputs.f32",&inputs},{"bias.f32",&bias},{"expected.f32",&expected}, @@ -123,9 +131,10 @@ int main(int argc,char ** argv) { std::cout<<"shape="< Date: Fri, 4 Sep 2026 23:00:00 -0400 Subject: [PATCH 051/123] test(ds4v): compare shared vision linears to frozen HIP source fixtures --- server/src/deepseek4/deepseek4_vision.cpp | 3 +- server/src/deepseek4/deepseek4_vision.h | 2 +- server/tools/ds4v_vision/CMakeLists.txt | 3 + server/tools/ds4v_vision/linear_source.cpp | 84 ++++++++++++++++++++++ 4 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 server/tools/ds4v_vision/linear_source.cpp diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 5372321f6..0549f1ade 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -108,7 +108,8 @@ std::vector read(Tensor * t) { } } namespace detail { -Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias,bool preserve_biased_product) { +Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias,bool preserve_biased_product,ggml_backend_t backend) { + (void)backend; // RED fixture probe: production arithmetic is unchanged. // Source biased linears round after the bias. The HIP BF16 BLAS path can // round its product to BF16 even with GGML_PREC_F32; an F32 weight operand // preserves the product until the explicit final boundary below. diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index 9f262dbb7..fb8de2ba8 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -54,7 +54,7 @@ class VisionRuntime { // The same geometry primitives used by the runtime and standalone unit tests. namespace detail { ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias, - bool preserve_biased_product); + bool preserve_biased_product, ggml_backend_t backend = nullptr); void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine); ggml_tensor * rotate(ggml_context *, ggml_tensor *, ggml_tensor * cosine, ggml_tensor * sine); ggml_tensor * attention(ggml_context *, ggml_tensor * q, ggml_tensor * k, ggml_tensor * v); diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 5ae9acc4a..371108a0e 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -46,11 +46,14 @@ target_include_directories(ds4v_vision PUBLIC ../../src) target_link_libraries(ds4v_vision PUBLIC ggml) add_executable(ds4v_vision_probe probe.cpp) target_link_libraries(ds4v_vision_probe PRIVATE ds4v_vision) +add_executable(ds4v_linear_source linear_source.cpp) +target_link_libraries(ds4v_linear_source PRIVATE ds4v_vision) add_executable(ds4v_linear_rounding linear_rounding.cpp) target_link_libraries(ds4v_linear_rounding PRIVATE ds4v_vision) if(DS4V_VISION_HIP) target_compile_definitions(ds4v_vision_probe PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_rounding PRIVATE DS4V_VISION_HIP) + target_compile_definitions(ds4v_linear_source PRIVATE DS4V_VISION_HIP) endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) diff --git a/server/tools/ds4v_vision/linear_source.cpp b/server/tools/ds4v_vision/linear_source.cpp new file mode 100644 index 000000000..71b248337 --- /dev/null +++ b/server/tools/ds4v_vision/linear_source.cpp @@ -0,0 +1,84 @@ +// Exact frozen original-source fixtures through the production vision helper. +#include "deepseek4/deepseek4_vision.h" +#include "ggml-alloc.h" +#include "ggml-cpu.h" +#ifdef DS4V_VISION_HIP +#include "ggml-cuda.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +static void check(bool b,const char *s) { if(!b) throw std::runtime_error(s); } +static uint32_t bits(float v) { uint32_t b; std::memcpy(&b,&v,4); return b; } +static std::vector load(const std::filesystem::path&p,size_t n) { + check(std::filesystem::file_size(p)==n*4,"fixture size mismatch"); + std::vector v(n); std::ifstream f(p,std::ios::binary); f.read((char*)v.data(),n*4); + check(bool(f),"fixture read failed"); + for(float x:v) check(std::isfinite(x) && !(bits(x)&65535),"fixture not exact finite BF16"); + return v; +} +static std::vector pack(const std::vector&v) { + std::vector r; for(float x:v) r.push_back({uint16_t(bits(x)>>16)}); return r; +} +struct Graph { + ggml_context *c=ggml_init({1024*1024,nullptr,true}); + ggml_gallocr_t a; + explicit Graph(ggml_backend_t b):a(ggml_gallocr_new(ggml_backend_get_default_buffer_type(b))) { check(c&&a,"graph creation failed"); } + ~Graph() { ggml_gallocr_free(a); ggml_free(c); } +}; +int main(int argc,char**argv) { + std::cout< actual(ref.size()),unbiased(ref.size()),expected(ref.size()); + ggml_backend_tensor_get(y,actual.data(),0,actual.size()*4); ggml_backend_tensor_get(u,unbiased.data(),0,unbiased.size()*4); ggml_backend_tensor_get(expected_u,expected.data(),0,expected.size()*4); + size_t bad=0,ubad=0; float maxabs=0; + for(size_t i=0;idata(),item.second->size()*4); check(bool(f),"output write failed"); } + std::cout<<"elements="< Date: Fri, 4 Sep 2026 23:06:05 -0400 Subject: [PATCH 052/123] fix(ds4v): use explicit HIP Lt fused BF16 bias operation --- server/deps/llama.cpp/VENDOR.md | 28 +++++++++ .../deps/llama.cpp/ggml/include/ggml-cuda.h | 7 +++ server/deps/llama.cpp/ggml/include/ggml.h | 8 +++ .../llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c | 3 + .../llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp | 1 + .../llama.cpp/ggml/src/ggml-cuda/common.cuh | 7 +++ .../llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu | 41 +++++++++++++ .../ggml/src/ggml-cuda/vision-bias.cu | 61 +++++++++++++++++++ .../ggml/src/ggml-cuda/vision-bias.cuh | 7 +++ .../ggml/src/ggml-hip/CMakeLists.txt | 3 +- server/deps/llama.cpp/ggml/src/ggml.c | 23 ++++++- server/src/deepseek4/deepseek4_vision.cpp | 33 ++++++++-- server/src/deepseek4/deepseek4_vision.h | 3 + server/tools/ds4v_vision/README.md | 27 ++++++++ server/tools/ds4v_vision/linear_source.cpp | 10 +++ 15 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh diff --git a/server/deps/llama.cpp/VENDOR.md b/server/deps/llama.cpp/VENDOR.md index ad185a2d9..c8fdacec7 100644 --- a/server/deps/llama.cpp/VENDOR.md +++ b/server/deps/llama.cpp/VENDOR.md @@ -27,3 +27,31 @@ the snapshot above for AMD heterogeneous MoE execution: These changes are limited to `ggml/`. Keep their public declarations in `ggml/include`, avoid DeepSeek-specific policy in generic kernels, and update this provenance when the patch set is moved to `lucebox-ggml`. + +## Hub-local DS4V HIP fused-bias linear + +The local `GGML_OP_MUL_MAT_BIAS_BF16` operation is appended after `PAGED_ATTN`; +all existing op numeric values are preserved, while `GGML_OP_COUNT` grows from +105 to 106. Rebuild ggml-base, CPU/HIP backends, and consumers together. Do not +mix old shared libraries with this header or serialize the new operation for +an older reader. This is an inference-only extension; CPU compute/backward +reject it, and HIP alone advertises the explicit registry capability. Other +backends are never selected by a generic unknown-op supports default. + +Only DS4V biased linears opt in. Ordinary text MUL_MAT/ADD fusion, unbiased +vision linears, and CPU/NVIDIA vision graph construction are unchanged. The +HIP-only CMake dependency is official hipBLASLt (`roc::hipblaslt`). The Lt +configuration follows the frozen PyTorch revision +`3d3aa833db84eed6b7f5595cb5f162c2f78300a4`: BF16 W/X/bias/output, F32 compute and +scalars, T/N, alpha=1, beta=0, bias epilogue, C=D, one first heuristic with a +76 MiB workspace maximum. There is no algorithm sweep or arithmetic fallback. + +One exact-size 76 MiB workspace and one Lt handle belong to each HIP backend +context that actually uses the operation. An event orders shared workspace +reuse on the actual execution stream; destruction waits for its last use. +The workspace is retained outside the ggml arena and is conservatively +included in VisionRuntime's scratch reservation/report even after arena +release. Graphs containing this operation are capture-ineligible; no global +text graph policy changes. Descriptors are per invocation, not cached. +Qualification is scoped to the Radeon RX 7900 XT and pinned ROCm/PyTorch +reference; availability on other HIP devices is not a qualification claim. diff --git a/server/deps/llama.cpp/ggml/include/ggml-cuda.h b/server/deps/llama.cpp/ggml/include/ggml-cuda.h index ec5643f44..8e52c72a3 100644 --- a/server/deps/llama.cpp/ggml/include/ggml-cuda.h +++ b/server/deps/llama.cpp/ggml/include/ggml-cuda.h @@ -24,6 +24,13 @@ extern "C" { // wider batches remain on the MMQ path. #define GGML_CUDA_DS4_MIX_MMV_MAX_TOKENS 5 +// HIP registry-only opt-in DS4V fused-bias capability (not NVIDIA/CUDA). +// Lookup "ggml_backend_hip_vision_bias_bf16_workspace" as size_t (*)(ggml_backend_t): +// nonzero means the explicit op is available, and returns its retained external +// workspace reservation (76 MiB). Unsupported op shapes must fail, not fallback. +// "ggml_backend_hip_vision_bias_bf16_launches" has the same signature and returns +// actual successful Lt submissions for focused dispatch verification. + // backend API GGML_BACKEND_API ggml_backend_t ggml_backend_cuda_init(int device); diff --git a/server/deps/llama.cpp/ggml/include/ggml.h b/server/deps/llama.cpp/ggml/include/ggml.h index 756193d7a..945869063 100644 --- a/server/deps/llama.cpp/ggml/include/ggml.h +++ b/server/deps/llama.cpp/ggml/include/ggml.h @@ -617,6 +617,8 @@ extern "C" { GGML_OP_PAGED_ATTN, + GGML_OP_MUL_MAT_BIAS_BF16, // explicit HIP-only DS4V fused bias + GGML_OP_COUNT, }; @@ -1455,6 +1457,12 @@ extern "C" { // A: k columns, n rows => [ne03, ne02, n, k] // B: k columns, m rows (i.e. we transpose it internally) => [ne03 * x, ne02 * y, m, k] // result is n columns, m rows => [ne03 * x, ne02 * y, m, n] + // Explicit inference-only BF16 W[k,m], X[k,n], bias[m] -> BF16 Y[m,n]. + // Contiguous 2D operands only; HIP Lt capability required, no fallback. + GGML_API struct ggml_tensor * ggml_mul_mat_bias_bf16( + struct ggml_context * ctx, struct ggml_tensor * weight, + struct ggml_tensor * input, struct ggml_tensor * bias); + GGML_API struct ggml_tensor * ggml_mul_mat( struct ggml_context * ctx, struct ggml_tensor * a, diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c index 6756c8383..e55f79892 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c @@ -2196,6 +2196,8 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm { GGML_ABORT("GGML_OP_FLASH_ATTN_SPARSE is only supported on the CUDA backend"); } + case GGML_OP_MUL_MAT_BIAS_BF16: + { GGML_ABORT("GGML_OP_MUL_MAT_BIAS_BF16 requires the HIP Lt backend"); } case GGML_OP_PAGED_ATTN: { GGML_ABORT("GGML_OP_PAGED_ATTN is only supported on the CUDA backend"); @@ -2591,6 +2593,7 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { case GGML_OP_FLASH_ATTN_EXT: case GGML_OP_FLASH_ATTN_SPARSE: case GGML_OP_PAGED_ATTN: + case GGML_OP_MUL_MAT_BIAS_BF16: case GGML_OP_FLASH_ATTN_BACK: case GGML_OP_SSM_CONV: case GGML_OP_SSM_SCAN: diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp index 423aebc0e..ebf0e1152 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp @@ -471,6 +471,7 @@ static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const st // The generic CPU kernel does not implement that contract. return !ggml_flash_attn_ext_is_ds4(op); case GGML_OP_PAGED_ATTN: + case GGML_OP_MUL_MAT_BIAS_BF16: return false; case GGML_OP_SSM_CONV: // Every nonzero mode is a dflash CUDA/HIP extension (SpecLA, diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh index 6b8230724..9bf53fca8 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh @@ -33,6 +33,7 @@ #if defined(GGML_USE_HIP) #include "vendors/hip.h" +#include #elif defined(GGML_USE_MUSA) #include "vendors/musa.h" #else @@ -1436,6 +1437,12 @@ struct ggml_backend_cuda_context { cudaStream_t streams[GGML_CUDA_MAX_DEVICES][GGML_CUDA_MAX_STREAMS] = { { nullptr } }; cublasHandle_t cublas_handles[GGML_CUDA_MAX_DEVICES] = {nullptr}; +#if defined(GGML_USE_HIP) + hipblasLtHandle_t vision_bias_handle = nullptr; + void * vision_bias_workspace = nullptr; // exactly 76 MiB, retained until context destruction + cudaEvent_t vision_bias_event = nullptr; + size_t vision_bias_launches = 0; +#endif int curr_stream_no = 0; bool low_priority_streams = false; int stream_priority = 0; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu index a1c178fde..0464d9d93 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu @@ -3,6 +3,7 @@ #include "ggml-backend-impl.h" #include "ggml-cuda/common.cuh" +#include "ggml-cuda/vision-bias.cuh" #include "ggml-cuda/acc.cuh" #include "ggml-cuda/add-id.cuh" #include "ggml-cuda/arange.cuh" @@ -766,6 +767,16 @@ ggml_backend_cuda_context::~ggml_backend_cuda_context() { luce_q8_memo.pop_back(); } +#if defined(GGML_USE_HIP) + if (vision_bias_workspace) { + ggml_cuda_set_device(device); + // The latest event follows every use of the shared workspace. + if (vision_bias_launches) CUDA_CHECK(cudaEventSynchronize(vision_bias_event)); + CUDA_CHECK(cudaFree(vision_bias_workspace)); + CUDA_CHECK(cudaEventDestroy(vision_bias_event)); + } + if (vision_bias_handle) CUBLAS_CHECK(hipblasLtDestroy(vision_bias_handle)); +#endif if (copy_event != nullptr) { CUDA_CHECK(cudaEventDestroy(copy_event)); } @@ -3610,6 +3621,13 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg case GGML_OP_FLASH_ATTN_SPARSE: ggml_cuda_flash_attn_sparse(ctx, dst); break; + case GGML_OP_MUL_MAT_BIAS_BF16: +#if defined(GGML_USE_HIP) + ggml_hip_vision_bias(ctx, dst); + break; +#else + return false; +#endif case GGML_OP_PAGED_ATTN: ggml_cuda_paged_attn(ctx, dst); break; @@ -3899,6 +3917,9 @@ static bool ggml_cuda_graph_check_compability(ggml_cgraph * cgraph) { for (int i = 0; i < cgraph->n_nodes; i++) { ggml_tensor * node = cgraph->nodes[i]; + // This explicit opt-in op uses a host heuristic and retained workspace + // event. Qualification is direct execution, never graph capture/replay. + if (node->op == GGML_OP_MUL_MAT_BIAS_BF16) return false; if (ggml_is_empty(node) || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE || node->op == GGML_OP_NONE) { continue; @@ -6447,6 +6468,12 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g return ggml_cuda_flash_attn_ext_supported(dev_ctx->device, op); case GGML_OP_FLASH_ATTN_SPARSE: return true; // Always supported on CUDA + case GGML_OP_MUL_MAT_BIAS_BF16: +#if defined(GGML_USE_HIP) + return ggml_hip_vision_bias_supported(op); +#else + return false; +#endif case GGML_OP_PAGED_ATTN: return ggml_cuda_paged_attn_supported(op); case GGML_OP_CROSS_ENTROPY_LOSS: @@ -6624,7 +6651,21 @@ static ggml_backend_feature * ggml_backend_cuda_get_features(ggml_backend_reg_t GGML_UNUSED(reg); } +#if defined(GGML_USE_HIP) +static size_t ggml_backend_hip_vision_bias_bf16_workspace(ggml_backend_t backend) { + return backend && ggml_backend_is_cuda(backend) ? 76ULL*1024*1024 : 0; +} +static size_t ggml_backend_hip_vision_bias_bf16_launches(ggml_backend_t backend) { + return backend && ggml_backend_is_cuda(backend) ? + static_cast(backend->context)->vision_bias_launches : 0; +} +#endif + static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, const char * name) { +#if defined(GGML_USE_HIP) + if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_workspace")==0) return (void *)ggml_backend_hip_vision_bias_bf16_workspace; + if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_launches")==0) return (void *)ggml_backend_hip_vision_bias_bf16_launches; +#endif GGML_UNUSED(reg); if (strcmp(name, "ggml_backend_comm_init") == 0) { return (void *)ggml_backend_cuda_comm_init; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu new file mode 100644 index 000000000..053a4ddaf --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu @@ -0,0 +1,61 @@ +#include "vision-bias.cuh" +#if defined(GGML_USE_HIP) +#include + +bool ggml_hip_vision_bias_supported(const ggml_tensor * d) { + if (!d || d->type != GGML_TYPE_BF16 || !ggml_is_contiguous(d)) return false; + const auto w=d->src[0],x=d->src[1],b=d->src[2]; + if (!w || !x || !b) return false; + for (auto t : {w,x,b}) if (t->type != GGML_TYPE_BF16 || !ggml_is_contiguous(t) || t->ne[2]!=1 || t->ne[3]!=1) return false; + return w->ne[0]>0 && w->ne[0]<=INT_MAX && w->ne[1]>0 && w->ne[1]<=INT_MAX && + x->ne[1]>0 && x->ne[1]<=INT_MAX && x->ne[0]==w->ne[0] && + b->ne[0]==w->ne[1] && b->ne[1]==1 && d->ne[0]==w->ne[1] && d->ne[1]==x->ne[1] && d->ne[2]==1 && d->ne[3]==1; +} + +void ggml_hip_vision_bias(ggml_backend_cuda_context &ctx, ggml_tensor *dst) { + GGML_ASSERT(ggml_hip_vision_bias_supported(dst)); + ggml_cuda_set_device(ctx.device); + const auto stream=ctx.stream(); + constexpr size_t bytes=76ULL*1024*1024; + // One retained workspace per context, not per layer or graph. An event + // serializes workspace use even if this context schedules other streams. + if (!ctx.vision_bias_handle) CUBLAS_CHECK(hipblasLtCreate(&ctx.vision_bias_handle)); + if (!ctx.vision_bias_workspace) { + CUDA_CHECK(cudaMalloc(&ctx.vision_bias_workspace,bytes)); + CUDA_CHECK(cudaEventCreateWithFlags(&ctx.vision_bias_event,cudaEventDisableTiming)); + } + if (ctx.vision_bias_launches) CUDA_CHECK(cudaStreamWaitEvent(stream,ctx.vision_bias_event,0)); + const auto w=dst->src[0],x=dst->src[1],b=dst->src[2]; + const int64_t k=w->ne[0],m=w->ne[1],n=x->ne[1]; + hipblasLtMatmulDesc_t op=nullptr; + hipblasLtMatrixLayout_t a=nullptr,bl=nullptr,c=nullptr; + hipblasLtMatmulPreference_t pref=nullptr; + CUBLAS_CHECK(hipblasLtMatmulDescCreate(&op,HIPBLAS_COMPUTE_32F,HIP_R_32F)); + const hipblasOperation_t ta=HIPBLAS_OP_T,tb=HIPBLAS_OP_N; + const hipblasLtEpilogue_t epilogue=HIPBLASLT_EPILOGUE_BIAS; + CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_TRANSA,&ta,sizeof(ta))); + CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_TRANSB,&tb,sizeof(tb))); + CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_EPILOGUE,&epilogue,sizeof(epilogue))); + CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_BIAS_POINTER,&b->data,sizeof(b->data))); + CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&a,HIP_R_16BF,k,m,k)); + CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&bl,HIP_R_16BF,k,n,k)); + CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&c,HIP_R_16BF,m,n,m)); + CUBLAS_CHECK(hipblasLtMatmulPreferenceCreate(&pref)); + CUBLAS_CHECK(hipblasLtMatmulPreferenceSetAttribute(pref,HIPBLASLT_MATMUL_PREF_MAX_WORKSPACE_BYTES,&bytes,sizeof(bytes))); + hipblasLtMatmulHeuristicResult_t heuristic{}; int returned=0; + CUBLAS_CHECK(hipblasLtMatmulAlgoGetHeuristic(ctx.vision_bias_handle,op,a,bl,c,c,pref,1,&heuristic,&returned)); + GGML_ASSERT(returned==1 && "HIP vision bias: no first Lt heuristic; fallback forbidden"); + CUBLAS_CHECK(heuristic.state); + GGML_ASSERT(heuristic.workspaceSize<=bytes); + const float alpha=1,beta=0; + CUBLAS_CHECK(hipblasLtMatmul(ctx.vision_bias_handle,op,&alpha,w->data,a,x->data,bl,&beta, + dst->data,c,dst->data,c,&heuristic.algo,ctx.vision_bias_workspace,bytes,stream)); + CUDA_CHECK(cudaEventRecord(ctx.vision_bias_event,stream)); + ++ctx.vision_bias_launches; + CUBLAS_CHECK(hipblasLtMatmulPreferenceDestroy(pref)); + CUBLAS_CHECK(hipblasLtMatrixLayoutDestroy(c)); + CUBLAS_CHECK(hipblasLtMatrixLayoutDestroy(bl)); + CUBLAS_CHECK(hipblasLtMatrixLayoutDestroy(a)); + CUBLAS_CHECK(hipblasLtMatmulDescDestroy(op)); +} +#endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh new file mode 100644 index 000000000..9624bdbf9 --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh @@ -0,0 +1,7 @@ +#pragma once +#include "common.cuh" +#if defined(GGML_USE_HIP) +// DS4V-only explicit op; no ordinary MUL_MAT/ADD fusion or fallback. +bool ggml_hip_vision_bias_supported(const ggml_tensor * dst); +void ggml_hip_vision_bias(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +#endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt b/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt index 67dfedbae..7e528a5a5 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt +++ b/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt @@ -45,6 +45,7 @@ endif() find_package(hip REQUIRED) find_package(hipblas REQUIRED) +find_package(hipblaslt REQUIRED) find_package(rocblas REQUIRED) if (GGML_HIP_RCCL) @@ -167,4 +168,4 @@ get_filename_component(GGML_HIP_RUNTIME_DIR "${hip_DIR}/../.." ABSOLUTE) target_link_directories(ggml-hip BEFORE PRIVATE "${GGML_HIP_RUNTIME_DIR}") target_link_options(ggml-hip PRIVATE "-L${GGML_HIP_RUNTIME_DIR}") -target_link_libraries(ggml-hip PRIVATE ggml-base hip::host roc::rocblas roc::hipblas) +target_link_libraries(ggml-hip PRIVATE ggml-base hip::host roc::rocblas roc::hipblas roc::hipblaslt) diff --git a/server/deps/llama.cpp/ggml/src/ggml.c b/server/deps/llama.cpp/ggml/src/ggml.c index 82e2d9a1c..8804ddcf4 100644 --- a/server/deps/llama.cpp/ggml/src/ggml.c +++ b/server/deps/llama.cpp/ggml/src/ggml.c @@ -1200,9 +1200,10 @@ static const char * GGML_OP_NAME[GGML_OP_COUNT] = { "MUL_MAT_GROUPED_SRC", "PAGED_ATTN", + "MUL_MAT_BIAS_BF16", }; -static_assert(GGML_OP_COUNT == 105, "GGML_OP_COUNT != 105"); +static_assert(GGML_OP_COUNT == 106, "GGML_OP_COUNT != 106"); static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "none", @@ -1327,9 +1328,10 @@ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "X*grouped(Y)", "paged_attn(q,k,v)", + "bf16(X*Y+bias)", }; -static_assert(GGML_OP_COUNT == 105, "GGML_OP_COUNT != 105"); +static_assert(GGML_OP_COUNT == 106, "GGML_OP_COUNT != 106"); static_assert(GGML_OP_POOL_COUNT == 2, "GGML_OP_POOL_COUNT != 2"); @@ -3419,6 +3421,22 @@ struct ggml_tensor * ggml_mul_mat( return result; } +struct ggml_tensor * ggml_mul_mat_bias_bf16( + struct ggml_context * ctx, struct ggml_tensor * w, + struct ggml_tensor * x, struct ggml_tensor * b) { + GGML_ASSERT(w && x && b); + GGML_ASSERT(w->type == GGML_TYPE_BF16 && x->type == GGML_TYPE_BF16 && b->type == GGML_TYPE_BF16); + GGML_ASSERT(ggml_is_contiguous(w) && ggml_is_contiguous(x) && ggml_is_contiguous(b)); + GGML_ASSERT(w->ne[2] == 1 && w->ne[3] == 1 && x->ne[2] == 1 && x->ne[3] == 1); + GGML_ASSERT(w->ne[0] > 0 && w->ne[0] <= INT_MAX && w->ne[1] > 0 && w->ne[1] <= INT_MAX); + GGML_ASSERT(x->ne[0] == w->ne[0] && x->ne[1] > 0 && x->ne[1] <= INT_MAX); + GGML_ASSERT(b->ne[0] == w->ne[1] && ggml_is_vector(b)); + struct ggml_tensor * result = ggml_new_tensor_2d(ctx, GGML_TYPE_BF16, w->ne[1], x->ne[1]); + result->op = GGML_OP_MUL_MAT_BIAS_BF16; + result->src[0] = w; result->src[1] = x; result->src[2] = b; + return result; +} + struct ggml_tensor * ggml_mul_mat_grouped_src( struct ggml_context * ctx, struct ggml_tensor * a, @@ -7771,6 +7789,7 @@ static void ggml_compute_backward( case GGML_OP_NONE: { // noop } break; + case GGML_OP_MUL_MAT_BIAS_BF16: // inference-only, no backward kernel case GGML_OP_COUNT: default: { GGML_ABORT("%s: unsupported ggml op for backward pass: %s\n", __func__, ggml_op_name(tensor->op)); diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 0549f1ade..88d918bbb 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -108,8 +108,26 @@ std::vector read(Tensor * t) { } } namespace detail { +static size_t hip_bias_query(ggml_backend_t backend,const char * name) { + if(!backend) return 0; + auto device=ggml_backend_get_device(backend); + if(!device) return 0; + auto reg=ggml_backend_dev_backend_reg(device); + if(!reg) return 0; + using Query=size_t (*)(ggml_backend_t); + auto query=reinterpret_cast(ggml_backend_reg_get_proc_address(reg,name)); + return query ? query(backend) : 0; +} +size_t hip_bias_workspace(ggml_backend_t b) { return hip_bias_query(b,"ggml_backend_hip_vision_bias_bf16_workspace"); } +size_t hip_bias_launches(ggml_backend_t b) { return hip_bias_query(b,"ggml_backend_hip_vision_bias_bf16_launches"); } Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias,bool preserve_biased_product,ggml_backend_t backend) { - (void)backend; // RED fixture probe: production arithmetic is unchanged. + // HIP's explicit capability is required: generic supports_op defaults on + // other backends are not evidence of this source-specific fused operation. + if(bias && hip_bias_workspace(backend)) { + auto y=ggml_mul_mat_bias_bf16(c,weight,ggml_cast(c,input,GGML_TYPE_BF16),bias); + require(ggml_backend_supports_op(backend,y),"HIP fused BF16 vision linear unsupported; fallback forbidden"); + return ggml_cast(c,y,GGML_TYPE_F32); + } // Source biased linears round after the bias. The HIP BF16 BLAS path can // round its product to BF16 even with GGML_PREC_F32; an F32 weight operand // preserves the product until the explicit final boundary below. @@ -179,7 +197,7 @@ struct VisionRuntime::Impl { const auto device=ggml_backend_get_device(backend); const bool preserve=device && (ggml_backend_dev_type(device)==GGML_BACKEND_DEVICE_TYPE_GPU || ggml_backend_dev_type(device)==GGML_BACKEND_DEVICE_TYPE_IGPU); - return detail::linear(c,weight(name+".weight"),x,bias ? weight(name+".bias") : nullptr,preserve); + return detail::linear(c,weight(name+".weight"),x,bias ? weight(name+".bias") : nullptr,preserve,backend); } Tensor * norm(ggml_context * c,Tensor * x,const std::string & name) { return rounded(c,ggml_mul(c,ggml_rms_norm(c,x,config.rms_epsilon), @@ -196,7 +214,8 @@ struct VisionRuntime::Impl { // reserve_n_size calculates requirements without allocating a backend buffer. size_t required=0; ggml_gallocr_reserve_n_size(allocator,g.graph,nullptr,nullptr,&required); - require(required<=MAX_SCRATCH,"vision scratch exceeds 2 GiB bound"); + const size_t external=detail::hip_bias_workspace(backend); + require(external<=MAX_SCRATCH && required<=MAX_SCRATCH-external,"vision scratch including retained Lt workspace exceeds 2 GiB bound"); for(int i=0;i & output,std:: void VisionRuntime::release_scratch() { if(impl_ && impl_->allocator) { ggml_gallocr_free(impl_->allocator); impl_->allocator=nullptr; } } const VisionConfig * VisionRuntime::config() const { return impl_ ? &impl_->config : nullptr; } size_t VisionRuntime::weight_bytes() const { return impl_ ? ggml_backend_buffer_get_size(impl_->buffer) : 0; } -size_t VisionRuntime::scratch_bytes() const { return impl_ && impl_->allocator ? ggml_gallocr_get_buffer_size(impl_->allocator,0) : 0; } +size_t VisionRuntime::scratch_bytes() const { + if(!impl_) return 0; + // This conservative reservation remains after release_scratch(): the HIP + // context retains its workspace until backend destruction. + return detail::hip_bias_workspace(impl_->backend) + + (impl_->allocator ? ggml_gallocr_get_buffer_size(impl_->allocator,0) : 0); +} } // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index fb8de2ba8..f90690097 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -53,6 +53,9 @@ class VisionRuntime { // The same geometry primitives used by the runtime and standalone unit tests. namespace detail { +// HIP registry capability; zero on CPU/NVIDIA or an unavailable backend. +size_t hip_bias_workspace(ggml_backend_t); +size_t hip_bias_launches(ggml_backend_t); ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias, bool preserve_biased_product, ggml_backend_t backend = nullptr); void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine); diff --git a/server/tools/ds4v_vision/README.md b/server/tools/ds4v_vision/README.md index 47a65a390..6c304e4f4 100644 --- a/server/tools/ds4v_vision/README.md +++ b/server/tools/ds4v_vision/README.md @@ -149,3 +149,30 @@ Append `hip:0` or `hip:1` to an encode/load-only probe command to request that device explicitly. It fails if unavailable and never falls back to CPU. Building the target is not GPU qualification. Do not run it on GPU before the private text load proof and the operator's GPU window permit it. + + +## Explicit HIP fused-bias source comparison + +`ds4v_linear_source cpu|hip:0 tiny|patch|qkv FIXTURE_DIR REFERENCE_F32 NEW_OUTPUT_DIR` +runs the shared production vision-linear helper on exact BF16 values stored as +F32 fixture files. Frozen source fixtures, input/library hashes, actual device +identity, and GPU release must be checked by the external supervisor. The tool +never selects a different backend or modifies a reference. Exit 3 preserves a +numerical failure; it is not a successful qualification. HIP requires exactly +one explicit fused operation in the graph and exactly one actual Lt launch. +The unbiased lane separately retains its ordinary product and final BF16 cast. + +The historical `ds4v_linear_rounding` mathematical RNE probe continues to test +its explicitly requested old helper mode (no HIP capability argument). Its +biased RNE oracle differs from the frozen original HIP fused-source oracle; +it does not qualify the new runtime operation. Use `ds4v_linear_source` and +then the unchanged full-image gates for the actual runtime path. + +HIP biased linears now use a dedicated, source-configured BF16 hipBLASLt bias +epilogue operation. CPU/NVIDIA and unbiased graph construction retain the +previous implementation. The HIP backend retains one 76 MiB workspace outside +the graph allocator, charged against the 2 GiB scratch bound and included in +`scratch_bytes()` even after `release_scratch()`. It is freed when the borrowed +backend context is destroyed. Graphs containing the operation do not use HIP +graph capture. Only the pinned Radeon RX 7900 XT reference is the qualification +target; other HIP hardware has not been qualified. diff --git a/server/tools/ds4v_vision/linear_source.cpp b/server/tools/ds4v_vision/linear_source.cpp index 71b248337..b0075d6b4 100644 --- a/server/tools/ds4v_vision/linear_source.cpp +++ b/server/tools/ds4v_vision/linear_source.cpp @@ -14,6 +14,7 @@ #include #include #include +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_COUNT==106,"operation ABI changed unexpectedly"); static void check(bool b,const char *s) { if(!b) throw std::runtime_error(s); } static uint32_t bits(float v) { uint32_t b; std::memcpy(&b,&v,4); return b; } static std::vector load(const std::filesystem::path&p,size_t n) { @@ -64,12 +65,21 @@ int main(int argc,char**argv) { auto expected_u=ggml_cast(c,ggml_cast(c,raw,GGML_TYPE_BF16),GGML_TYPE_F32); auto g=ggml_new_graph(c); for(auto t:{y,u,expected_u}) { ggml_set_output(t); ggml_build_forward_expand(g,t); } + const size_t external=dflash::vision::detail::hip_bias_workspace(backend); + size_t ops=0; + for(int i=0;iop==GGML_OP_MUL_MAT_BIAS_BF16; + check(ops==(dev=="hip:0"?1u:0u),"wrong actual fused-op graph dispatch"); + check((external!=0)==(dev=="hip:0"),"wrong HIP-only capability"); + const size_t before=dflash::vision::detail::hip_bias_launches(backend); size_t scratch=0; ggml_gallocr_reserve_n_size(owner.a,g,nullptr,nullptr,&scratch); check(scratch<128ULL*1024*1024,"graph scratch exceeds fixed bound"); for(int i=0;iop == GGML_OP_MUL_MAT_BIAS_BF16) return false; const struct ggml_tensor * src0 = op->src[0]; const struct ggml_tensor * src1 = op->src[1]; From 969318c82ccfaa41c53ff87dc9cc39ab37c329db Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Fri, 4 Sep 2026 23:09:05 -0400 Subject: [PATCH 054/123] test(ds4v): enforce local HIP op contract and preserved CPU graph --- server/deps/llama.cpp/VENDOR.md | 4 +- .../llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp | 2 + server/tools/ds4v_vision/CMakeLists.txt | 3 ++ server/tools/ds4v_vision/linear_contract.cpp | 45 +++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 server/tools/ds4v_vision/linear_contract.cpp diff --git a/server/deps/llama.cpp/VENDOR.md b/server/deps/llama.cpp/VENDOR.md index 47efa79f2..cf1b606d8 100644 --- a/server/deps/llama.cpp/VENDOR.md +++ b/server/deps/llama.cpp/VENDOR.md @@ -34,7 +34,9 @@ The local `GGML_OP_MUL_MAT_BIAS_BF16` operation is appended after `PAGED_ATTN`; all existing op numeric values are preserved, while `GGML_OP_COUNT` grows from 105 to 106; the existing RPC header contract advances protocol patch 5 to 6. The RPC registry does not expose the HIP capability, so vision never sends this -new operation through RPC. Rebuild ggml-base, CPU/HIP backends, and consumers together. Do not +new operation through RPC; RPC supports_op also rejects it locally. Protocol +patch mismatches only warn, so this does not rely on a version handshake to +reject older peers. Rebuild ggml-base, CPU/HIP backends, and consumers together. Do not mix old shared libraries with this header or serialize the new operation for an older reader. This is an inference-only extension; CPU compute/backward reject it, and HIP alone advertises the explicit registry capability. Other diff --git a/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp b/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp index 3c5990dae..03bfa2ffd 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp @@ -1984,6 +1984,8 @@ static ggml_backend_buffer_type_t ggml_backend_rpc_device_get_buffer_type(ggml_b } static bool ggml_backend_rpc_device_supports_op(ggml_backend_dev_t dev, const struct ggml_tensor * op) { + // This opt-in local HIP extension must not be sent to an older RPC peer. + if (op->op == GGML_OP_MUL_MAT_BIAS_BF16) return false; GGML_UNUSED(dev); GGML_UNUSED(op); //TODO: call the remote backend and cache the results diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 371108a0e..63ea65034 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -57,5 +57,8 @@ if(DS4V_VISION_HIP) endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) +add_executable(ds4v_linear_contract linear_contract.cpp) +target_link_libraries(ds4v_linear_contract PRIVATE ds4v_vision) enable_testing() +add_test(NAME ds4v_linear_contract COMMAND ds4v_linear_contract) add_test(NAME ds4v_vision_geometry COMMAND ds4v_vision_geometry) diff --git a/server/tools/ds4v_vision/linear_contract.cpp b/server/tools/ds4v_vision/linear_contract.cpp new file mode 100644 index 000000000..66a3c090a --- /dev/null +++ b/server/tools/ds4v_vision/linear_contract.cpp @@ -0,0 +1,45 @@ +#include "deepseek4/deepseek4_vision.h" +#include "ggml-cpu.h" +#include "ggml-rpc.h" // operation-count/version compile contract +#include +#include +#include +#include +#include +static void check(bool ok,const char *why) { if(!ok)throw std::runtime_error(why); } +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_COUNT==106,"operation ABI changed"); +static void rejected(int mode) { + pid_t pid=fork(); check(pid>=0,"fork failed"); + if(pid==0) { + auto c=ggml_init({1024*1024,nullptr,true}); + auto w=ggml_new_tensor_2d(c,mode==0?GGML_TYPE_F32:GGML_TYPE_BF16,64,32); + auto x=ggml_new_tensor_2d(c,GGML_TYPE_BF16,mode==1?32:64,32); + auto b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,mode==2?16:32); + if(mode==3) x=ggml_transpose(c,x); + (void)ggml_mul_mat_bias_bf16(c,w,x,b); + _exit(0); + } + int status=0; check(waitpid(pid,&status,0)==pid,"wait failed"); + check(WIFSIGNALED(status)&&WTERMSIG(status)==SIGABRT,"invalid constructor was not rejected"); +} +int main() { + auto backend=ggml_backend_cpu_init(); auto c=ggml_init({1024*1024,nullptr,true}); + try { + check(backend&&c,"initialization failed"); + check(dflash::vision::detail::hip_bias_workspace(nullptr)==0 && dflash::vision::detail::hip_bias_workspace(backend)==0,"CPU/null advertised HIP capability"); + auto w=ggml_new_tensor_2d(c,GGML_TYPE_BF16,64,32),x=ggml_new_tensor_2d(c,GGML_TYPE_BF16,64,32),b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,32); + auto y=ggml_mul_mat_bias_bf16(c,w,x,b); + check(y->type==GGML_TYPE_BF16 && y->src[0]==w && y->src[1]==x && y->src[2]==b && y->ne[0]==32 && y->ne[1]==32,"explicit op contract mismatch"); + check(!ggml_backend_supports_op(backend,y),"CPU advertised HIP op"); + auto xf=ggml_new_tensor_2d(c,GGML_TYPE_F32,64,32); + for(bool preserve:{false,true}) { + auto old=dflash::vision::detail::linear(c,w,xf,b,preserve); + auto selected=dflash::vision::detail::linear(c,w,xf,b,preserve,backend); + check(old->op==selected->op && selected->src[0]->op==old->src[0]->op,"non-HIP graph root changed"); + auto product=selected->src[0]->src[0]->src[0]; + check(product->op==GGML_OP_MUL_MAT && (product->src[0]->op==GGML_OP_CPY)==preserve,"non-HIP product dispatch changed"); + } + for(int mode=0;mode<4;++mode) rejected(mode); + ggml_free(c); ggml_backend_free(backend); std::cout<<"PASS: CPU/null capability, preserved op ABI/graph, invalid constructor rejection\n"; return 0; + } catch(const std::exception&e) { std::cerr< Date: Fri, 4 Sep 2026 23:10:26 -0400 Subject: [PATCH 055/123] test(ds4v): verify full-tower Lt dispatch and retained workspace accounting --- server/src/deepseek4/deepseek4_vision.h | 2 ++ server/tools/ds4v_vision/probe.cpp | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index f90690097..8360f4898 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -42,9 +42,11 @@ class VisionRuntime { VisionOutput & output, std::string & error, bool retain_features = false, const StageObserver & observer = {}); bool sentinel(Sentinel identity, std::vector & output, std::string & error) const; + // Releases graph arena; backend-owned HIP Lt workspace remains retained. void release_scratch(); const VisionConfig * config() const; size_t weight_bytes() const; + // Conservative graph arena + external HIP workspace reservation. size_t scratch_bytes() const; private: struct Impl; diff --git a/server/tools/ds4v_vision/probe.cpp b/server/tools/ds4v_vision/probe.cpp index 025c05ac4..ec8bfff1b 100644 --- a/server/tools/ds4v_vision/probe.cpp +++ b/server/tools/ds4v_vision/probe.cpp @@ -75,8 +75,13 @@ int main(int argc,char ** argv) { std::cout<<"stage="< Date: Fri, 4 Sep 2026 23:11:13 -0400 Subject: [PATCH 056/123] test(ds4v): require explicit HIP capability in full qualification probe --- server/tools/ds4v_vision/probe.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/tools/ds4v_vision/probe.cpp b/server/tools/ds4v_vision/probe.cpp index ec8bfff1b..7518993fc 100644 --- a/server/tools/ds4v_vision/probe.cpp +++ b/server/tools/ds4v_vision/probe.cpp @@ -80,7 +80,9 @@ int main(int argc,char ** argv) { if(!runtime.encode(patches,grid,output,error,true,observer)) throw std::runtime_error(error); const auto lt_launches=detail::hip_bias_launches(backend)-lt_before; const auto external=detail::hip_bias_workspace(backend); - if(lt_launches!=(external ? 67u : 0u)) throw std::runtime_error("unexpected actual HIP fused-bias dispatch count"); + const bool hip_requested=device=="hip:0" || device=="hip:1"; + if((external!=0)!=hip_requested) throw std::runtime_error("requested HIP backend lacks fused-bias capability"); + if(lt_launches!=(hip_requested ? 67u : 0u)) throw std::runtime_error("unexpected actual HIP fused-bias dispatch count"); std::cout<<"hip_fused_bias_launches="< Date: Sat, 5 Sep 2026 01:58:42 -0400 Subject: [PATCH 057/123] fix: reserve buffers in DS4V source probe before allocation --- server/tools/ds4v_vision/linear_source.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/tools/ds4v_vision/linear_source.cpp b/server/tools/ds4v_vision/linear_source.cpp index b0075d6b4..a6d507fd5 100644 --- a/server/tools/ds4v_vision/linear_source.cpp +++ b/server/tools/ds4v_vision/linear_source.cpp @@ -74,6 +74,7 @@ int main(int argc,char**argv) { size_t scratch=0; ggml_gallocr_reserve_n_size(owner.a,g,nullptr,nullptr,&scratch); check(scratch<128ULL*1024*1024,"graph scratch exceeds fixed bound"); for(int i=0;i Date: Sat, 5 Sep 2026 02:49:44 -0400 Subject: [PATCH 058/123] fix: match source HIP math for unbiased vision projections --- .../deps/llama.cpp/ggml/include/ggml-cuda.h | 5 +- server/deps/llama.cpp/ggml/include/ggml.h | 4 +- .../ggml/src/ggml-cuda/vision-bias.cu | 11 +- server/deps/llama.cpp/ggml/src/ggml.c | 11 +- server/src/deepseek4/deepseek4_vision.cpp | 6 +- server/tools/ds4v_vision/CMakeLists.txt | 3 + server/tools/ds4v_vision/linear_contract.cpp | 33 +++- server/tools/ds4v_vision/linear_rounding.cpp | 26 ++- server/tools/ds4v_vision/linear_source.cpp | 24 +-- .../ds4v_vision/linear_unbiased_source.cpp | 148 ++++++++++++++++++ server/tools/ds4v_vision/probe.cpp | 6 +- 11 files changed, 239 insertions(+), 38 deletions(-) create mode 100644 server/tools/ds4v_vision/linear_unbiased_source.cpp diff --git a/server/deps/llama.cpp/ggml/include/ggml-cuda.h b/server/deps/llama.cpp/ggml/include/ggml-cuda.h index 8e52c72a3..8685ab931 100644 --- a/server/deps/llama.cpp/ggml/include/ggml-cuda.h +++ b/server/deps/llama.cpp/ggml/include/ggml-cuda.h @@ -24,12 +24,13 @@ extern "C" { // wider batches remain on the MMQ path. #define GGML_CUDA_DS4_MIX_MMV_MAX_TOKENS 5 -// HIP registry-only opt-in DS4V fused-bias capability (not NVIDIA/CUDA). +// HIP registry-only opt-in DS4V BF16 linear capability (not NVIDIA/CUDA). // Lookup "ggml_backend_hip_vision_bias_bf16_workspace" as size_t (*)(ggml_backend_t): // nonzero means the explicit op is available, and returns its retained external // workspace reservation (76 MiB). Unsupported op shapes must fail, not fallback. // "ggml_backend_hip_vision_bias_bf16_launches" has the same signature and returns -// actual successful Lt submissions for focused dispatch verification. +// actual successful Lt submissions, with or without bias. Registry names retain +// their original spelling; both modes require a matching GGML/HIP library set. // backend API GGML_BACKEND_API ggml_backend_t ggml_backend_cuda_init(int device); diff --git a/server/deps/llama.cpp/ggml/include/ggml.h b/server/deps/llama.cpp/ggml/include/ggml.h index 945869063..25d9c740f 100644 --- a/server/deps/llama.cpp/ggml/include/ggml.h +++ b/server/deps/llama.cpp/ggml/include/ggml.h @@ -1457,7 +1457,9 @@ extern "C" { // A: k columns, n rows => [ne03, ne02, n, k] // B: k columns, m rows (i.e. we transpose it internally) => [ne03 * x, ne02 * y, m, k] // result is n columns, m rows => [ne03 * x, ne02 * y, m, n] - // Explicit inference-only BF16 W[k,m], X[k,n], bias[m] -> BF16 Y[m,n]. + // Explicit inference-only BF16 W[k,m], X[k,n], optional bias[m] -> BF16 Y[m,n]. + // Null bias selects the default Lt epilogue without a bias pointer. A present + // bias, including an all-zero vector, selects the fused bias epilogue. // Contiguous 2D operands only; HIP Lt capability required, no fallback. GGML_API struct ggml_tensor * ggml_mul_mat_bias_bf16( struct ggml_context * ctx, struct ggml_tensor * weight, diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu index 053a4ddaf..3a9da31ca 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu @@ -5,11 +5,12 @@ bool ggml_hip_vision_bias_supported(const ggml_tensor * d) { if (!d || d->type != GGML_TYPE_BF16 || !ggml_is_contiguous(d)) return false; const auto w=d->src[0],x=d->src[1],b=d->src[2]; - if (!w || !x || !b) return false; - for (auto t : {w,x,b}) if (t->type != GGML_TYPE_BF16 || !ggml_is_contiguous(t) || t->ne[2]!=1 || t->ne[3]!=1) return false; + if (!w || !x) return false; + for (auto t : {w,x}) if (t->type != GGML_TYPE_BF16 || !ggml_is_contiguous(t) || t->ne[2]!=1 || t->ne[3]!=1) return false; + if (b && (b->type!=GGML_TYPE_BF16 || !ggml_is_contiguous(b) || !ggml_is_vector(b) || b->ne[0]!=w->ne[1])) return false; return w->ne[0]>0 && w->ne[0]<=INT_MAX && w->ne[1]>0 && w->ne[1]<=INT_MAX && x->ne[1]>0 && x->ne[1]<=INT_MAX && x->ne[0]==w->ne[0] && - b->ne[0]==w->ne[1] && b->ne[1]==1 && d->ne[0]==w->ne[1] && d->ne[1]==x->ne[1] && d->ne[2]==1 && d->ne[3]==1; + d->ne[0]==w->ne[1] && d->ne[1]==x->ne[1] && d->ne[2]==1 && d->ne[3]==1; } void ggml_hip_vision_bias(ggml_backend_cuda_context &ctx, ggml_tensor *dst) { @@ -32,11 +33,11 @@ void ggml_hip_vision_bias(ggml_backend_cuda_context &ctx, ggml_tensor *dst) { hipblasLtMatmulPreference_t pref=nullptr; CUBLAS_CHECK(hipblasLtMatmulDescCreate(&op,HIPBLAS_COMPUTE_32F,HIP_R_32F)); const hipblasOperation_t ta=HIPBLAS_OP_T,tb=HIPBLAS_OP_N; - const hipblasLtEpilogue_t epilogue=HIPBLASLT_EPILOGUE_BIAS; + const hipblasLtEpilogue_t epilogue=b ? HIPBLASLT_EPILOGUE_BIAS : HIPBLASLT_EPILOGUE_DEFAULT; CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_TRANSA,&ta,sizeof(ta))); CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_TRANSB,&tb,sizeof(tb))); CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_EPILOGUE,&epilogue,sizeof(epilogue))); - CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_BIAS_POINTER,&b->data,sizeof(b->data))); + if (b) CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_BIAS_POINTER,&b->data,sizeof(b->data))); CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&a,HIP_R_16BF,k,m,k)); CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&bl,HIP_R_16BF,k,n,k)); CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&c,HIP_R_16BF,m,n,m)); diff --git a/server/deps/llama.cpp/ggml/src/ggml.c b/server/deps/llama.cpp/ggml/src/ggml.c index 8804ddcf4..a5de0bd8f 100644 --- a/server/deps/llama.cpp/ggml/src/ggml.c +++ b/server/deps/llama.cpp/ggml/src/ggml.c @@ -3424,13 +3424,16 @@ struct ggml_tensor * ggml_mul_mat( struct ggml_tensor * ggml_mul_mat_bias_bf16( struct ggml_context * ctx, struct ggml_tensor * w, struct ggml_tensor * x, struct ggml_tensor * b) { - GGML_ASSERT(w && x && b); - GGML_ASSERT(w->type == GGML_TYPE_BF16 && x->type == GGML_TYPE_BF16 && b->type == GGML_TYPE_BF16); - GGML_ASSERT(ggml_is_contiguous(w) && ggml_is_contiguous(x) && ggml_is_contiguous(b)); + GGML_ASSERT(w && x); + GGML_ASSERT(w->type == GGML_TYPE_BF16 && x->type == GGML_TYPE_BF16); + GGML_ASSERT(ggml_is_contiguous(w) && ggml_is_contiguous(x)); GGML_ASSERT(w->ne[2] == 1 && w->ne[3] == 1 && x->ne[2] == 1 && x->ne[3] == 1); GGML_ASSERT(w->ne[0] > 0 && w->ne[0] <= INT_MAX && w->ne[1] > 0 && w->ne[1] <= INT_MAX); GGML_ASSERT(x->ne[0] == w->ne[0] && x->ne[1] > 0 && x->ne[1] <= INT_MAX); - GGML_ASSERT(b->ne[0] == w->ne[1] && ggml_is_vector(b)); + if (b) { + GGML_ASSERT(b->type == GGML_TYPE_BF16 && ggml_is_contiguous(b)); + GGML_ASSERT(b->ne[0] == w->ne[1] && ggml_is_vector(b)); + } struct ggml_tensor * result = ggml_new_tensor_2d(ctx, GGML_TYPE_BF16, w->ne[1], x->ne[1]); result->op = GGML_OP_MUL_MAT_BIAS_BF16; result->src[0] = w; result->src[1] = x; result->src[2] = b; diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 88d918bbb..c12f2c6ba 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -122,10 +122,10 @@ size_t hip_bias_workspace(ggml_backend_t b) { return hip_bias_query(b,"ggml_back size_t hip_bias_launches(ggml_backend_t b) { return hip_bias_query(b,"ggml_backend_hip_vision_bias_bf16_launches"); } Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias,bool preserve_biased_product,ggml_backend_t backend) { // HIP's explicit capability is required: generic supports_op defaults on - // other backends are not evidence of this source-specific fused operation. - if(bias && hip_bias_workspace(backend)) { + // other backends are not evidence of this source-specific operation. + if(hip_bias_workspace(backend)) { auto y=ggml_mul_mat_bias_bf16(c,weight,ggml_cast(c,input,GGML_TYPE_BF16),bias); - require(ggml_backend_supports_op(backend,y),"HIP fused BF16 vision linear unsupported; fallback forbidden"); + require(ggml_backend_supports_op(backend,y),"HIP BF16 vision linear unsupported; fallback forbidden"); return ggml_cast(c,y,GGML_TYPE_F32); } // Source biased linears round after the bias. The HIP BF16 BLAS path can diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 63ea65034..3192e845d 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -48,12 +48,15 @@ add_executable(ds4v_vision_probe probe.cpp) target_link_libraries(ds4v_vision_probe PRIVATE ds4v_vision) add_executable(ds4v_linear_source linear_source.cpp) target_link_libraries(ds4v_linear_source PRIVATE ds4v_vision) +add_executable(ds4v_linear_unbiased_source linear_unbiased_source.cpp) +target_link_libraries(ds4v_linear_unbiased_source PRIVATE ds4v_vision) add_executable(ds4v_linear_rounding linear_rounding.cpp) target_link_libraries(ds4v_linear_rounding PRIVATE ds4v_vision) if(DS4V_VISION_HIP) target_compile_definitions(ds4v_vision_probe PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_rounding PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_source PRIVATE DS4V_VISION_HIP) + target_compile_definitions(ds4v_linear_unbiased_source PRIVATE DS4V_VISION_HIP) endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) diff --git a/server/tools/ds4v_vision/linear_contract.cpp b/server/tools/ds4v_vision/linear_contract.cpp index 66a3c090a..30839a95c 100644 --- a/server/tools/ds4v_vision/linear_contract.cpp +++ b/server/tools/ds4v_vision/linear_contract.cpp @@ -8,15 +8,22 @@ #include static void check(bool ok,const char *why) { if(!ok)throw std::runtime_error(why); } static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_COUNT==106,"operation ABI changed"); -static void rejected(int mode) { +static void rejected(int mode,bool with_bias=true) { pid_t pid=fork(); check(pid>=0,"fork failed"); if(pid==0) { auto c=ggml_init({1024*1024,nullptr,true}); auto w=ggml_new_tensor_2d(c,mode==0?GGML_TYPE_F32:GGML_TYPE_BF16,64,32); - auto x=ggml_new_tensor_2d(c,GGML_TYPE_BF16,mode==1?32:64,32); - auto b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,mode==2?16:32); - if(mode==3) x=ggml_transpose(c,x); - (void)ggml_mul_mat_bias_bf16(c,w,x,b); + auto x=ggml_new_tensor_2d(c,mode==4?GGML_TYPE_F32:GGML_TYPE_BF16,mode==1?32:64,32); + auto b=ggml_new_tensor_1d(c,mode==10?GGML_TYPE_F32:GGML_TYPE_BF16,mode==2?16:32); + // Keep valid dimensions so these cases independently exercise contiguity. + if(mode==3) x=ggml_transpose(c,ggml_new_tensor_2d(c,GGML_TYPE_BF16,32,64)); + if(mode==5) w=ggml_transpose(c,ggml_new_tensor_2d(c,GGML_TYPE_BF16,32,64)); + if(mode==6) w=ggml_new_tensor_3d(c,GGML_TYPE_BF16,64,32,2); + if(mode==7) x=ggml_new_tensor_3d(c,GGML_TYPE_BF16,64,32,2); + if(mode==8) w=nullptr; + if(mode==9) x=nullptr; + if(mode==11) b=ggml_new_tensor_2d(c,GGML_TYPE_BF16,32,2); + (void)ggml_mul_mat_bias_bf16(c,w,x,with_bias?b:nullptr); _exit(0); } int status=0; check(waitpid(pid,&status,0)==pid,"wait failed"); @@ -31,6 +38,11 @@ int main() { auto y=ggml_mul_mat_bias_bf16(c,w,x,b); check(y->type==GGML_TYPE_BF16 && y->src[0]==w && y->src[1]==x && y->src[2]==b && y->ne[0]==32 && y->ne[1]==32,"explicit op contract mismatch"); check(!ggml_backend_supports_op(backend,y),"CPU advertised HIP op"); + auto unbiased_op=ggml_mul_mat_bias_bf16(c,w,x,nullptr); + check(unbiased_op->op==GGML_OP_MUL_MAT_BIAS_BF16 && unbiased_op->type==GGML_TYPE_BF16 && + unbiased_op->src[0]==w && unbiased_op->src[1]==x && unbiased_op->src[2]==nullptr && + unbiased_op->ne[0]==32 && unbiased_op->ne[1]==32,"optional-bias op contract mismatch"); + check(!ggml_backend_supports_op(backend,unbiased_op),"CPU advertised unbiased HIP op"); auto xf=ggml_new_tensor_2d(c,GGML_TYPE_F32,64,32); for(bool preserve:{false,true}) { auto old=dflash::vision::detail::linear(c,w,xf,b,preserve); @@ -38,8 +50,19 @@ int main() { check(old->op==selected->op && selected->src[0]->op==old->src[0]->op,"non-HIP graph root changed"); auto product=selected->src[0]->src[0]->src[0]; check(product->op==GGML_OP_MUL_MAT && (product->src[0]->op==GGML_OP_CPY)==preserve,"non-HIP product dispatch changed"); + for(auto cpu_backend:{static_cast(nullptr),backend}) { + auto unbiased=dflash::vision::detail::linear(c,w,xf,nullptr,preserve,cpu_backend); + check(unbiased->op==GGML_OP_CPY && unbiased->type==GGML_TYPE_F32 && + unbiased->src[0]->op==GGML_OP_CPY && unbiased->src[0]->type==GGML_TYPE_BF16, + "non-HIP unbiased rounding boundary changed"); + auto raw=unbiased->src[0]->src[0]; + check(raw->op==GGML_OP_MUL_MAT && raw->src[0]==w && raw->src[1]==xf, + "non-HIP unbiased product dispatch changed"); + } } for(int mode=0;mode<4;++mode) rejected(mode); + for(int mode:{0,1,3,4,5,6,7,8,9}) rejected(mode,false); + for(int mode:{4,5,6,7,8,9,10,11}) rejected(mode); ggml_free(c); ggml_backend_free(backend); std::cout<<"PASS: CPU/null capability, preserved op ABI/graph, invalid constructor rejection\n"; return 0; } catch(const std::exception&e) { std::cerr<0) backend=ggml_backend_cuda_init(0); + else if(device=="hip:0" && ggml_backend_cuda_get_device_count()==1) backend=ggml_backend_cuda_init(0); #endif if(!backend) { std::cerr<<"requested backend unavailable\n"; return 1; } int status=1; @@ -98,12 +98,21 @@ int main(int argc,char ** argv) { auto b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,m); for(auto t:{w,x,b}) ggml_set_input(t); auto raw_dot=ggml_mul_mat(c,w,x); ggml_mul_mat_set_prec(raw_dot,GGML_PREC_F32); - auto actual=dflash::vision::detail::linear(c,w,x,b,preserve); - auto unbiased=dflash::vision::detail::linear(c,w,x,nullptr,preserve); + auto actual=dflash::vision::detail::linear(c,w,x,b,preserve,backend); + auto unbiased=dflash::vision::detail::linear(c,w,x,nullptr,preserve,backend); auto graph=ggml_new_graph(c); for(auto t:{raw_dot,actual,unbiased}) { ggml_set_output(t); ggml_build_forward_expand(graph,t); } size_t required=0; ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&required); check(required<16*1024*1024,"tiny graph scratch unexpectedly large"); + const size_t external=dflash::vision::detail::hip_bias_workspace(backend); + constexpr size_t limit=128ULL*1024*1024; + check(external<=limit && required<=limit-external,"graph arena plus workspace exceeds bound"); + check((external!=0)==(device=="hip:0"),"wrong HIP capability"); + size_t explicit_ops=0; + for(int i=0;iop==GGML_OP_MUL_MAT_BIAS_BF16; + check(explicit_ops==(device=="hip:0"?2u:0u),"wrong explicit linear graph dispatch"); + const size_t launches_before=dflash::vision::detail::hip_bias_launches(backend); for(int i=0;iop==GGML_OP_MUL_MAT_BIAS_BF16; - check(ops==(dev=="hip:0"?1u:0u),"wrong actual fused-op graph dispatch"); + check(ops==(dev=="hip:0"?2u:0u),"wrong actual fused-op graph dispatch"); check((external!=0)==(dev=="hip:0"),"wrong HIP-only capability"); const size_t before=dflash::vision::detail::hip_bias_launches(backend); size_t scratch=0; ggml_gallocr_reserve_n_size(owner.a,g,nullptr,nullptr,&scratch); - check(scratch<128ULL*1024*1024,"graph scratch exceeds fixed bound"); + constexpr size_t limit=128ULL*1024*1024; + check(external<=limit && scratch<=limit-external,"graph arena plus workspace exceeds fixed bound"); for(int i=0;idata(),item.second->size()*4); check(bool(f),"output write failed"); } - std::cout<<"elements="< +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_COUNT==106, + "operation ABI changed unexpectedly"); +static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } +static size_t product(size_t a,size_t b) { + check(b==0 || a<=std::numeric_limits::max()/b,"size overflow"); + return a*b; +} +static uint32_t bits(float value) { uint32_t result; std::memcpy(&result,&value,4); return result; } +static std::vector load(const std::filesystem::path &path,size_t count) { + const size_t bytes=product(count,sizeof(float)); + check(bytes<=size_t(std::numeric_limits::max()),"fixture exceeds stream limit"); + check(std::filesystem::is_regular_file(path) && std::filesystem::file_size(path)==bytes,"fixture size mismatch"); + std::vector result(count); + std::ifstream file(path,std::ios::binary); + file.read(reinterpret_cast(result.data()),std::streamsize(bytes)); + check(bool(file),"fixture read failed"); + for(float value:result) + check(std::isfinite(value) && !(bits(value)&65535),"fixture must contain finite exact BF16 values"); + return result; +} +static std::vector pack(const std::vector &values) { + std::vector result; result.reserve(values.size()); + for(float value:values) result.push_back({uint16_t(bits(value)>>16)}); + return result; +} +struct Resources { + ggml_backend_t backend=nullptr; + ggml_context *context=nullptr; + ggml_gallocr_t allocator=nullptr; + ~Resources() { + if(backend) ggml_backend_synchronize(backend); + if(allocator) ggml_gallocr_free(allocator); + if(context) ggml_free(context); + if(backend) ggml_backend_free(backend); + } +}; +int main(int argc,char **argv) { + std::cout<type==GGML_TYPE_F32 && size_t(ggml_nelements(y))==elements,"unexpected output layout"); + ggml_set_output(y); + auto graph=ggml_new_graph(owner.context); + ggml_build_forward_expand(graph,y); + size_t explicit_ops=0; + for(int i=0;iop==GGML_OP_MUL_MAT_BIAS_BF16) { + ++explicit_ops; + check(node->src[2]==nullptr,"unbiased graph unexpectedly has a bias operand"); + } + } + check(explicit_ops==(device=="hip:0"?1u:0u),"wrong explicit unbiased graph dispatch"); + const size_t external=dflash::vision::detail::hip_bias_workspace(owner.backend); + check(external==(device=="hip:0"?76ULL*1024*1024:0),"wrong fixed HIP workspace capability"); + owner.allocator=ggml_gallocr_new(ggml_backend_get_default_buffer_type(owner.backend)); + check(owner.allocator,"graph allocator creation failed"); + size_t arena=0; + ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&arena); + constexpr size_t limit=128ULL*1024*1024; + check(external<=limit && arena<=limit-external,"graph arena plus workspace exceeds 128 MiB"); + check(ggml_gallocr_reserve(owner.allocator,graph),"graph reservation failed"); + check(ggml_gallocr_alloc_graph(owner.allocator,graph),"graph allocation failed"); + check(ggml_gallocr_get_buffer_size(owner.allocator,0)<=arena,"actual arena exceeds reservation estimate"); + ggml_backend_tensor_set(w,weights.data(),0,product(weights.size(),sizeof(ggml_bf16_t))); + ggml_backend_tensor_set(x,inputs.data(),0,product(inputs.size(),sizeof(float))); + const size_t before=dflash::vision::detail::hip_bias_launches(owner.backend); + check(ggml_backend_graph_compute(owner.backend,graph)==GGML_STATUS_SUCCESS,"graph execution failed"); + ggml_backend_synchronize(owner.backend); + const size_t after=dflash::vision::detail::hip_bias_launches(owner.backend); + check(after>=before && after-before==explicit_ops,"wrong actual Lt submission count"); + std::vector actual(elements); + ggml_backend_tensor_get(y,actual.data(),0,output_bytes); + size_t mismatches=0; double max_abs=0; + for(size_t i=0;i(actual.data()),std::streamsize(output_bytes)); + file.close(); check(bool(file),"output write failed"); + std::cout<<"explicit_unbiased_ops="< Date: Sat, 5 Sep 2026 02:51:33 -0400 Subject: [PATCH 059/123] test: retain the generic linear rounding oracle --- server/tools/ds4v_vision/linear_rounding.cpp | 26 +++++--------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/server/tools/ds4v_vision/linear_rounding.cpp b/server/tools/ds4v_vision/linear_rounding.cpp index 1ea2a6a17..e9304d1a1 100644 --- a/server/tools/ds4v_vision/linear_rounding.cpp +++ b/server/tools/ds4v_vision/linear_rounding.cpp @@ -52,7 +52,7 @@ int main(int argc,char ** argv) { const std::string device=argv[1]; if(device=="cpu") { backend=ggml_backend_cpu_init(); if(backend) ggml_backend_cpu_set_n_threads(backend,2); } #ifdef DS4V_VISION_HIP - else if(device=="hip:0" && ggml_backend_cuda_get_device_count()==1) backend=ggml_backend_cuda_init(0); + else if(device=="hip:0" && ggml_backend_cuda_get_device_count()>0) backend=ggml_backend_cuda_init(0); #endif if(!backend) { std::cerr<<"requested backend unavailable\n"; return 1; } int status=1; @@ -98,21 +98,12 @@ int main(int argc,char ** argv) { auto b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,m); for(auto t:{w,x,b}) ggml_set_input(t); auto raw_dot=ggml_mul_mat(c,w,x); ggml_mul_mat_set_prec(raw_dot,GGML_PREC_F32); - auto actual=dflash::vision::detail::linear(c,w,x,b,preserve,backend); - auto unbiased=dflash::vision::detail::linear(c,w,x,nullptr,preserve,backend); + auto actual=dflash::vision::detail::linear(c,w,x,b,preserve); + auto unbiased=dflash::vision::detail::linear(c,w,x,nullptr,preserve); auto graph=ggml_new_graph(c); for(auto t:{raw_dot,actual,unbiased}) { ggml_set_output(t); ggml_build_forward_expand(graph,t); } size_t required=0; ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&required); check(required<16*1024*1024,"tiny graph scratch unexpectedly large"); - const size_t external=dflash::vision::detail::hip_bias_workspace(backend); - constexpr size_t limit=128ULL*1024*1024; - check(external<=limit && required<=limit-external,"graph arena plus workspace exceeds bound"); - check((external!=0)==(device=="hip:0"),"wrong HIP capability"); - size_t explicit_ops=0; - for(int i=0;iop==GGML_OP_MUL_MAT_BIAS_BF16; - check(explicit_ops==(device=="hip:0"?2u:0u),"wrong explicit linear graph dispatch"); - const size_t launches_before=dflash::vision::detail::hip_bias_launches(backend); for(int i=0;i Date: Sat, 5 Sep 2026 03:38:11 -0400 Subject: [PATCH 060/123] Match original DS4V HIP vision normalization arithmetic --- server/deps/llama.cpp/ggml/include/ggml-rpc.h | 4 +- server/deps/llama.cpp/ggml/include/ggml.h | 9 ++ .../llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c | 4 + .../llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp | 3 +- .../llama.cpp/ggml/src/ggml-cuda/common.cuh | 1 + .../llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu | 29 ++++- .../deps/llama.cpp/ggml/src/ggml-cuda/norm.cu | 64 ++++++++++ .../llama.cpp/ggml/src/ggml-cuda/norm.cuh | 6 + .../llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp | 2 +- server/deps/llama.cpp/ggml/src/ggml.c | 24 +++- server/src/deepseek4/deepseek4_vision.cpp | 30 ++++- server/src/deepseek4/deepseek4_vision.h | 4 + server/tools/ds4v_vision/CMakeLists.txt | 6 + server/tools/ds4v_vision/linear_contract.cpp | 2 +- server/tools/ds4v_vision/linear_source.cpp | 2 +- .../ds4v_vision/linear_unbiased_source.cpp | 2 +- server/tools/ds4v_vision/norm_contract.cpp | 63 ++++++++++ server/tools/ds4v_vision/norm_source.cpp | 116 ++++++++++++++++++ server/tools/ds4v_vision/probe.cpp | 4 + 19 files changed, 359 insertions(+), 16 deletions(-) create mode 100644 server/tools/ds4v_vision/norm_contract.cpp create mode 100644 server/tools/ds4v_vision/norm_source.cpp diff --git a/server/deps/llama.cpp/ggml/include/ggml-rpc.h b/server/deps/llama.cpp/ggml/include/ggml-rpc.h index a36120ecc..31ad769c2 100644 --- a/server/deps/llama.cpp/ggml/include/ggml-rpc.h +++ b/server/deps/llama.cpp/ggml/include/ggml-rpc.h @@ -8,10 +8,10 @@ extern "C" { #define RPC_PROTO_MAJOR_VERSION 3 #define RPC_PROTO_MINOR_VERSION 6 -#define RPC_PROTO_PATCH_VERSION 6 +#define RPC_PROTO_PATCH_VERSION 7 #ifdef __cplusplus -static_assert(GGML_OP_COUNT == 106, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION"); +static_assert(GGML_OP_COUNT == 107, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION"); #endif #define GGML_RPC_MAX_SERVERS 16 diff --git a/server/deps/llama.cpp/ggml/include/ggml.h b/server/deps/llama.cpp/ggml/include/ggml.h index 25d9c740f..f39452193 100644 --- a/server/deps/llama.cpp/ggml/include/ggml.h +++ b/server/deps/llama.cpp/ggml/include/ggml.h @@ -618,6 +618,7 @@ extern "C" { GGML_OP_PAGED_ATTN, GGML_OP_MUL_MAT_BIAS_BF16, // explicit HIP-only DS4V fused bias + GGML_OP_RMS_NORM_VISION_F32, // inference-only HIP source-order DS4V normalization GGML_OP_COUNT, }; @@ -1420,6 +1421,14 @@ extern "C" { struct ggml_tensor * a, float eps); + // HIP wave32 only: contiguous F32 [1024, rows], 16 <= rows <= INT_MAX/1024. + // Input values must be BF16-representable. Returns normalized F32 before + // weight multiplication and BF16 rounding; preserves the DS4V source order. + GGML_API struct ggml_tensor * ggml_rms_norm_vision_f32( + struct ggml_context * ctx, + struct ggml_tensor * a, + float eps); + // group normalize along ne0*ne1*n_groups // used in stable-diffusion GGML_API struct ggml_tensor * ggml_group_norm( diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c index e55f79892..d455ff1d4 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c @@ -2198,6 +2198,8 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm } case GGML_OP_MUL_MAT_BIAS_BF16: { GGML_ABORT("GGML_OP_MUL_MAT_BIAS_BF16 requires the HIP Lt backend"); } + case GGML_OP_RMS_NORM_VISION_F32: + { GGML_ABORT("GGML_OP_RMS_NORM_VISION_F32 requires the HIP wave32 backend"); } case GGML_OP_PAGED_ATTN: { GGML_ABORT("GGML_OP_PAGED_ATTN is only supported on the CUDA backend"); @@ -2600,6 +2602,8 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { { n_tasks = n_threads; } break; + case GGML_OP_RMS_NORM_VISION_F32: + GGML_ABORT("GGML_OP_RMS_NORM_VISION_F32 cannot be planned on CPU"); case GGML_OP_RWKV_WKV6: case GGML_OP_GATED_LINEAR_ATTN: case GGML_OP_RWKV_WKV7: diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp index 8faf494a0..9f784cde6 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp @@ -422,7 +422,7 @@ static ggml_backend_buffer_t ggml_backend_cpu_device_buffer_from_host_ptr(ggml_b static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const struct ggml_tensor * op) { // No extra-buffer handler may accidentally advertise this HIP-only op. - if (op->op == GGML_OP_MUL_MAT_BIAS_BF16) return false; + if (op->op == GGML_OP_MUL_MAT_BIAS_BF16 || op->op == GGML_OP_RMS_NORM_VISION_F32) return false; const struct ggml_tensor * src0 = op->src[0]; const struct ggml_tensor * src1 = op->src[1]; @@ -474,6 +474,7 @@ static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const st return !ggml_flash_attn_ext_is_ds4(op); case GGML_OP_PAGED_ATTN: case GGML_OP_MUL_MAT_BIAS_BF16: + case GGML_OP_RMS_NORM_VISION_F32: return false; case GGML_OP_SSM_CONV: // Every nonzero mode is a dflash CUDA/HIP extension (SpecLA, diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh index 9bf53fca8..e6370705c 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh @@ -1442,6 +1442,7 @@ struct ggml_backend_cuda_context { void * vision_bias_workspace = nullptr; // exactly 76 MiB, retained until context destruction cudaEvent_t vision_bias_event = nullptr; size_t vision_bias_launches = 0; + size_t vision_norm_launches = 0; #endif int curr_stream_no = 0; bool low_priority_streams = false; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu index 0464d9d93..839f01f7d 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu @@ -3627,6 +3627,13 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg break; #else return false; +#endif + case GGML_OP_RMS_NORM_VISION_F32: +#if defined(GGML_USE_HIP) + ggml_hip_vision_norm(ctx, dst); + break; +#else + return false; #endif case GGML_OP_PAGED_ATTN: ggml_cuda_paged_attn(ctx, dst); @@ -3917,9 +3924,9 @@ static bool ggml_cuda_graph_check_compability(ggml_cgraph * cgraph) { for (int i = 0; i < cgraph->n_nodes; i++) { ggml_tensor * node = cgraph->nodes[i]; - // This explicit opt-in op uses a host heuristic and retained workspace - // event. Qualification is direct execution, never graph capture/replay. - if (node->op == GGML_OP_MUL_MAT_BIAS_BF16) return false; + // These explicit vision operations are qualified through direct + // execution only; the linear also retains a workspace and event. + if (node->op == GGML_OP_MUL_MAT_BIAS_BF16 || node->op == GGML_OP_RMS_NORM_VISION_F32) return false; if (ggml_is_empty(node) || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE || node->op == GGML_OP_NONE) { continue; @@ -6473,6 +6480,12 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g return ggml_hip_vision_bias_supported(op); #else return false; +#endif + case GGML_OP_RMS_NORM_VISION_F32: +#if defined(GGML_USE_HIP) + return ggml_hip_vision_norm_supported(dev_ctx->device, op); +#else + return false; #endif case GGML_OP_PAGED_ATTN: return ggml_cuda_paged_attn_supported(op); @@ -6659,12 +6672,22 @@ static size_t ggml_backend_hip_vision_bias_bf16_launches(ggml_backend_t backend) return backend && ggml_backend_is_cuda(backend) ? static_cast(backend->context)->vision_bias_launches : 0; } +static bool ggml_backend_hip_vision_norm_f32_capable(ggml_backend_t backend) { + return backend && ggml_backend_is_cuda(backend) && + ggml_hip_vision_norm_capable(static_cast(backend->context)->device); +} +static size_t ggml_backend_hip_vision_norm_f32_launches(ggml_backend_t backend) { + return backend && ggml_backend_is_cuda(backend) ? + static_cast(backend->context)->vision_norm_launches : 0; +} #endif static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, const char * name) { #if defined(GGML_USE_HIP) if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_workspace")==0) return (void *)ggml_backend_hip_vision_bias_bf16_workspace; if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_launches")==0) return (void *)ggml_backend_hip_vision_bias_bf16_launches; + if (strcmp(name,"ggml_backend_hip_vision_norm_f32_capable")==0) return (void *)ggml_backend_hip_vision_norm_f32_capable; + if (strcmp(name,"ggml_backend_hip_vision_norm_f32_launches")==0) return (void *)ggml_backend_hip_vision_norm_f32_launches; #endif GGML_UNUSED(reg); if (strcmp(name, "ggml_backend_comm_init") == 0) { diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu index 696a6f441..ae948e935 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu @@ -1,6 +1,70 @@ #include "norm.cuh" +#include +#include #include +#if defined(GGML_USE_HIP) +bool ggml_hip_vision_norm_capable(int device) { + const auto & info = ggml_cuda_info(); + return device >= 0 && device < info.device_count && info.devices[device].warp_size == 32; +} + +bool ggml_hip_vision_norm_supported(int device, const ggml_tensor * op) { + if (!ggml_hip_vision_norm_capable(device) || !op || op->op != GGML_OP_RMS_NORM_VISION_F32 || + !op->src[0] || op->type != GGML_TYPE_F32 || op->src[0]->type != GGML_TYPE_F32 || + !ggml_is_contiguous(op) || !ggml_is_contiguous(op->src[0]) || + !ggml_are_same_shape(op, op->src[0]) || op->ne[0] != 1024 || op->ne[1] < 16 || + op->ne[1] > INT_MAX/1024 || op->ne[2] != 1 || op->ne[3] != 1) { + return false; + } + float eps; + memcpy(&eps, op->op_params, sizeof(eps)); + return std::isfinite(eps) && eps >= 0.0f; +} + +// PyTorch 3d3aa833db84eed6b7f5595cb5f162c2f78300a4, aten/src/ATen/native/cuda/ +// Reduce.cuh:499-557,655-668: four input accumulators and ascending ROCm shuffles. +// With contiguous width1024 and >=16 rows the source uses one wave32 per row. +static __global__ void rms_norm_vision_f32(const float * x, float * dst, int rows, float eps) { + const int row = blockIdx.x * 16 + threadIdx.y; + const int lane = threadIdx.x; + if (row >= rows) { + return; + } + float a[4] = {0, 0, 0, 0}; + for (int k = 0; k < 8; ++k) { + for (int j = 0; j < 4; ++j) { + const float xi = x[row*1024 + 4*(lane + 32*k) + j]; + volatile float squared = xi*xi; // source materializes F32 square before mean + a[j] = a[j] + squared; + } + } + float sum = ((a[0] + a[1]) + a[2]) + a[3]; + for (int offset = 1; offset < 32; offset *= 2) { + sum = sum + __shfl_down(sum, offset, 32); + } + sum = __shfl(sum, 0, 32); + const float mean = sum * (1.0f/1024.0f); + // Same pinned Torch UnaryOpsKernel.cu:78-80 calls global ::rsqrt. Its ROCm + // overload promotes float to double; keep the verified promotion explicitly. + const float scale = static_cast(::rsqrt(static_cast(mean + eps))); + for (int col = lane; col < 1024; col += 32) { + dst[row*1024 + col] = x[row*1024 + col] * scale; + } +} + +void ggml_hip_vision_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + GGML_ASSERT(ggml_hip_vision_norm_supported(ctx.device, dst)); + float eps; + memcpy(&eps, dst->op_params, sizeof(eps)); + const int rows = static_cast(dst->ne[1]); + rms_norm_vision_f32<<<(rows + 15)/16, dim3(32, 16), 0, ctx.stream()>>>( + static_cast(dst->src[0]->data), static_cast(dst->data), rows, eps); + CUDA_CHECK(cudaGetLastError()); + ++ctx.vision_norm_launches; +} +#endif + template static __global__ void norm_f32( const float * x, float * dst, const int ncols, const int64_t stride_row, const int64_t stride_channel, diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh index 6313a98ce..c6c91cb7d 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh @@ -6,6 +6,12 @@ void ggml_cuda_op_group_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst) void ggml_cuda_op_rms_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +#if defined(GGML_USE_HIP) +bool ggml_hip_vision_norm_capable(int device); +bool ggml_hip_vision_norm_supported(int device, const ggml_tensor * op); +void ggml_hip_vision_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +#endif + void ggml_cuda_op_rms_norm_fused(ggml_backend_cuda_context & ctx, ggml_tensor * dst, ggml_tensor * mul_tensor); void ggml_cuda_op_rms_norm_fused_add(ggml_backend_cuda_context & ctx, diff --git a/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp b/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp index 03bfa2ffd..09772b562 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp @@ -1985,7 +1985,7 @@ static ggml_backend_buffer_type_t ggml_backend_rpc_device_get_buffer_type(ggml_b static bool ggml_backend_rpc_device_supports_op(ggml_backend_dev_t dev, const struct ggml_tensor * op) { // This opt-in local HIP extension must not be sent to an older RPC peer. - if (op->op == GGML_OP_MUL_MAT_BIAS_BF16) return false; + if (op->op == GGML_OP_MUL_MAT_BIAS_BF16 || op->op == GGML_OP_RMS_NORM_VISION_F32) return false; GGML_UNUSED(dev); GGML_UNUSED(op); //TODO: call the remote backend and cache the results diff --git a/server/deps/llama.cpp/ggml/src/ggml.c b/server/deps/llama.cpp/ggml/src/ggml.c index a5de0bd8f..a03752344 100644 --- a/server/deps/llama.cpp/ggml/src/ggml.c +++ b/server/deps/llama.cpp/ggml/src/ggml.c @@ -1201,9 +1201,10 @@ static const char * GGML_OP_NAME[GGML_OP_COUNT] = { "PAGED_ATTN", "MUL_MAT_BIAS_BF16", + "RMS_NORM_VISION_F32", }; -static_assert(GGML_OP_COUNT == 106, "GGML_OP_COUNT != 106"); +static_assert(GGML_OP_COUNT == 107, "GGML_OP_COUNT != 107"); static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "none", @@ -1329,9 +1330,10 @@ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "paged_attn(q,k,v)", "bf16(X*Y+bias)", + "rms_norm_vision_f32(x)", }; -static_assert(GGML_OP_COUNT == 106, "GGML_OP_COUNT != 106"); +static_assert(GGML_OP_COUNT == 107, "GGML_OP_COUNT != 107"); static_assert(GGML_OP_POOL_COUNT == 2, "GGML_OP_POOL_COUNT != 2"); @@ -3310,6 +3312,23 @@ struct ggml_tensor * ggml_rms_norm_inplace( return ggml_rms_norm_impl(ctx, a, eps, true); } +// ggml_rms_norm_vision_f32 + +struct ggml_tensor * ggml_rms_norm_vision_f32( + struct ggml_context * ctx, + struct ggml_tensor * a, + float eps) { + GGML_ASSERT(a && a->type == GGML_TYPE_F32 && ggml_is_contiguous(a)); + GGML_ASSERT(a->ne[0] == 1024 && a->ne[1] >= 16 && a->ne[1] <= INT_MAX/1024); + GGML_ASSERT(a->ne[2] == 1 && a->ne[3] == 1); + GGML_ASSERT(isfinite(eps) && eps >= 0.0f); + struct ggml_tensor * result = ggml_dup_tensor(ctx, a); + ggml_set_op_params(result, &eps, sizeof(eps)); + result->op = GGML_OP_RMS_NORM_VISION_F32; + result->src[0] = a; + return result; +} + // ggml_rms_norm_back struct ggml_tensor * ggml_rms_norm_back( @@ -7793,6 +7812,7 @@ static void ggml_compute_backward( // noop } break; case GGML_OP_MUL_MAT_BIAS_BF16: // inference-only, no backward kernel + case GGML_OP_RMS_NORM_VISION_F32: case GGML_OP_COUNT: default: { GGML_ABORT("%s: unsupported ggml op for backward pass: %s\n", __func__, ggml_op_name(tensor->op)); diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index c12f2c6ba..0183a5917 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -108,7 +108,7 @@ std::vector read(Tensor * t) { } } namespace detail { -static size_t hip_bias_query(ggml_backend_t backend,const char * name) { +static size_t hip_size_query(ggml_backend_t backend,const char * name) { if(!backend) return 0; auto device=ggml_backend_get_device(backend); if(!device) return 0; @@ -118,8 +118,26 @@ static size_t hip_bias_query(ggml_backend_t backend,const char * name) { auto query=reinterpret_cast(ggml_backend_reg_get_proc_address(reg,name)); return query ? query(backend) : 0; } -size_t hip_bias_workspace(ggml_backend_t b) { return hip_bias_query(b,"ggml_backend_hip_vision_bias_bf16_workspace"); } -size_t hip_bias_launches(ggml_backend_t b) { return hip_bias_query(b,"ggml_backend_hip_vision_bias_bf16_launches"); } +size_t hip_bias_workspace(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_bias_bf16_workspace"); } +size_t hip_bias_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_bias_bf16_launches"); } +size_t hip_norm_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_norm_f32_launches"); } +bool hip_norm_capable(ggml_backend_t backend) { + if(!backend) return false; + auto device=ggml_backend_get_device(backend); + if(!device) return false; + auto reg=ggml_backend_dev_backend_reg(device); + if(!reg) return false; + using Query=bool (*)(ggml_backend_t); + auto query=reinterpret_cast(ggml_backend_reg_get_proc_address(reg,"ggml_backend_hip_vision_norm_f32_capable")); + return query && query(backend); +} +Tensor * rms_norm(ggml_context * c,Tensor * input,float epsilon,ggml_backend_t backend) { + if(!hip_bias_workspace(backend)) return ggml_rms_norm(c,input,epsilon); + require(hip_norm_capable(backend),"HIP source-order vision normalization unavailable; fallback forbidden"); + auto y=ggml_rms_norm_vision_f32(c,input,epsilon); + require(ggml_backend_supports_op(backend,y),"HIP source-order vision normalization unsupported; fallback forbidden"); + return y; +} Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias,bool preserve_biased_product,ggml_backend_t backend) { // HIP's explicit capability is required: generic supports_op defaults on // other backends are not evidence of this source-specific operation. @@ -200,7 +218,7 @@ struct VisionRuntime::Impl { return detail::linear(c,weight(name+".weight"),x,bias ? weight(name+".bias") : nullptr,preserve,backend); } Tensor * norm(ggml_context * c,Tensor * x,const std::string & name) { - return rounded(c,ggml_mul(c,ggml_rms_norm(c,x,config.rms_epsilon), + return rounded(c,ggml_mul(c,detail::rms_norm(c,x,config.rms_epsilon,backend), ggml_cast(c,weight(name+".weight"),GGML_TYPE_F32))); } std::vector execute(Graph & g,Tensor * input,const std::vector & values,Tensor * output, @@ -239,6 +257,8 @@ bool VisionRuntime::load(const std::string & path,ggml_backend_t backend,int dim error.clear(); try { require(backend!=nullptr,"null vision backend"); + require(!detail::hip_bias_workspace(backend) || detail::hip_norm_capable(backend), + "HIP source-order vision normalization unavailable"); Meta meta; meta.g=gguf_init_from_file(path.c_str(),{true,&meta.c}); require(meta.g && meta.c,"could not parse vision GGUF"); @@ -293,6 +313,8 @@ bool VisionRuntime::encode(const std::vector & patches,PatchGrid grid,Vis const int64_t block_tokens=padded_height*row_length+2+(padded_height/2*row_length%2)*2; require(block_tokens+3<=impl_->config.max_image_tokens,"patch grid exceeds image token budget"); require(patches.size()==size_t(n)*588,"patch count/shape mismatch"); + require(!detail::hip_bias_workspace(impl_->backend) || n>=16, + "HIP source-order vision normalization requires at least 16 patches"); for(float value:patches) require(std::isfinite(value),"non-finite image patch"); std::vector x; { diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index 8360f4898..5f0458df8 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -58,6 +58,10 @@ namespace detail { // HIP registry capability; zero on CPU/NVIDIA or an unavailable backend. size_t hip_bias_workspace(ggml_backend_t); size_t hip_bias_launches(ggml_backend_t); +bool hip_norm_capable(ggml_backend_t); +size_t hip_norm_launches(ggml_backend_t); +// F32 values exactly representing BF16 inputs; HIP source order needs at least 16 rows. +ggml_tensor * rms_norm(ggml_context *, ggml_tensor * input, float epsilon, ggml_backend_t); ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias, bool preserve_biased_product, ggml_backend_t backend = nullptr); void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine); diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 3192e845d..6558ff286 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -50,6 +50,8 @@ add_executable(ds4v_linear_source linear_source.cpp) target_link_libraries(ds4v_linear_source PRIVATE ds4v_vision) add_executable(ds4v_linear_unbiased_source linear_unbiased_source.cpp) target_link_libraries(ds4v_linear_unbiased_source PRIVATE ds4v_vision) +add_executable(ds4v_norm_source norm_source.cpp) +target_link_libraries(ds4v_norm_source PRIVATE ds4v_vision) add_executable(ds4v_linear_rounding linear_rounding.cpp) target_link_libraries(ds4v_linear_rounding PRIVATE ds4v_vision) if(DS4V_VISION_HIP) @@ -57,6 +59,7 @@ if(DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_rounding PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_source PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_unbiased_source PRIVATE DS4V_VISION_HIP) + target_compile_definitions(ds4v_norm_source PRIVATE DS4V_VISION_HIP) endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) @@ -65,3 +68,6 @@ target_link_libraries(ds4v_linear_contract PRIVATE ds4v_vision) enable_testing() add_test(NAME ds4v_linear_contract COMMAND ds4v_linear_contract) add_test(NAME ds4v_vision_geometry COMMAND ds4v_vision_geometry) +add_executable(ds4v_norm_contract norm_contract.cpp) +target_link_libraries(ds4v_norm_contract PRIVATE ds4v_vision) +add_test(NAME ds4v_norm_contract COMMAND ds4v_norm_contract) diff --git a/server/tools/ds4v_vision/linear_contract.cpp b/server/tools/ds4v_vision/linear_contract.cpp index 30839a95c..dd7451b02 100644 --- a/server/tools/ds4v_vision/linear_contract.cpp +++ b/server/tools/ds4v_vision/linear_contract.cpp @@ -7,7 +7,7 @@ #include #include static void check(bool ok,const char *why) { if(!ok)throw std::runtime_error(why); } -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_COUNT==106,"operation ABI changed"); +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107,"operation ABI changed"); static void rejected(int mode,bool with_bias=true) { pid_t pid=fork(); check(pid>=0,"fork failed"); if(pid==0) { diff --git a/server/tools/ds4v_vision/linear_source.cpp b/server/tools/ds4v_vision/linear_source.cpp index 597f28711..52bcabd60 100644 --- a/server/tools/ds4v_vision/linear_source.cpp +++ b/server/tools/ds4v_vision/linear_source.cpp @@ -14,7 +14,7 @@ #include #include #include -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_COUNT==106,"operation ABI changed unexpectedly"); +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107,"operation ABI changed unexpectedly"); static void check(bool b,const char *s) { if(!b) throw std::runtime_error(s); } static uint32_t bits(float v) { uint32_t b; std::memcpy(&b,&v,4); return b; } static std::vector load(const std::filesystem::path&p,size_t n) { diff --git a/server/tools/ds4v_vision/linear_unbiased_source.cpp b/server/tools/ds4v_vision/linear_unbiased_source.cpp index ea59de0f9..1ec1b2192 100644 --- a/server/tools/ds4v_vision/linear_unbiased_source.cpp +++ b/server/tools/ds4v_vision/linear_unbiased_source.cpp @@ -18,7 +18,7 @@ #include #include -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_COUNT==106, +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107, "operation ABI changed unexpectedly"); static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } static size_t product(size_t a,size_t b) { diff --git a/server/tools/ds4v_vision/norm_contract.cpp b/server/tools/ds4v_vision/norm_contract.cpp new file mode 100644 index 000000000..a17f5eee2 --- /dev/null +++ b/server/tools/ds4v_vision/norm_contract.cpp @@ -0,0 +1,63 @@ +#include "deepseek4/deepseek4_vision.h" +#include "ggml-cpu.h" +#include +#include +#include +#include +#include +#include +#include +#include + +static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && + GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107,"operation ABI changed"); +static void rejected(int mode) { + const pid_t pid=fork(); check(pid>=0,"fork failed"); + if(pid==0) { + auto c=ggml_init({1024*1024,nullptr,true}); + auto x=ggml_new_tensor_2d(c,mode==1?GGML_TYPE_BF16:GGML_TYPE_F32,mode==2?512:1024,mode==3?15:16); + if(mode==0) x=nullptr; + if(mode==4) x=ggml_transpose(c,ggml_new_tensor_2d(c,GGML_TYPE_F32,16,1024)); + if(mode==5) x=ggml_new_tensor_3d(c,GGML_TYPE_F32,1024,16,2); + if(mode==9) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,1024,int64_t(INT_MAX)/1024+1); + const float eps=mode==6?-1.f:mode==7?std::numeric_limits::infinity(): + mode==8?std::numeric_limits::quiet_NaN():1e-6f; + (void)ggml_rms_norm_vision_f32(c,x,eps); + _exit(0); + } + int status=0; check(waitpid(pid,&status,0)==pid,"wait failed"); + check(WIFSIGNALED(status) && WTERMSIG(status)==SIGABRT,"invalid source-order norm was accepted"); +} +int main() { + auto backend=ggml_backend_cpu_init(); auto c=ggml_init({1024*1024,nullptr,true}); + try { + check(backend && c,"initialization failed"); + check(!dflash::vision::detail::hip_norm_capable(nullptr) && !dflash::vision::detail::hip_norm_capable(backend), + "CPU/null advertised HIP source normalization"); + check(dflash::vision::detail::hip_norm_launches(nullptr)==0 && dflash::vision::detail::hip_norm_launches(backend)==0, + "CPU/null reported HIP normalization launches"); + for(int rows:{16,782,2562}) { + auto x=ggml_new_tensor_2d(c,GGML_TYPE_F32,1024,rows); + auto y=ggml_rms_norm_vision_f32(c,x,1e-6f); + check(y->op==GGML_OP_RMS_NORM_VISION_F32 && y->type==GGML_TYPE_F32 && + y->src[0]==x && y->src[1]==nullptr && y->ne[0]==1024 && y->ne[1]==rows, + "source normalization constructor contract changed"); + check(!ggml_backend_supports_op(backend,y),"CPU advertised source-order HIP operation"); + } + for(int rows:{1,16,782}) { + auto x=ggml_new_tensor_2d(c,GGML_TYPE_F32,1024,rows); + for(auto selected:{static_cast(nullptr),backend}) { + auto y=dflash::vision::detail::rms_norm(c,x,1e-6f,selected); + check(y->op==GGML_OP_RMS_NORM && y->src[0]==x && y->type==GGML_TYPE_F32, + "generic CPU/null normalization path changed"); + } + } + for(int mode=0;mode<10;++mode) rejected(mode); + ggml_free(c); ggml_backend_free(backend); + std::cout<<"PASS: explicit HIP norm contract, CPU/null preservation, invalid input rejection\n"; + return 0; + } catch(const std::exception &e) { + std::cerr< +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs=std::filesystem; +constexpr int columns=1024,source_rows=782; +static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } +static uint32_t bits(float value) { uint32_t out; std::memcpy(&out,&value,4); return out; } +static std::vector read(const fs::path &path,size_t count,bool bf16=false) { + check(fs::is_regular_file(path) && fs::file_size(path)==count*4,"wrong fixture size"); + std::vector values(count); std::ifstream file(path,std::ios::binary); + file.read(reinterpret_cast(values.data()),values.size()*4); check(bool(file),"fixture read failed"); + for(float value:values) check(std::isfinite(value) && (!bf16 || !(bits(value)&65535)),"invalid fixture values"); + return values; +} +static std::vector tile(const std::vector &source,int rows) { + check(source.size()==size_t(source_rows)*columns,"wrong fixed source shape"); + std::vector result(size_t(rows)*columns); + for(int row=0;row packed; for(float x:weights) packed.push_back({uint16_t(bits(x)>>16)}); + std::vector> expected; + for(const char *name:{"scaled","weighted","output"}) expected.push_back(tile(read(fixtures/(std::string(name)+".f32"),size_t(source_rows)*columns),rows)); + Resources owner; +#ifdef DS4V_VISION_HIP + check(ggml_backend_cuda_get_device_count()==1,"exactly one visible GPU required"); + owner.backend=ggml_backend_cuda_init(0); +#endif + check(owner.backend && dflash::vision::detail::hip_norm_capable(owner.backend),"source-order HIP normalization unavailable"); + std::cout<<"backend="<op==GGML_OP_RMS_NORM_VISION_F32; + check(node->op!=GGML_OP_RMS_NORM,"generic norm appeared in source graph"); + } + check(explicit_ops==1,"one explicit normalization operation required"); + owner.allocator=ggml_gallocr_new(ggml_backend_get_default_buffer_type(owner.backend)); + check(owner.allocator,"allocator unavailable"); size_t arena=0; + ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&arena); + check(arena<=128ULL*1024*1024,"normalization graph exceeds 128 MiB"); + check(ggml_gallocr_reserve(owner.allocator,graph) && ggml_gallocr_alloc_graph(owner.allocator,graph),"allocation failed"); + check(ggml_gallocr_get_buffer_size(owner.allocator,0)<=arena,"allocation exceeds reservation"); + ggml_backend_tensor_set(x,inputs.data(),0,inputs.size()*4); + ggml_backend_tensor_set(w,packed.data(),0,packed.size()*2); + const size_t before=dflash::vision::detail::hip_norm_launches(owner.backend); + check(ggml_backend_graph_compute(owner.backend,graph)==GGML_STATUS_SUCCESS,"normalization graph failed"); + ggml_backend_synchronize(owner.backend); + const size_t after=dflash::vision::detail::hip_norm_launches(owner.backend); + check(after>=before && after-before==1,"actual source normalization dispatch mismatch"); + check(dflash::vision::detail::hip_bias_launches(owner.backend)==0,"normalization unexpectedly submitted Lt"); + fs::create_directory(out); size_t total=0; + const char *names[]={"scaled","weighted","output"}; + for(int field=0;field<3;++field) { + std::vector actual(inputs.size()); + ggml_backend_tensor_get(outputs[field],actual.data(),0,actual.size()*4); + size_t mismatches=0; + for(size_t i=0;i(actual.data()),actual.size()*4); check(bool(file),"output write failed"); + std::cout< Date: Sat, 5 Sep 2026 03:39:35 -0400 Subject: [PATCH 061/123] Verify both HIP vision dispatch counters in source probes --- server/tools/ds4v_vision/linear_source.cpp | 4 +++- server/tools/ds4v_vision/linear_unbiased_source.cpp | 3 +++ server/tools/ds4v_vision/norm_source.cpp | 5 +++-- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/server/tools/ds4v_vision/linear_source.cpp b/server/tools/ds4v_vision/linear_source.cpp index 52bcabd60..038b56ed5 100644 --- a/server/tools/ds4v_vision/linear_source.cpp +++ b/server/tools/ds4v_vision/linear_source.cpp @@ -85,7 +85,9 @@ int main(int argc,char**argv) { check(ggml_backend_graph_compute(backend,g)==GGML_STATUS_SUCCESS,"compute failed"); const size_t launches=dflash::vision::detail::hip_bias_launches(backend)-before; check(launches==ops,"actual Lt launch count differs from explicit op count"); - std::cout<<"fused_graph_ops="< actual(elements); ggml_backend_tensor_get(y,actual.data(),0,output_bytes); size_t mismatches=0; double max_abs=0; @@ -136,6 +138,7 @@ int main(int argc,char **argv) { file.write(reinterpret_cast(actual.data()),std::streamsize(output_bytes)); file.close(); check(bool(file),"output write failed"); std::cout<<"explicit_unbiased_ops="<(actual.data()),actual.size()*4); check(bool(file),"output write failed"); std::cout< Date: Sat, 5 Sep 2026 04:43:27 -0400 Subject: [PATCH 062/123] Match HIP vision rotary tables to source GPU math --- .../llama.cpp/ggml/src/ggml-cuda/common.cuh | 1 + .../llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu | 18 +++ .../ggml/src/ggml-cuda/vision-rotary.cu | 117 ++++++++++++++++++ .../ggml/src/ggml-cuda/vision-rotary.cuh | 9 ++ server/src/deepseek4/deepseek4_vision.cpp | 35 +++++- server/src/deepseek4/deepseek4_vision.h | 5 +- server/tools/ds4v_vision/CMakeLists.txt | 6 + server/tools/ds4v_vision/probe.cpp | 4 + server/tools/ds4v_vision/rotary_contract.cpp | 33 +++++ server/tools/ds4v_vision/rotary_source.cpp | 69 +++++++++++ 10 files changed, 294 insertions(+), 3 deletions(-) create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh create mode 100644 server/tools/ds4v_vision/rotary_contract.cpp create mode 100644 server/tools/ds4v_vision/rotary_source.cpp diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh index e6370705c..13375a5c6 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh @@ -1443,6 +1443,7 @@ struct ggml_backend_cuda_context { cudaEvent_t vision_bias_event = nullptr; size_t vision_bias_launches = 0; size_t vision_norm_launches = 0; + size_t vision_rotary_launches = 0; // successful synchronous table calls (five kernels each) #endif int curr_stream_no = 0; bool low_priority_streams = false; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu index 839f01f7d..b70b0e793 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu @@ -4,6 +4,7 @@ #include "ggml-cuda/common.cuh" #include "ggml-cuda/vision-bias.cuh" +#include "ggml-cuda/vision-rotary.cuh" #include "ggml-cuda/acc.cuh" #include "ggml-cuda/add-id.cuh" #include "ggml-cuda/arange.cuh" @@ -6680,6 +6681,20 @@ static size_t ggml_backend_hip_vision_norm_f32_launches(ggml_backend_t backend) return backend && ggml_backend_is_cuda(backend) ? static_cast(backend->context)->vision_norm_launches : 0; } +static bool ggml_backend_hip_vision_rotary_f32_capable(ggml_backend_t backend) { + return ggml_backend_hip_vision_norm_f32_capable(backend); +} +static bool ggml_backend_hip_vision_rotary_f32(ggml_backend_t backend, int64_t height, int64_t width, + float * cosine, float * sine) { + return ggml_backend_hip_vision_rotary_f32_capable(backend) && + ggml_hip_vision_rotary(*static_cast(backend->context), + height, width, cosine, sine); +} +static size_t ggml_backend_hip_vision_rotary_f32_launches(ggml_backend_t backend) { + return backend && ggml_backend_is_cuda(backend) ? + static_cast(backend->context)->vision_rotary_launches : 0; +} + #endif static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, const char * name) { @@ -6688,6 +6703,9 @@ static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, con if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_launches")==0) return (void *)ggml_backend_hip_vision_bias_bf16_launches; if (strcmp(name,"ggml_backend_hip_vision_norm_f32_capable")==0) return (void *)ggml_backend_hip_vision_norm_f32_capable; if (strcmp(name,"ggml_backend_hip_vision_norm_f32_launches")==0) return (void *)ggml_backend_hip_vision_norm_f32_launches; + if (strcmp(name,"ggml_backend_hip_vision_rotary_f32_capable")==0) return (void *)ggml_backend_hip_vision_rotary_f32_capable; + if (strcmp(name,"ggml_backend_hip_vision_rotary_f32")==0) return (void *)ggml_backend_hip_vision_rotary_f32; + if (strcmp(name,"ggml_backend_hip_vision_rotary_f32_launches")==0) return (void *)ggml_backend_hip_vision_rotary_f32_launches; #endif GGML_UNUSED(reg); if (strcmp(name, "ggml_backend_comm_init") == 0) { diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu new file mode 100644 index 000000000..16dd114bb --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu @@ -0,0 +1,117 @@ +#include "vision-rotary.cuh" +#include "norm.cuh" +#include +#include + +#if defined(GGML_USE_HIP) +// PyTorch 3d3aa833db84eed6b7f5595cb5f162c2f78300a4, aten/src/ATen/native/cuda/: +// Pow.cuh uses global ::pow on Linux; UnaryFractionKernels.cu uses float 1/a; +// UnaryGeometric{Cos,Sin}Kernel.cu use global ::cos and ::sin. The five kernels +// preserve eager F32 materialization between power, reciprocal, angle and trig. +static __global__ void vision_rotary_power(float * output, float base) { + static_assert(sizeof(decltype(::pow(0.0f, 0.0f))) == sizeof(float), "source pow overload changed"); + const int j = threadIdx.x; + if (j < 16) { + output[j] = ::pow(base, float(2*j)*(1.0f/32.0f)); + } +} + +static __global__ void vision_rotary_inverse(const float * power, float * inverse) { + const int j = threadIdx.x; + if (j < 16) { + inverse[j] = 1.0f/power[j]; + } +} + +static __global__ void vision_rotary_angle(const float * inverse, float * angle, int count, int width) { + const int i = blockIdx.x*blockDim.x + threadIdx.x; + if (i < count) { + const int row = i/32, j = i%32; + angle[i] = float(j < 16 ? row/width : row%width)*inverse[j%16]; + } +} + +static __global__ void vision_rotary_cos(const float * angle, float * cosine, int count) { + static_assert(sizeof(decltype(::cos(0.0f))) == sizeof(float), "source cos overload changed"); + const int i = blockIdx.x*blockDim.x + threadIdx.x; + if (i < count) { + cosine[i] = ::cos(angle[i]); + } +} + +static __global__ void vision_rotary_sin(const float * angle, float * sine, int count) { + static_assert(sizeof(decltype(::sin(0.0f))) == sizeof(float), "source sin overload changed"); + const int i = blockIdx.x*blockDim.x + threadIdx.x; + if (i < count) { + sine[i] = ::sin(angle[i]); + } +} + +namespace { +struct rotary_temporary { + float * data = nullptr; + cudaStream_t stream; + + rotary_temporary(size_t bytes, cudaStream_t stream) : stream(stream) { + CUDA_CHECK(cudaMalloc(reinterpret_cast(&data), bytes)); + } + ~rotary_temporary() { + CUDA_CHECK(cudaStreamSynchronize(stream)); + CUDA_CHECK(cudaFree(data)); + } + rotary_temporary(const rotary_temporary &) = delete; + rotary_temporary & operator=(const rotary_temporary &) = delete; +}; +} // namespace + +bool ggml_hip_vision_rotary(ggml_backend_cuda_context & ctx, int64_t height, int64_t width, + float * cosine, float * sine) { + if (!ggml_hip_vision_norm_capable(ctx.device) || !cosine || !sine || cosine == sine || + height <= 0 || width <= 0 || height > 1152 || width > 1152) { + return false; + } + const size_t h = static_cast(height), w = static_cast(width); + constexpr size_t cap = 128ULL*1024*1024; + constexpr size_t workspace = 76ULL*1024*1024; + constexpr size_t fixed_bytes = 2*16*sizeof(float); + constexpr size_t bytes_per_row = 3*32*sizeof(float); + if (h > std::numeric_limits::max()/w) { + return false; + } + const size_t rows = h*w; + if (rows > (cap - workspace - fixed_bytes)/bytes_per_row) { + return false; + } + const size_t bytes = fixed_bytes + rows*bytes_per_row; + const int count = static_cast(rows*32); + ggml_cuda_set_device(ctx.device); + const auto stream = ctx.stream(); + cudaStreamCaptureStatus capture; + CUDA_CHECK(cudaStreamIsCapturing(stream, &capture)); + if (capture != cudaStreamCaptureStatusNone) { + return false; + } + { + rotary_temporary temporary(bytes, stream); + float * power = temporary.data; + float * inverse = power + 16; + float * angle = inverse + 16; + float * cos = angle + count; + float * sin = cos + count; + vision_rotary_power<<<1, 32, 0, stream>>>(power, 10000.0f); + CUDA_CHECK(cudaGetLastError()); + vision_rotary_inverse<<<1, 32, 0, stream>>>(power, inverse); + CUDA_CHECK(cudaGetLastError()); + vision_rotary_angle<<<(count + 255)/256, 256, 0, stream>>>(inverse, angle, count, int(width)); + CUDA_CHECK(cudaGetLastError()); + vision_rotary_cos<<<(count + 255)/256, 256, 0, stream>>>(angle, cos, count); + CUDA_CHECK(cudaGetLastError()); + vision_rotary_sin<<<(count + 255)/256, 256, 0, stream>>>(angle, sin, count); + CUDA_CHECK(cudaGetLastError()); + CUDA_CHECK(cudaMemcpyAsync(cosine, cos, count*sizeof(float), cudaMemcpyDeviceToHost, stream)); + CUDA_CHECK(cudaMemcpyAsync(sine, sin, count*sizeof(float), cudaMemcpyDeviceToHost, stream)); + } + ++ctx.vision_rotary_launches; + return true; +} +#endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh new file mode 100644 index 000000000..2f11acbc7 --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh @@ -0,0 +1,9 @@ +#pragma once +#include "common.cuh" + +#if defined(GGML_USE_HIP) +// Caller supplies two distinct host buffers of height*width*32 floats each. +// Success fills both buffers synchronously and records one complete table call. +bool ggml_hip_vision_rotary(ggml_backend_cuda_context & ctx, int64_t height, int64_t width, + float * cosine, float * sine); +#endif diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 0183a5917..6b0a3f6d8 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -121,6 +121,17 @@ static size_t hip_size_query(ggml_backend_t backend,const char * name) { size_t hip_bias_workspace(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_bias_bf16_workspace"); } size_t hip_bias_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_bias_bf16_launches"); } size_t hip_norm_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_norm_f32_launches"); } +size_t hip_rotary_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_rotary_f32_launches"); } +bool hip_rotary_capable(ggml_backend_t backend) { + if(!backend) return false; + auto device=ggml_backend_get_device(backend); + if(!device) return false; + auto reg=ggml_backend_dev_backend_reg(device); + if(!reg) return false; + using Query=bool (*)(ggml_backend_t); + auto query=reinterpret_cast(ggml_backend_reg_get_proc_address(reg,"ggml_backend_hip_vision_rotary_f32_capable")); + return query && query(backend); +} bool hip_norm_capable(ggml_backend_t backend) { if(!backend) return false; auto device=ggml_backend_get_device(backend); @@ -156,8 +167,23 @@ Tensor * linear(ggml_context * c,Tensor * weight,Tensor * input,Tensor * bias,bo if(bias) y=ggml_add(c,y,ggml_cast(c,bias,GGML_TYPE_F32)); return rounded(c,y); } -void rotary_tables(PatchGrid grid,std::vector & cosine,std::vector & sine) { +void rotary_tables(PatchGrid grid,std::vector & cosine,std::vector & sine,ggml_backend_t backend) { + require(grid.height>0 && grid.width>0 && grid.height<=1152 && grid.width<=1152,"invalid rotary grid"); const int n=grid.height*grid.width; + if(hip_bias_workspace(backend)) { + const size_t temporary=(32+size_t(n)*32*3)*sizeof(float); + constexpr size_t limit=128ULL*1024*1024; + const size_t external=hip_bias_workspace(backend); + require(external<=limit && temporary<=limit-external,"HIP rotary temporary storage exceeds 128 MiB bound"); + require(hip_rotary_capable(backend),"HIP source-order vision rotary tables unavailable; fallback forbidden"); + using Fill=bool (*)(ggml_backend_t,int64_t,int64_t,float *,float *); + auto reg=ggml_backend_dev_backend_reg(ggml_backend_get_device(backend)); + auto fill=reinterpret_cast(ggml_backend_reg_get_proc_address(reg,"ggml_backend_hip_vision_rotary_f32")); + require(fill!=nullptr,"HIP source-order vision rotary table function unavailable"); + cosine.resize(size_t(n)*32); sine.resize(size_t(n)*32); + require(fill(backend,grid.height,grid.width,cosine.data(),sine.data()),"HIP source-order vision rotary table preparation failed"); + return; + } cosine.resize(size_t(n)*32); sine.resize(size_t(n)*32); for(int i=0;i & patches,PatchGrid grid,Vis x=impl_->execute(g,input,patches,y,observer); } std::vector cos_values,sin_values; - detail::rotary_tables(grid,cos_values,sin_values); + // Patch output is already on the host. Release its reusable device arena + // before the independently bounded rotary temporary allocation. + if(detail::hip_bias_workspace(impl_->backend)) release_scratch(); + detail::rotary_tables(grid,cos_values,sin_values,impl_->backend); for(int i=0;i<32;++i) { Graph g; const auto p="vision.blocks."+std::to_string(i); diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index 5f0458df8..f212ecf46 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -60,11 +60,14 @@ size_t hip_bias_workspace(ggml_backend_t); size_t hip_bias_launches(ggml_backend_t); bool hip_norm_capable(ggml_backend_t); size_t hip_norm_launches(ggml_backend_t); +bool hip_rotary_capable(ggml_backend_t); +size_t hip_rotary_launches(ggml_backend_t); // F32 values exactly representing BF16 inputs; HIP source order needs at least 16 rows. ggml_tensor * rms_norm(ggml_context *, ggml_tensor * input, float epsilon, ggml_backend_t); ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias, bool preserve_biased_product, ggml_backend_t backend = nullptr); -void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine); +void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine, + ggml_backend_t backend = nullptr); ggml_tensor * rotate(ggml_context *, ggml_tensor *, ggml_tensor * cosine, ggml_tensor * sine); ggml_tensor * attention(ggml_context *, ggml_tensor * q, ggml_tensor * k, ggml_tensor * v); ggml_tensor * unfold(ggml_context *, ggml_tensor *, PatchGrid, int channels); diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 6558ff286..189a58e7a 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -52,6 +52,8 @@ add_executable(ds4v_linear_unbiased_source linear_unbiased_source.cpp) target_link_libraries(ds4v_linear_unbiased_source PRIVATE ds4v_vision) add_executable(ds4v_norm_source norm_source.cpp) target_link_libraries(ds4v_norm_source PRIVATE ds4v_vision) +add_executable(ds4v_rotary_source rotary_source.cpp) +target_link_libraries(ds4v_rotary_source PRIVATE ds4v_vision) add_executable(ds4v_linear_rounding linear_rounding.cpp) target_link_libraries(ds4v_linear_rounding PRIVATE ds4v_vision) if(DS4V_VISION_HIP) @@ -60,6 +62,7 @@ if(DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_source PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_unbiased_source PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_norm_source PRIVATE DS4V_VISION_HIP) + target_compile_definitions(ds4v_rotary_source PRIVATE DS4V_VISION_HIP) endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) @@ -71,3 +74,6 @@ add_test(NAME ds4v_vision_geometry COMMAND ds4v_vision_geometry) add_executable(ds4v_norm_contract norm_contract.cpp) target_link_libraries(ds4v_norm_contract PRIVATE ds4v_vision) add_test(NAME ds4v_norm_contract COMMAND ds4v_norm_contract) +add_executable(ds4v_rotary_contract rotary_contract.cpp) +target_link_libraries(ds4v_rotary_contract PRIVATE ds4v_vision) +add_test(NAME ds4v_rotary_contract COMMAND ds4v_rotary_contract) diff --git a/server/tools/ds4v_vision/probe.cpp b/server/tools/ds4v_vision/probe.cpp index defd456ba..0cf671048 100644 --- a/server/tools/ds4v_vision/probe.cpp +++ b/server/tools/ds4v_vision/probe.cpp @@ -77,17 +77,21 @@ int main(int argc,char ** argv) { VisionOutput output; const auto lt_before=detail::hip_bias_launches(backend); const auto norm_before=detail::hip_norm_launches(backend); + const auto rotary_before=detail::hip_rotary_launches(backend); auto started=std::chrono::steady_clock::now(); if(!runtime.encode(patches,grid,output,error,true,observer)) throw std::runtime_error(error); const auto lt_launches=detail::hip_bias_launches(backend)-lt_before; const auto norm_launches=detail::hip_norm_launches(backend)-norm_before; + const auto rotary_launches=detail::hip_rotary_launches(backend)-rotary_before; const auto external=detail::hip_bias_workspace(backend); const bool hip_requested=device=="hip:0" || device=="hip:1"; if((external!=0)!=hip_requested) throw std::runtime_error("requested HIP backend lacks BF16 linear capability"); if(lt_launches!=(hip_requested ? 131u : 0u)) throw std::runtime_error("unexpected actual HIP BF16 linear dispatch count"); if(norm_launches!=(hip_requested ? 65u : 0u)) throw std::runtime_error("unexpected actual HIP vision normalization dispatch count"); + if(rotary_launches!=(hip_requested ? 1u : 0u)) throw std::runtime_error("unexpected actual HIP vision rotary table preparation count"); std::cout<<"hip_vision_linear_launches="< +#include + +using namespace dflash::vision; +static void check(bool ok,const char *message) { if(!ok) throw std::runtime_error(message); } +int main() { + auto backend=ggml_backend_cpu_init(); + try { + check(backend,"CPU backend unavailable"); + check(!detail::hip_rotary_capable(nullptr) && !detail::hip_rotary_capable(backend),"CPU reports HIP rotary capability"); + check(detail::hip_rotary_launches(nullptr)==0 && detail::hip_rotary_launches(backend)==0,"CPU reports HIP rotary dispatch"); + std::vector generic_cos,generic_sin,cpu_cos,cpu_sin; + detail::rotary_tables({23,34},generic_cos,generic_sin); + detail::rotary_tables({23,34},cpu_cos,cpu_sin,backend); + check(generic_cos==cpu_cos && generic_sin==cpu_sin,"CPU backend changed generic rotary tables"); + check(cpu_cos.size()==782*32 && cpu_sin.size()==cpu_cos.size(),"wrong rotary table size"); + for(int i=0;i<32;++i) check(cpu_cos[i]==1.f && cpu_sin[i]==0.f,"zero-position rotary changed"); + for(PatchGrid grid:std::vector{{0,1},{1,0},{-1,1},{1,-1},{1153,1},{1,1153}}) { + bool rejected=false; + try { detail::rotary_tables(grid,cpu_cos,cpu_sin,backend); } + catch(const std::runtime_error &) { rejected=true; } + check(rejected,"invalid rotary grid accepted"); + } + ggml_backend_free(backend); + std::cout<<"PASS: rotary CPU portability and grid contract\n"; + return 0; + } catch(const std::exception &error) { + if(backend) ggml_backend_free(backend); + std::cerr< +#include +#include +#include +#include +#include +#include + +using namespace dflash::vision; +namespace fs=std::filesystem; +static void check(bool ok,const char *message) { if(!ok) throw std::runtime_error(message); } +static std::vector read(const fs::path &path) { + constexpr size_t count=782*32; + check(fs::is_regular_file(path) && fs::file_size(path)==count*4,"invalid rotary source file"); + std::vector values(count); std::ifstream f(path,std::ios::binary); + f.read(reinterpret_cast(values.data()),count*4); check(bool(f),"rotary fixture read failed"); + for(float value:values) check(std::isfinite(value),"nonfinite rotary source"); + return values; +} +static void save(const fs::path &path,const std::vector &values) { + std::ofstream f(path,std::ios::binary); + f.write(reinterpret_cast(values.data()),values.size()*4); + check(bool(f),"rotary output write failed"); +} +int main(int argc,char **argv) { + std::cout<(ggml_backend_reg_get_proc_address(reg,"ggml_backend_hip_vision_rotary_f32")); + check(fill!=nullptr,"HIP rotary fill unavailable"); + float a=123.f,b=456.f; + check(!fill(nullptr,23,34,&a,&b),"null backend accepted"); + for(PatchGrid grid:std::vector{{0,1},{1,0},{-1,1},{1,-1},{1153,1},{1,1153},{1152,1152}}) + check(!fill(backend,grid.height,grid.width,&a,&b),"invalid or oversized direct rotary grid accepted"); + check(!fill(backend,23,34,nullptr,&b) && !fill(backend,23,34,&a,nullptr),"null rotary output accepted"); + check(!fill(backend,23,34,&a,&a),"aliased rotary outputs accepted"); + check(a==123.f && b==456.f && detail::hip_rotary_launches(backend)==0,"rejected rotary call had side effects"); + std::vector cosine,sine; + detail::rotary_tables({23,34},cosine,sine,backend); + check(cosine.size()==expected_cos.size() && sine.size()==expected_sin.size(),"rotary output shape changed"); + check(std::memcmp(cosine.data(),expected_cos.data(),cosine.size()*4)==0 + && std::memcmp(sine.data(),expected_sin.data(),sine.size()*4)==0,"rotary source differs bitwise"); + check(detail::hip_rotary_launches(backend)==1,"wrong rotary preparation count"); + check(detail::hip_bias_launches(backend)==0 && detail::hip_norm_launches(backend)==0,"unexpected graph operation"); + fs::create_directory(out); save(out/"cos.f32",cosine); save(out/"sin.f32",sine); + std::cout<<"source_bitwise_mismatches=0 actual_rotary_launches=1 actual_lt_launches=0 actual_norm_launches=0\n"; + std::cout<<"PASS: HIP rotary tables match original source\n"; + ggml_backend_free(backend); return 0; + } catch(const std::exception &error) { + if(backend) ggml_backend_free(backend); + std::cerr<<"FAIL: "< Date: Sat, 5 Sep 2026 05:02:34 -0400 Subject: [PATCH 063/123] Match HIP vision softmax and attention products to source order --- server/deps/llama.cpp/ggml/include/ggml-rpc.h | 4 +- server/deps/llama.cpp/ggml/include/ggml.h | 16 + .../llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c | 6 + .../llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp | 5 +- .../llama.cpp/ggml/src/ggml-cuda/common.cuh | 4 +- .../llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu | 51 ++- .../llama.cpp/ggml/src/ggml-cuda/vision-av.cu | 74 ++++ .../ggml/src/ggml-cuda/vision-av.cuh | 7 + .../ggml/src/ggml-cuda/vision-bias.cu | 28 +- .../ggml/src/ggml-cuda/vision-bias.cuh | 4 + .../src/ggml-cuda/vision-softmax-kernels.cuh | 318 ++++++++++++++++++ .../ggml/src/ggml-cuda/vision-softmax.cu | 52 +++ .../ggml/src/ggml-cuda/vision-softmax.cuh | 8 + .../ggml/src/ggml-hip/CMakeLists.txt | 5 + .../llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp | 3 +- server/deps/llama.cpp/ggml/src/ggml.c | 49 ++- server/src/deepseek4/deepseek4_vision.cpp | 47 ++- server/src/deepseek4/deepseek4_vision.h | 7 +- server/tools/ds4v_vision/CMakeLists.txt | 6 + .../tools/ds4v_vision/attention_contract.cpp | 87 +++++ server/tools/ds4v_vision/attention_source.cpp | 165 +++++++++ server/tools/ds4v_vision/linear_contract.cpp | 2 +- server/tools/ds4v_vision/linear_source.cpp | 2 +- .../ds4v_vision/linear_unbiased_source.cpp | 2 +- server/tools/ds4v_vision/norm_contract.cpp | 2 +- server/tools/ds4v_vision/probe.cpp | 8 + 26 files changed, 933 insertions(+), 29 deletions(-) create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax-kernels.cuh create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu create mode 100644 server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh create mode 100644 server/tools/ds4v_vision/attention_contract.cpp create mode 100644 server/tools/ds4v_vision/attention_source.cpp diff --git a/server/deps/llama.cpp/ggml/include/ggml-rpc.h b/server/deps/llama.cpp/ggml/include/ggml-rpc.h index 31ad769c2..20dc8a357 100644 --- a/server/deps/llama.cpp/ggml/include/ggml-rpc.h +++ b/server/deps/llama.cpp/ggml/include/ggml-rpc.h @@ -8,10 +8,10 @@ extern "C" { #define RPC_PROTO_MAJOR_VERSION 3 #define RPC_PROTO_MINOR_VERSION 6 -#define RPC_PROTO_PATCH_VERSION 7 +#define RPC_PROTO_PATCH_VERSION 8 #ifdef __cplusplus -static_assert(GGML_OP_COUNT == 107, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION"); +static_assert(GGML_OP_COUNT == 109, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION"); #endif #define GGML_RPC_MAX_SERVERS 16 diff --git a/server/deps/llama.cpp/ggml/include/ggml.h b/server/deps/llama.cpp/ggml/include/ggml.h index f39452193..7ccf86ae4 100644 --- a/server/deps/llama.cpp/ggml/include/ggml.h +++ b/server/deps/llama.cpp/ggml/include/ggml.h @@ -619,6 +619,8 @@ extern "C" { GGML_OP_MUL_MAT_BIAS_BF16, // explicit HIP-only DS4V fused bias GGML_OP_RMS_NORM_VISION_F32, // inference-only HIP source-order DS4V normalization + GGML_OP_SOFT_MAX_VISION_F32, // inference-only HIP source-order DS4V softmax + GGML_OP_MUL_MAT_VISION_AV_F32, // inference-only HIP source-layout DS4V AV GGML_OP_COUNT, }; @@ -1429,6 +1431,20 @@ extern "C" { struct ggml_tensor * a, float eps); + // Inference-only HIP wave32 operations; no CPU/RPC/backward implementation. + // scores: contiguous F32, width 16..4096, positive rows, total bytes <= INT_MAX. + GGML_API struct ggml_tensor * ggml_soft_max_vision_f32( + struct ggml_context * ctx, + struct ggml_tensor * scores); + + // v: contiguous F32 [64,16,N,1], probabilities: contiguous F32 [N,N,16,1]. + // N is 16..4096; result is F32 [64,N,16,1]. + GGML_API struct ggml_tensor * ggml_mul_mat_vision_av_f32( + struct ggml_context * ctx, + struct ggml_tensor * v, + struct ggml_tensor * probabilities); + + // group normalize along ne0*ne1*n_groups // used in stable-diffusion GGML_API struct ggml_tensor * ggml_group_norm( diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c index d455ff1d4..4b53a8390 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c @@ -2200,6 +2200,9 @@ static void ggml_compute_forward(struct ggml_compute_params * params, struct ggm { GGML_ABORT("GGML_OP_MUL_MAT_BIAS_BF16 requires the HIP Lt backend"); } case GGML_OP_RMS_NORM_VISION_F32: { GGML_ABORT("GGML_OP_RMS_NORM_VISION_F32 requires the HIP wave32 backend"); } + case GGML_OP_SOFT_MAX_VISION_F32: + case GGML_OP_MUL_MAT_VISION_AV_F32: + { GGML_ABORT("explicit vision attention requires the HIP wave32 backend"); } case GGML_OP_PAGED_ATTN: { GGML_ABORT("GGML_OP_PAGED_ATTN is only supported on the CUDA backend"); @@ -2604,6 +2607,9 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { } break; case GGML_OP_RMS_NORM_VISION_F32: GGML_ABORT("GGML_OP_RMS_NORM_VISION_F32 cannot be planned on CPU"); + case GGML_OP_SOFT_MAX_VISION_F32: + case GGML_OP_MUL_MAT_VISION_AV_F32: + GGML_ABORT("explicit vision attention cannot be planned on CPU"); case GGML_OP_RWKV_WKV6: case GGML_OP_GATED_LINEAR_ATTN: case GGML_OP_RWKV_WKV7: diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp index 9f784cde6..291c630d6 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.cpp @@ -422,7 +422,8 @@ static ggml_backend_buffer_t ggml_backend_cpu_device_buffer_from_host_ptr(ggml_b static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const struct ggml_tensor * op) { // No extra-buffer handler may accidentally advertise this HIP-only op. - if (op->op == GGML_OP_MUL_MAT_BIAS_BF16 || op->op == GGML_OP_RMS_NORM_VISION_F32) return false; + if (op->op == GGML_OP_MUL_MAT_BIAS_BF16 || op->op == GGML_OP_RMS_NORM_VISION_F32 || + op->op == GGML_OP_SOFT_MAX_VISION_F32 || op->op == GGML_OP_MUL_MAT_VISION_AV_F32) return false; const struct ggml_tensor * src0 = op->src[0]; const struct ggml_tensor * src1 = op->src[1]; @@ -475,6 +476,8 @@ static bool ggml_backend_cpu_device_supports_op(ggml_backend_dev_t dev, const st case GGML_OP_PAGED_ATTN: case GGML_OP_MUL_MAT_BIAS_BF16: case GGML_OP_RMS_NORM_VISION_F32: + case GGML_OP_SOFT_MAX_VISION_F32: + case GGML_OP_MUL_MAT_VISION_AV_F32: return false; case GGML_OP_SSM_CONV: // Every nonzero mode is a dflash CUDA/HIP extension (SpecLA, diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh index 13375a5c6..cce0206d8 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh @@ -1439,10 +1439,12 @@ struct ggml_backend_cuda_context { #if defined(GGML_USE_HIP) hipblasLtHandle_t vision_bias_handle = nullptr; - void * vision_bias_workspace = nullptr; // exactly 76 MiB, retained until context destruction + void * vision_bias_workspace = nullptr; // shared linear/AV 76 MiB, retained until context destruction cudaEvent_t vision_bias_event = nullptr; size_t vision_bias_launches = 0; size_t vision_norm_launches = 0; + size_t vision_softmax_launches = 0; + size_t vision_av_launches = 0; size_t vision_rotary_launches = 0; // successful synchronous table calls (five kernels each) #endif int curr_stream_no = 0; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu index b70b0e793..f48c134b3 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu @@ -5,6 +5,8 @@ #include "ggml-cuda/common.cuh" #include "ggml-cuda/vision-bias.cuh" #include "ggml-cuda/vision-rotary.cuh" +#include "ggml-cuda/vision-softmax.cuh" +#include "ggml-cuda/vision-av.cuh" #include "ggml-cuda/acc.cuh" #include "ggml-cuda/add-id.cuh" #include "ggml-cuda/arange.cuh" @@ -772,7 +774,7 @@ ggml_backend_cuda_context::~ggml_backend_cuda_context() { if (vision_bias_workspace) { ggml_cuda_set_device(device); // The latest event follows every use of the shared workspace. - if (vision_bias_launches) CUDA_CHECK(cudaEventSynchronize(vision_bias_event)); + if (vision_bias_launches || vision_av_launches) CUDA_CHECK(cudaEventSynchronize(vision_bias_event)); CUDA_CHECK(cudaFree(vision_bias_workspace)); CUDA_CHECK(cudaEventDestroy(vision_bias_event)); } @@ -3635,6 +3637,20 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg break; #else return false; +#endif + case GGML_OP_SOFT_MAX_VISION_F32: +#if defined(GGML_USE_HIP) + ggml_cuda_op_soft_max_vision_f32(ctx, dst); + break; +#else + return false; +#endif + case GGML_OP_MUL_MAT_VISION_AV_F32: +#if defined(GGML_USE_HIP) + ggml_hip_vision_av_f32(ctx, dst); + break; +#else + return false; #endif case GGML_OP_PAGED_ATTN: ggml_cuda_paged_attn(ctx, dst); @@ -3927,7 +3943,8 @@ static bool ggml_cuda_graph_check_compability(ggml_cgraph * cgraph) { ggml_tensor * node = cgraph->nodes[i]; // These explicit vision operations are qualified through direct // execution only; the linear also retains a workspace and event. - if (node->op == GGML_OP_MUL_MAT_BIAS_BF16 || node->op == GGML_OP_RMS_NORM_VISION_F32) return false; + if (node->op == GGML_OP_MUL_MAT_BIAS_BF16 || node->op == GGML_OP_RMS_NORM_VISION_F32 || + node->op == GGML_OP_SOFT_MAX_VISION_F32 || node->op == GGML_OP_MUL_MAT_VISION_AV_F32) return false; if (ggml_is_empty(node) || node->op == GGML_OP_RESHAPE || node->op == GGML_OP_TRANSPOSE || node->op == GGML_OP_VIEW || node->op == GGML_OP_PERMUTE || node->op == GGML_OP_NONE) { continue; @@ -6487,6 +6504,18 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g return ggml_hip_vision_norm_supported(dev_ctx->device, op); #else return false; +#endif + case GGML_OP_SOFT_MAX_VISION_F32: +#if defined(GGML_USE_HIP) + return ggml_hip_vision_softmax_f32_supported(dev_ctx->device, op); +#else + return false; +#endif + case GGML_OP_MUL_MAT_VISION_AV_F32: +#if defined(GGML_USE_HIP) + return ggml_hip_vision_av_f32_supported(dev_ctx->device, op); +#else + return false; #endif case GGML_OP_PAGED_ATTN: return ggml_cuda_paged_attn_supported(op); @@ -6681,6 +6710,20 @@ static size_t ggml_backend_hip_vision_norm_f32_launches(ggml_backend_t backend) return backend && ggml_backend_is_cuda(backend) ? static_cast(backend->context)->vision_norm_launches : 0; } +static bool ggml_backend_hip_vision_softmax_f32_capable(ggml_backend_t backend) { + return ggml_backend_hip_vision_norm_f32_capable(backend); +} +static size_t ggml_backend_hip_vision_softmax_f32_launches(ggml_backend_t backend) { + return backend && ggml_backend_is_cuda(backend) ? + static_cast(backend->context)->vision_softmax_launches : 0; +} +static bool ggml_backend_hip_vision_av_f32_capable(ggml_backend_t backend) { + return ggml_backend_hip_vision_norm_f32_capable(backend); +} +static size_t ggml_backend_hip_vision_av_f32_launches(ggml_backend_t backend) { + return backend && ggml_backend_is_cuda(backend) ? + static_cast(backend->context)->vision_av_launches : 0; +} static bool ggml_backend_hip_vision_rotary_f32_capable(ggml_backend_t backend) { return ggml_backend_hip_vision_norm_f32_capable(backend); } @@ -6703,6 +6746,10 @@ static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, con if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_launches")==0) return (void *)ggml_backend_hip_vision_bias_bf16_launches; if (strcmp(name,"ggml_backend_hip_vision_norm_f32_capable")==0) return (void *)ggml_backend_hip_vision_norm_f32_capable; if (strcmp(name,"ggml_backend_hip_vision_norm_f32_launches")==0) return (void *)ggml_backend_hip_vision_norm_f32_launches; + if (strcmp(name,"ggml_backend_hip_vision_softmax_f32_capable")==0) return (void *)ggml_backend_hip_vision_softmax_f32_capable; + if (strcmp(name,"ggml_backend_hip_vision_softmax_f32_launches")==0) return (void *)ggml_backend_hip_vision_softmax_f32_launches; + if (strcmp(name,"ggml_backend_hip_vision_av_f32_capable")==0) return (void *)ggml_backend_hip_vision_av_f32_capable; + if (strcmp(name,"ggml_backend_hip_vision_av_f32_launches")==0) return (void *)ggml_backend_hip_vision_av_f32_launches; if (strcmp(name,"ggml_backend_hip_vision_rotary_f32_capable")==0) return (void *)ggml_backend_hip_vision_rotary_f32_capable; if (strcmp(name,"ggml_backend_hip_vision_rotary_f32")==0) return (void *)ggml_backend_hip_vision_rotary_f32; if (strcmp(name,"ggml_backend_hip_vision_rotary_f32_launches")==0) return (void *)ggml_backend_hip_vision_rotary_f32_launches; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu new file mode 100644 index 000000000..051e87178 --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu @@ -0,0 +1,74 @@ +#include "vision-av.cuh" +#include "vision-bias.cuh" +#include "norm.cuh" + +#if defined(GGML_USE_HIP) +bool ggml_hip_vision_av_f32_supported(int device, const ggml_tensor * op) { + if (!ggml_hip_vision_norm_capable(device) || !op || op->op != GGML_OP_MUL_MAT_VISION_AV_F32 || + !op->src[0] || !op->src[1] || op->type != GGML_TYPE_F32) { + return false; + } + for (int i = 2; i < GGML_MAX_SRC; ++i) { + if (op->src[i]) return false; + } + const auto v = op->src[0], p = op->src[1]; + if (v->type != GGML_TYPE_F32 || p->type != GGML_TYPE_F32 || + v->ne[0] != 64 || v->ne[1] != 16 || v->ne[3] != 1) { + return false; + } + const int64_t n = v->ne[2]; + return n >= 16 && n <= 4096 && p->ne[0] == n && p->ne[1] == n && p->ne[2] == 16 && p->ne[3] == 1 && + op->ne[0] == 64 && op->ne[1] == n && op->ne[2] == 16 && op->ne[3] == 1 && + ggml_is_contiguous(v) && ggml_is_contiguous(p) && ggml_is_contiguous(op); +} + +void ggml_hip_vision_av_f32(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + GGML_ASSERT(ggml_hip_vision_av_f32_supported(ctx.device, dst)); + ggml_cuda_set_device(ctx.device); + const auto stream = ctx.stream(); + cudaStreamCaptureStatus capture; + CUDA_CHECK(cudaStreamIsCapturing(stream, &capture)); + GGML_ASSERT(capture == cudaStreamCaptureStatusNone && "HIP vision AV requires direct execution"); + ggml_hip_vision_workspace_acquire(ctx); + const int64_t n = dst->src[0]->ne[2]; + hipblasLtMatmulDesc_t op = nullptr; + hipblasLtMatrixLayout_t a = nullptr, b = nullptr, c = nullptr; + hipblasLtMatmulPreference_t pref = nullptr; + CUBLAS_CHECK(hipblasLtMatmulDescCreate(&op,HIPBLAS_COMPUTE_32F,HIP_R_32F)); + const hipblasOperation_t transpose = HIPBLAS_OP_N; + CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_TRANSA,&transpose,sizeof(transpose))); + CUBLAS_CHECK(hipblasLtMatmulDescSetAttribute(op,HIPBLASLT_MATMUL_DESC_TRANSB,&transpose,sizeof(transpose))); + // Pinned Torch 3d3aa833 Blas.cpp/CUDABlas.cpp: row-major output swaps the + // operands, retaining V's token-major stride. Default epilogue, C equals D. + CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&a,HIP_R_32F,64,n,1024)); + CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&b,HIP_R_32F,n,n,n)); + CUBLAS_CHECK(hipblasLtMatrixLayoutCreate(&c,HIP_R_32F,64,n,64)); + const int batches = 16; + const int64_t stride_a = 64, stride_b = n*n, stride_c = n*64; + for (auto layout : {a,b,c}) { + CUBLAS_CHECK(hipblasLtMatrixLayoutSetAttribute(layout,HIPBLASLT_MATRIX_LAYOUT_BATCH_COUNT,&batches,sizeof(batches))); + } + CUBLAS_CHECK(hipblasLtMatrixLayoutSetAttribute(a,HIPBLASLT_MATRIX_LAYOUT_STRIDED_BATCH_OFFSET,&stride_a,sizeof(stride_a))); + CUBLAS_CHECK(hipblasLtMatrixLayoutSetAttribute(b,HIPBLASLT_MATRIX_LAYOUT_STRIDED_BATCH_OFFSET,&stride_b,sizeof(stride_b))); + CUBLAS_CHECK(hipblasLtMatrixLayoutSetAttribute(c,HIPBLASLT_MATRIX_LAYOUT_STRIDED_BATCH_OFFSET,&stride_c,sizeof(stride_c))); + CUBLAS_CHECK(hipblasLtMatmulPreferenceCreate(&pref)); + constexpr size_t bytes = GGML_HIP_VISION_WORKSPACE_BYTES; + CUBLAS_CHECK(hipblasLtMatmulPreferenceSetAttribute(pref,HIPBLASLT_MATMUL_PREF_MAX_WORKSPACE_BYTES,&bytes,sizeof(bytes))); + hipblasLtMatmulHeuristicResult_t heuristic{}; + int returned = 0; + CUBLAS_CHECK(hipblasLtMatmulAlgoGetHeuristic(ctx.vision_bias_handle,op,a,b,c,c,pref,1,&heuristic,&returned)); + GGML_ASSERT(returned == 1 && "HIP vision AV: no first Lt heuristic; fallback forbidden"); + CUBLAS_CHECK(heuristic.state); + GGML_ASSERT(heuristic.workspaceSize <= bytes); + const float alpha = 1.0f, beta = 0.0f; + CUBLAS_CHECK(hipblasLtMatmul(ctx.vision_bias_handle,op,&alpha,dst->src[0]->data,a,dst->src[1]->data,b, + &beta,dst->data,c,dst->data,c,&heuristic.algo,ctx.vision_bias_workspace,bytes,stream)); + ggml_hip_vision_workspace_record(ctx); + ++ctx.vision_av_launches; + CUBLAS_CHECK(hipblasLtMatmulPreferenceDestroy(pref)); + CUBLAS_CHECK(hipblasLtMatrixLayoutDestroy(c)); + CUBLAS_CHECK(hipblasLtMatrixLayoutDestroy(b)); + CUBLAS_CHECK(hipblasLtMatrixLayoutDestroy(a)); + CUBLAS_CHECK(hipblasLtMatmulDescDestroy(op)); +} +#endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh new file mode 100644 index 000000000..70eaa54f2 --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh @@ -0,0 +1,7 @@ +#pragma once +#include "common.cuh" + +#if defined(GGML_USE_HIP) +bool ggml_hip_vision_av_f32_supported(int device, const ggml_tensor * op); +void ggml_hip_vision_av_f32(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +#endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu index 3a9da31ca..3e422eb17 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu @@ -13,19 +13,29 @@ bool ggml_hip_vision_bias_supported(const ggml_tensor * d) { d->ne[0]==w->ne[1] && d->ne[1]==x->ne[1] && d->ne[2]==1 && d->ne[3]==1; } -void ggml_hip_vision_bias(ggml_backend_cuda_context &ctx, ggml_tensor *dst) { - GGML_ASSERT(ggml_hip_vision_bias_supported(dst)); +void ggml_hip_vision_workspace_acquire(ggml_backend_cuda_context &ctx) { ggml_cuda_set_device(ctx.device); - const auto stream=ctx.stream(); - constexpr size_t bytes=76ULL*1024*1024; - // One retained workspace per context, not per layer or graph. An event - // serializes workspace use even if this context schedules other streams. + // A single event serializes shared workspace use across context streams. if (!ctx.vision_bias_handle) CUBLAS_CHECK(hipblasLtCreate(&ctx.vision_bias_handle)); if (!ctx.vision_bias_workspace) { - CUDA_CHECK(cudaMalloc(&ctx.vision_bias_workspace,bytes)); + CUDA_CHECK(cudaMalloc(&ctx.vision_bias_workspace,GGML_HIP_VISION_WORKSPACE_BYTES)); CUDA_CHECK(cudaEventCreateWithFlags(&ctx.vision_bias_event,cudaEventDisableTiming)); } - if (ctx.vision_bias_launches) CUDA_CHECK(cudaStreamWaitEvent(stream,ctx.vision_bias_event,0)); + if (ctx.vision_bias_launches || ctx.vision_av_launches) { + CUDA_CHECK(cudaStreamWaitEvent(ctx.stream(),ctx.vision_bias_event,0)); + } +} + +void ggml_hip_vision_workspace_record(ggml_backend_cuda_context &ctx) { + CUDA_CHECK(cudaEventRecord(ctx.vision_bias_event,ctx.stream())); +} + +void ggml_hip_vision_bias(ggml_backend_cuda_context &ctx, ggml_tensor *dst) { + GGML_ASSERT(ggml_hip_vision_bias_supported(dst)); + ggml_cuda_set_device(ctx.device); + const auto stream=ctx.stream(); + constexpr size_t bytes=GGML_HIP_VISION_WORKSPACE_BYTES; + ggml_hip_vision_workspace_acquire(ctx); const auto w=dst->src[0],x=dst->src[1],b=dst->src[2]; const int64_t k=w->ne[0],m=w->ne[1],n=x->ne[1]; hipblasLtMatmulDesc_t op=nullptr; @@ -51,7 +61,7 @@ void ggml_hip_vision_bias(ggml_backend_cuda_context &ctx, ggml_tensor *dst) { const float alpha=1,beta=0; CUBLAS_CHECK(hipblasLtMatmul(ctx.vision_bias_handle,op,&alpha,w->data,a,x->data,bl,&beta, dst->data,c,dst->data,c,&heuristic.algo,ctx.vision_bias_workspace,bytes,stream)); - CUDA_CHECK(cudaEventRecord(ctx.vision_bias_event,stream)); + ggml_hip_vision_workspace_record(ctx); ++ctx.vision_bias_launches; CUBLAS_CHECK(hipblasLtMatmulPreferenceDestroy(pref)); CUBLAS_CHECK(hipblasLtMatrixLayoutDestroy(c)); diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh index 9624bdbf9..c4a92c33f 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh @@ -1,6 +1,10 @@ #pragma once #include "common.cuh" #if defined(GGML_USE_HIP) +// One workspace/handle/event shared by explicit linear and AV operations. +constexpr size_t GGML_HIP_VISION_WORKSPACE_BYTES = 76ULL*1024*1024; +void ggml_hip_vision_workspace_acquire(ggml_backend_cuda_context & ctx); +void ggml_hip_vision_workspace_record(ggml_backend_cuda_context & ctx); // DS4V-only explicit op; no ordinary MUL_MAT/ADD fusion or fallback. bool ggml_hip_vision_bias_supported(const ggml_tensor * dst); void ggml_hip_vision_bias(ggml_backend_cuda_context & ctx, ggml_tensor * dst); diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax-kernels.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax-kernels.cuh new file mode 100644 index 000000000..4c416fc40 --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax-kernels.cuh @@ -0,0 +1,318 @@ +#pragma once + +// F32, wave32 adaptation of PyTorch 3d3aa833db84eed6b7f5595cb5f162c2f78300a4: +// aten/src/ATen/native/cuda/{PersistentSoftmax.cuh,SoftMax.cu,block_reduce.cuh}. +// Preserve source traversal, reduction, exp, and division order. Build without +// fast math or floating-point contraction. See the full upstream license below. +#include +#include +#include +#include +#include + +namespace ggml_vision_softmax { + +template +static __global__ void persistent(const float * input, float * output, int width, int rows) { + constexpr int power = 1 << LOG2_WIDTH; + constexpr int lanes = power < 32 ? power : 32; + constexpr int iterations = power / lanes; + constexpr int batch = power <= 128 ? 2 : 1; + const int first = (int(blockDim.y) * int(blockIdx.x) + int(threadIdx.y)) * batch; + const int lane = int(threadIdx.x); + float values[batch][iterations]; + float maximum[batch]; + float sums[batch]; + static_assert(std::is_same::value, "float exp required"); +#pragma unroll + for (int b = 0; b < batch; ++b) { +#pragma unroll + for (int it = 0; it < iterations; ++it) { + const int col = lane + it * lanes; + values[b][it] = first + b < rows && col < width ? + input[(first + b) * width + col] : -std::numeric_limits::infinity(); + } + maximum[b] = values[b][0]; +#pragma unroll + for (int it = 0; it < iterations; ++it) { + maximum[b] = maximum[b] > values[b][it] ? maximum[b] : values[b][it]; + } + } +#pragma unroll + for (int offset = lanes / 2; offset > 0; offset /= 2) { +#pragma unroll + for (int b = 0; b < batch; ++b) { + const float other = __shfl_xor(maximum[b], offset, lanes); + maximum[b] = maximum[b] < other ? other : maximum[b]; + } + } +#pragma unroll + for (int b = 0; b < batch; ++b) { + sums[b] = 0.0f; +#pragma unroll + for (int it = 0; it < iterations; ++it) { + values[b][it] = std::exp(values[b][it] - maximum[b]); + sums[b] += values[b][it]; + } + } +#pragma unroll + for (int offset = lanes / 2; offset > 0; offset /= 2) { +#pragma unroll + for (int b = 0; b < batch; ++b) { + sums[b] += __shfl_xor(sums[b], offset, lanes); + } + } +#pragma unroll + for (int b = 0; b < batch; ++b) { + if (first + b >= rows) { + break; + } +#pragma unroll + for (int it = 0; it < iterations; ++it) { + const int col = lane + it * lanes; + if (col < width) { + output[(first + b) * width + col] = values[b][it] / sums[b]; + } + } + } +} + +template +static __device__ __forceinline__ float combine(float a, float b) { + if constexpr (SUM) { + return a + b; + } else { + return a < b ? b : a; + } +} + +template +static __device__ __forceinline__ float block_reduce(float value, float * shared) { + const int tid = int(threadIdx.x), lane = tid % 32, warp = tid / 32; +#pragma unroll + for (int offset = 16; offset > 0; offset /= 2) { + value = combine(value, __shfl_down(value, offset, 32)); + } + __syncthreads(); + if (lane == 0) { + shared[warp] = value; + } + __syncthreads(); + value = tid < 16 ? shared[lane] : (SUM ? 0.0f : -std::numeric_limits::max()); + if (warp == 0) { +#pragma unroll + for (int offset = 16; offset > 0; offset /= 2) { + value = combine(value, __shfl_down(value, offset, 32)); + } + } + if (tid == 0) { + if constexpr (SUM) { + shared[0] = 1.0f / value; + } else { + shared[0] = value; + } + } + __syncthreads(); + return shared[0]; +} + +struct alignas(16) vector4 { float val[4]; }; + +template +static __device__ __forceinline__ float accumulate(float acc, float value, float maximum) { + if constexpr (SUM) { + return acc + __expf(value - maximum); + } else { + return ::max(acc, value); + } +} + +template +static __device__ __forceinline__ float local_reduce(const float * data, int size, float maximum) { + float result = SUM ? 0.0f : -std::numeric_limits::max(); + int offset = int(threadIdx.x); + if constexpr (DIVISIBLE) { + for (; offset * 4 < size; offset += 512) { + const vector4 values = reinterpret_cast(data)[offset]; +#pragma unroll + for (int j = 0; j < 4; ++j) { + result = accumulate(result, values.val[j], maximum); + } + } + } else { + const int shift = int(uintptr_t(data) % 16) / int(sizeof(float)); + if (shift > 0) { + data -= shift; + size += shift; + if (offset >= shift && offset < size) { + result = accumulate(result, data[offset], maximum); + } + size -= size < 512 ? size : 512; + data += 512; + } + const int last = size % (4 * 512); + for (; offset * 4 < size - last; offset += 512) { + const vector4 values = reinterpret_cast(data)[offset]; +#pragma unroll + for (int j = 0; j < 4; ++j) { + result = accumulate(result, values.val[j], maximum); + } + } + for (offset = size - last + int(threadIdx.x); offset < size; offset += 512) { + result = accumulate(result, data[offset], maximum); + } + } + return result; +} + +template +static __global__ void fast(const float * input, float * output, int width) { + extern __shared__ float shared[]; + input += int(blockIdx.x) * width; + output += int(blockIdx.x) * width; + const float maximum = block_reduce(local_reduce(input, width, 0.0f), shared); + const float inverse = block_reduce(local_reduce(input, width, maximum), shared); + if constexpr (DIVISIBLE) { + for (int offset = int(threadIdx.x); offset * 4 < width; offset += 512) { + const vector4 values = reinterpret_cast(input)[offset]; + vector4 result; +#pragma unroll + for (int j = 0; j < 4; ++j) { + result.val[j] = __expf(values.val[j] - maximum) * inverse; + } + reinterpret_cast(output)[offset] = result; + } + } else { + for (int col = int(threadIdx.x); col < width; col += 512) { + output[col] = __expf(input[col] - maximum) * inverse; + } + } +} + +template +static inline void launch_persistent(const float * input, float * output, int width, int rows, hipStream_t stream) { + constexpr int power = 1 << LOG2_WIDTH; + constexpr int lanes = power < 32 ? power : 32; + constexpr int batch = power <= 128 ? 2 : 1; + constexpr int groups = 128 / lanes; + persistent<<<(rows + groups * batch - 1) / (groups * batch), dim3(lanes, groups), 0, stream>>>( + input, output, width, rows); +} + +// Preconditions: F32, contiguous, aligned base, width in [16,4096], rows > 0, +// wave32, and total byte span <= INT_MAX. One launch, no allocation. +static inline void launch(const float * input, float * output, int width, int rows, hipStream_t stream) { + if (width > 2048) { + if (width % 4 == 0) { + fast<<>>(input, output, width); + } else { + fast<<>>(input, output, width); + } + return; + } + int log2_width = 4; + while ((1 << log2_width) < width) { + ++log2_width; + } + switch (log2_width) { +#define GGML_VISION_SOFTMAX_CASE(N) case N: launch_persistent(input, output, width, rows, stream); break + GGML_VISION_SOFTMAX_CASE(4); + GGML_VISION_SOFTMAX_CASE(5); + GGML_VISION_SOFTMAX_CASE(6); + GGML_VISION_SOFTMAX_CASE(7); + GGML_VISION_SOFTMAX_CASE(8); + GGML_VISION_SOFTMAX_CASE(9); + GGML_VISION_SOFTMAX_CASE(10); + GGML_VISION_SOFTMAX_CASE(11); +#undef GGML_VISION_SOFTMAX_CASE + } +} + +} // namespace ggml_vision_softmax + +/* +From PyTorch: + +Copyright (c) 2016- Facebook, Inc (Adam Paszke) +Copyright (c) 2014- Facebook, Inc (Soumith Chintala) +Copyright (c) 2011-2014 Idiap Research Institute (Ronan Collobert) +Copyright (c) 2012-2014 Deepmind Technologies (Koray Kavukcuoglu) +Copyright (c) 2011-2012 NEC Laboratories America (Koray Kavukcuoglu) +Copyright (c) 2011-2013 NYU (Clement Farabet) +Copyright (c) 2006-2010 NEC Laboratories America (Ronan Collobert, Leon Bottou, Iain Melvin, Jason Weston) +Copyright (c) 2006 Idiap Research Institute (Samy Bengio) +Copyright (c) 2001-2004 Idiap Research Institute (Ronan Collobert, Samy Bengio, Johnny Mariethoz) + +From Caffe2: + +Copyright (c) 2016-present, Facebook Inc. All rights reserved. + +All contributions by Facebook: +Copyright (c) 2016 Facebook Inc. + +All contributions by Google: +Copyright (c) 2015 Google Inc. +All rights reserved. + +All contributions by Yangqing Jia: +Copyright (c) 2015 Yangqing Jia +All rights reserved. + +All contributions by Kakao Brain: +Copyright 2019-2020 Kakao Brain + +All contributions by Cruise LLC: +Copyright (c) 2022 Cruise LLC. +All rights reserved. + +All contributions by Tri Dao: +Copyright (c) 2024 Tri Dao. +All rights reserved. + +All contributions by Arm: +Copyright (c) 2021, 2023-2025 Arm Limited and/or its affiliates + +All contributions from Caffe: +Copyright(c) 2013, 2014, 2015, the respective contributors +All rights reserved. + +All other contributions: +Copyright(c) 2015, 2016 the respective contributors +All rights reserved. + +Caffe2 uses a copyright model similar to Caffe: each contributor holds +copyright over their contributions to Caffe2. The project versioning records +all such contribution and copyright details. If a contributor wants to further +mark their specific copyright on a particular contribution, they should +indicate their copyright solely in the commit message of the change when it is +committed. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the names of Facebook, Deepmind Technologies, NYU, NEC Laboratories America + and IDIAP Research Institute nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu new file mode 100644 index 000000000..7e5399a0e --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu @@ -0,0 +1,52 @@ +#include "vision-softmax.cuh" + +#if defined(GGML_USE_HIP) +#include "vision-softmax-kernels.cuh" +#include + +bool ggml_hip_vision_softmax_f32_supported(int device, const ggml_tensor * op) { + const auto & info = ggml_cuda_info(); + if (device < 0 || device >= info.device_count || info.devices[device].warp_size != 32 || + !op || op->op != GGML_OP_SOFT_MAX_VISION_F32 || !op->src[0] || + op->type != GGML_TYPE_F32 || op->src[0]->type != GGML_TYPE_F32 || + op->ne[0] < 16 || op->ne[0] > 4096) { + return false; + } + int64_t elements = 1; + for (int i = 0; i < GGML_MAX_DIMS; ++i) { + if (op->ne[i] != op->src[0]->ne[i] || op->ne[i] <= 0 || + op->ne[i] > (INT_MAX / int64_t(sizeof(float))) / elements) { + return false; + } + elements *= op->ne[i]; + } + if (!ggml_is_contiguous(op) || !ggml_is_contiguous(op->src[0]) || + op->view_offs % 16 != 0 || op->src[0]->view_offs % 16 != 0 || + uintptr_t(op->data) % 16 != 0 || uintptr_t(op->src[0]->data) % 16 != 0) { + return false; + } + for (int i = 1; i < GGML_MAX_SRC; ++i) { + if (op->src[i]) { + return false; + } + } + return true; +} + +void ggml_cuda_op_soft_max_vision_f32(ggml_backend_cuda_context & ctx, ggml_tensor * dst) { + GGML_ASSERT(ggml_hip_vision_softmax_f32_supported(ctx.device, dst)); + const int width = int(dst->ne[0]); + const int rows = int(ggml_nelements(dst) / width); + const auto * input = static_cast(dst->src[0]->data); + auto * output = static_cast(dst->data); + cudaStreamCaptureStatus capture; + CUDA_CHECK(cudaStreamIsCapturing(ctx.stream(), &capture)); + GGML_ASSERT(capture == cudaStreamCaptureStatusNone); + // A contiguous Torch allocation has an aligned base; row misalignment is + // reproduced inside the non-vector-width branch of the kernel. + GGML_ASSERT(uintptr_t(input) % 16 == 0 && uintptr_t(output) % 16 == 0); + ggml_vision_softmax::launch(input, output, width, rows, ctx.stream()); + CUDA_CHECK(cudaGetLastError()); + ++ctx.vision_softmax_launches; +} +#endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh new file mode 100644 index 000000000..7bcbffef7 --- /dev/null +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh @@ -0,0 +1,8 @@ +#pragma once + +#include "common.cuh" + +#if defined(GGML_USE_HIP) +bool ggml_hip_vision_softmax_f32_supported(int device, const ggml_tensor * op); +void ggml_cuda_op_soft_max_vision_f32(ggml_backend_cuda_context & ctx, ggml_tensor * dst); +#endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt b/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt index 7e528a5a5..33d6d4607 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt +++ b/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt @@ -153,6 +153,11 @@ else() set_source_files_properties(${GGML_SOURCES_ROCM} PROPERTIES LANGUAGE HIP) endif() +# Preserve source eager F32 softmax arithmetic; the kernel explicitly selects +# its fast exponential calls and must not gain reciprocal/FMA substitutions. +set_property(SOURCE "${CMAKE_CURRENT_SOURCE_DIR}/../ggml-cuda/vision-softmax.cu" + APPEND PROPERTY COMPILE_OPTIONS -fno-fast-math -ffp-contract=off) + if (GGML_STATIC) message(FATAL_ERROR "Static linking not supported for HIP/ROCm") endif() diff --git a/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp b/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp index 09772b562..1fb22b9b2 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp +++ b/server/deps/llama.cpp/ggml/src/ggml-rpc/ggml-rpc.cpp @@ -1985,7 +1985,8 @@ static ggml_backend_buffer_type_t ggml_backend_rpc_device_get_buffer_type(ggml_b static bool ggml_backend_rpc_device_supports_op(ggml_backend_dev_t dev, const struct ggml_tensor * op) { // This opt-in local HIP extension must not be sent to an older RPC peer. - if (op->op == GGML_OP_MUL_MAT_BIAS_BF16 || op->op == GGML_OP_RMS_NORM_VISION_F32) return false; + if (op->op == GGML_OP_MUL_MAT_BIAS_BF16 || op->op == GGML_OP_RMS_NORM_VISION_F32 || + op->op == GGML_OP_SOFT_MAX_VISION_F32 || op->op == GGML_OP_MUL_MAT_VISION_AV_F32) return false; GGML_UNUSED(dev); GGML_UNUSED(op); //TODO: call the remote backend and cache the results diff --git a/server/deps/llama.cpp/ggml/src/ggml.c b/server/deps/llama.cpp/ggml/src/ggml.c index a03752344..a1e322a0b 100644 --- a/server/deps/llama.cpp/ggml/src/ggml.c +++ b/server/deps/llama.cpp/ggml/src/ggml.c @@ -1202,9 +1202,11 @@ static const char * GGML_OP_NAME[GGML_OP_COUNT] = { "PAGED_ATTN", "MUL_MAT_BIAS_BF16", "RMS_NORM_VISION_F32", + "SOFT_MAX_VISION_F32", + "MUL_MAT_VISION_AV_F32", }; -static_assert(GGML_OP_COUNT == 107, "GGML_OP_COUNT != 107"); +static_assert(GGML_OP_COUNT == 109, "GGML_OP_COUNT != 109"); static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "none", @@ -1331,9 +1333,11 @@ static const char * GGML_OP_SYMBOL[GGML_OP_COUNT] = { "paged_attn(q,k,v)", "bf16(X*Y+bias)", "rms_norm_vision_f32(x)", + "soft_max_vision_f32(x)", + "vision_av_f32(v,p)", }; -static_assert(GGML_OP_COUNT == 107, "GGML_OP_COUNT != 107"); +static_assert(GGML_OP_COUNT == 109, "GGML_OP_COUNT != 109"); static_assert(GGML_OP_POOL_COUNT == 2, "GGML_OP_POOL_COUNT != 2"); @@ -3329,6 +3333,45 @@ struct ggml_tensor * ggml_rms_norm_vision_f32( return result; } +// ggml_soft_max_vision_f32 + +struct ggml_tensor * ggml_soft_max_vision_f32( + struct ggml_context * ctx, + struct ggml_tensor * scores) { + GGML_ASSERT(scores && scores->type == GGML_TYPE_F32); + GGML_ASSERT(scores->ne[0] >= 16 && scores->ne[0] <= 4096); + int64_t elements = scores->ne[0]; + for (int i = 1; i < GGML_MAX_DIMS; ++i) { + GGML_ASSERT(scores->ne[i] > 0 && scores->ne[i] <= (INT_MAX/(int64_t)sizeof(float))/elements); + elements *= scores->ne[i]; + } + GGML_ASSERT(ggml_is_contiguous(scores)); + struct ggml_tensor * result = ggml_dup_tensor(ctx, scores); + result->op = GGML_OP_SOFT_MAX_VISION_F32; + result->src[0] = scores; + return result; +} + +// ggml_mul_mat_vision_av_f32 + +struct ggml_tensor * ggml_mul_mat_vision_av_f32( + struct ggml_context * ctx, + struct ggml_tensor * v, + struct ggml_tensor * probabilities) { + GGML_ASSERT(v && probabilities && v->type == GGML_TYPE_F32 && probabilities->type == GGML_TYPE_F32); + GGML_ASSERT(v->ne[0] == 64 && v->ne[1] == 16 && v->ne[3] == 1); + const int64_t n = v->ne[2]; + GGML_ASSERT(n >= 16 && n <= 4096); + GGML_ASSERT(probabilities->ne[0] == n && probabilities->ne[1] == n && + probabilities->ne[2] == 16 && probabilities->ne[3] == 1); + GGML_ASSERT(ggml_is_contiguous(v) && ggml_is_contiguous(probabilities)); + struct ggml_tensor * result = ggml_new_tensor_3d(ctx, GGML_TYPE_F32, 64, n, 16); + result->op = GGML_OP_MUL_MAT_VISION_AV_F32; + result->src[0] = v; + result->src[1] = probabilities; + return result; +} + // ggml_rms_norm_back struct ggml_tensor * ggml_rms_norm_back( @@ -7813,6 +7856,8 @@ static void ggml_compute_backward( } break; case GGML_OP_MUL_MAT_BIAS_BF16: // inference-only, no backward kernel case GGML_OP_RMS_NORM_VISION_F32: + case GGML_OP_SOFT_MAX_VISION_F32: + case GGML_OP_MUL_MAT_VISION_AV_F32: case GGML_OP_COUNT: default: { GGML_ABORT("%s: unsupported ggml op for backward pass: %s\n", __func__, ggml_op_name(tensor->op)); diff --git a/server/src/deepseek4/deepseek4_vision.cpp b/server/src/deepseek4/deepseek4_vision.cpp index 6b0a3f6d8..9d53964c7 100644 --- a/server/src/deepseek4/deepseek4_vision.cpp +++ b/server/src/deepseek4/deepseek4_vision.cpp @@ -122,6 +122,28 @@ size_t hip_bias_workspace(ggml_backend_t b) { return hip_size_query(b,"ggml_back size_t hip_bias_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_bias_bf16_launches"); } size_t hip_norm_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_norm_f32_launches"); } size_t hip_rotary_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_rotary_f32_launches"); } +size_t hip_softmax_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_softmax_f32_launches"); } +size_t hip_av_launches(ggml_backend_t b) { return hip_size_query(b,"ggml_backend_hip_vision_av_f32_launches"); } +bool hip_softmax_capable(ggml_backend_t backend) { + if(!backend) return false; + auto device=ggml_backend_get_device(backend); + if(!device) return false; + auto reg=ggml_backend_dev_backend_reg(device); + if(!reg) return false; + using Query=bool (*)(ggml_backend_t); + auto query=reinterpret_cast(ggml_backend_reg_get_proc_address(reg,"ggml_backend_hip_vision_softmax_f32_capable")); + return query && query(backend); +} +bool hip_av_capable(ggml_backend_t backend) { + if(!backend) return false; + auto device=ggml_backend_get_device(backend); + if(!device) return false; + auto reg=ggml_backend_dev_backend_reg(device); + if(!reg) return false; + using Query=bool (*)(ggml_backend_t); + auto query=reinterpret_cast(ggml_backend_reg_get_proc_address(reg,"ggml_backend_hip_vision_av_f32_capable")); + return query && query(backend); +} bool hip_rotary_capable(ggml_backend_t backend) { if(!backend) return false; auto device=ggml_backend_get_device(backend); @@ -200,7 +222,7 @@ Tensor * rotate(ggml_context * c,Tensor * x,Tensor * cosine,Tensor * sine) { auto second=ggml_add(c,ggml_mul(c,b,cosine),ggml_mul(c,a,sine)); return rounded(c,ggml_concat(c,first,second,0)); } -Tensor * attention(ggml_context * c,Tensor * q,Tensor * k,Tensor * v) { +Tensor * attention(ggml_context * c,Tensor * q,Tensor * k,Tensor * v,ggml_backend_t backend) { q=ggml_cont(c,ggml_permute(c,q,0,2,1,3)); k=ggml_cont(c,ggml_permute(c,k,0,2,1,3)); // PyTorch 2.10 Math SDPA (the source's 3D input dispatch) scales both @@ -209,10 +231,21 @@ Tensor * attention(ggml_context * c,Tensor * q,Tensor * k,Tensor * v) { const float scale=float(std::sqrt(1.0/std::sqrt(double(q->ne[0])))); auto scores=ggml_mul_mat(c,ggml_scale(c,k,scale),ggml_scale(c,q,scale)); ggml_mul_mat_set_prec(scores,GGML_PREC_F32); - auto probabilities=ggml_soft_max(c,scores); - v=ggml_cont(c,ggml_permute(c,v,1,2,0,3)); // [N, D, heads] - auto out=ggml_mul_mat(c,v,probabilities); - ggml_mul_mat_set_prec(out,GGML_PREC_F32); + Tensor * out; + if(hip_bias_workspace(backend)) { + require(hip_softmax_capable(backend) && hip_av_capable(backend), + "HIP source-order vision attention unavailable; fallback forbidden"); + auto probabilities=ggml_soft_max_vision_f32(c,scores); + require(ggml_backend_supports_op(backend,probabilities),"HIP vision softmax unsupported"); + // Retain V's token-major layout for the original batched Lt NN call. + out=ggml_mul_mat_vision_av_f32(c,v,probabilities); + require(ggml_backend_supports_op(backend,out),"HIP vision attention product unsupported"); + } else { + auto probabilities=ggml_soft_max(c,scores); + v=ggml_cont(c,ggml_permute(c,v,1,2,0,3)); // [N, D, heads] + out=ggml_mul_mat(c,v,probabilities); + ggml_mul_mat_set_prec(out,GGML_PREC_F32); + } out=ggml_cont(c,ggml_permute(c,out,0,2,1,3)); return rounded(c,ggml_reshape_2d(c,out,out->ne[0]*out->ne[1],out->ne[2])); } @@ -287,6 +320,8 @@ bool VisionRuntime::load(const std::string & path,ggml_backend_t backend,int dim "HIP source-order vision normalization unavailable"); require(!detail::hip_bias_workspace(backend) || detail::hip_rotary_capable(backend), "HIP source-order vision rotary tables unavailable"); + require(!detail::hip_bias_workspace(backend) || (detail::hip_softmax_capable(backend) && detail::hip_av_capable(backend)), + "HIP source-order vision attention unavailable"); Meta meta; meta.g=gguf_init_from_file(path.c_str(),{true,&meta.c}); require(meta.g && meta.c,"could not parse vision GGUF"); @@ -371,7 +406,7 @@ bool VisionRuntime::encode(const std::vector & patches,PatchGrid grid,Vis }; auto q=detail::rotate(g.c,slice(0),cosine,sine); auto k=detail::rotate(g.c,slice(1),cosine,sine); - auto attention=detail::attention(g.c,q,k,slice(2)); + auto attention=detail::attention(g.c,q,k,slice(2),impl_->backend); auto projected=impl_->linear(g.c,attention,p+".attn.wo"); auto residual=rounded(g.c,ggml_add(g.c,input,projected)); auto mlp=impl_->linear(g.c,impl_->norm(g.c,residual,p+".norm2"),p+".mlp.w1",false); diff --git a/server/src/deepseek4/deepseek4_vision.h b/server/src/deepseek4/deepseek4_vision.h index f212ecf46..2b0846329 100644 --- a/server/src/deepseek4/deepseek4_vision.h +++ b/server/src/deepseek4/deepseek4_vision.h @@ -62,6 +62,10 @@ bool hip_norm_capable(ggml_backend_t); size_t hip_norm_launches(ggml_backend_t); bool hip_rotary_capable(ggml_backend_t); size_t hip_rotary_launches(ggml_backend_t); +bool hip_softmax_capable(ggml_backend_t); +size_t hip_softmax_launches(ggml_backend_t); +bool hip_av_capable(ggml_backend_t); +size_t hip_av_launches(ggml_backend_t); // F32 values exactly representing BF16 inputs; HIP source order needs at least 16 rows. ggml_tensor * rms_norm(ggml_context *, ggml_tensor * input, float epsilon, ggml_backend_t); ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, ggml_tensor * bias, @@ -69,7 +73,8 @@ ggml_tensor * linear(ggml_context *, ggml_tensor * weight, ggml_tensor * input, void rotary_tables(PatchGrid grid, std::vector & cosine, std::vector & sine, ggml_backend_t backend = nullptr); ggml_tensor * rotate(ggml_context *, ggml_tensor *, ggml_tensor * cosine, ggml_tensor * sine); -ggml_tensor * attention(ggml_context *, ggml_tensor * q, ggml_tensor * k, ggml_tensor * v); +ggml_tensor * attention(ggml_context *, ggml_tensor * q, ggml_tensor * k, ggml_tensor * v, + ggml_backend_t backend = nullptr); ggml_tensor * unfold(ggml_context *, ggml_tensor *, PatchGrid, int channels); } } // namespace dflash::vision diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt index 189a58e7a..168261cb5 100644 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ b/server/tools/ds4v_vision/CMakeLists.txt @@ -54,6 +54,8 @@ add_executable(ds4v_norm_source norm_source.cpp) target_link_libraries(ds4v_norm_source PRIVATE ds4v_vision) add_executable(ds4v_rotary_source rotary_source.cpp) target_link_libraries(ds4v_rotary_source PRIVATE ds4v_vision) +add_executable(ds4v_attention_source attention_source.cpp) +target_link_libraries(ds4v_attention_source PRIVATE ds4v_vision) add_executable(ds4v_linear_rounding linear_rounding.cpp) target_link_libraries(ds4v_linear_rounding PRIVATE ds4v_vision) if(DS4V_VISION_HIP) @@ -63,6 +65,7 @@ if(DS4V_VISION_HIP) target_compile_definitions(ds4v_linear_unbiased_source PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_norm_source PRIVATE DS4V_VISION_HIP) target_compile_definitions(ds4v_rotary_source PRIVATE DS4V_VISION_HIP) + target_compile_definitions(ds4v_attention_source PRIVATE DS4V_VISION_HIP) endif() add_executable(ds4v_vision_geometry geometry.cpp) target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) @@ -77,3 +80,6 @@ add_test(NAME ds4v_norm_contract COMMAND ds4v_norm_contract) add_executable(ds4v_rotary_contract rotary_contract.cpp) target_link_libraries(ds4v_rotary_contract PRIVATE ds4v_vision) add_test(NAME ds4v_rotary_contract COMMAND ds4v_rotary_contract) +add_executable(ds4v_attention_contract attention_contract.cpp) +target_link_libraries(ds4v_attention_contract PRIVATE ds4v_vision) +add_test(NAME ds4v_attention_contract COMMAND ds4v_attention_contract) diff --git a/server/tools/ds4v_vision/attention_contract.cpp b/server/tools/ds4v_vision/attention_contract.cpp new file mode 100644 index 000000000..2d9d02e8d --- /dev/null +++ b/server/tools/ds4v_vision/attention_contract.cpp @@ -0,0 +1,87 @@ +#include "deepseek4/deepseek4_vision.h" +#include "ggml-cpu.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace dflash::vision; +static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && + GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && + GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed"); + +static void reject(bool av,int mode) { + const pid_t pid=fork(); check(pid>=0,"fork failed"); + if(pid==0) { + const rlimit limit={0,0}; setrlimit(RLIMIT_CORE,&limit); + auto c=ggml_init({1024*1024,nullptr,true}); + if(!av) { + auto x=ggml_new_tensor_2d(c,mode==1?GGML_TYPE_BF16:GGML_TYPE_F32,16,16); + if(mode==0) x=nullptr; + if(mode==2) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,15,16); + if(mode==3) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,4097,16); + if(mode==4) x=ggml_transpose(c,x); + if(mode==5) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,16,int64_t(INT_MAX)/64+1); + (void)ggml_soft_max_vision_f32(c,x); + } else { + int n=mode==5?15:mode==6?4097:16; + auto v=ggml_new_tensor_3d(c,mode==2?GGML_TYPE_BF16:GGML_TYPE_F32,mode==3?32:64,mode==4?8:16,n); + auto p=ggml_new_tensor_3d(c,mode==7?GGML_TYPE_BF16:GGML_TYPE_F32,n,n,16); + if(mode==0) v=nullptr; + if(mode==1) p=nullptr; + if(mode==8) p=ggml_new_tensor_3d(c,GGML_TYPE_F32,n,n+1,16); + if(mode==9) p=ggml_new_tensor_3d(c,GGML_TYPE_F32,n,n,8); + if(mode==10) p=ggml_transpose(c,p); + if(mode==11) v=ggml_permute(c,ggml_new_tensor_3d(c,GGML_TYPE_F32,16,64,n),1,0,2,3); + if(mode==12) v=ggml_new_tensor_4d(c,GGML_TYPE_F32,64,16,n,2); + if(mode==13) p=ggml_new_tensor_4d(c,GGML_TYPE_F32,n,n,16,2); + (void)ggml_mul_mat_vision_av_f32(c,v,p); + } + _exit(0); + } + int status=0; check(waitpid(pid,&status,0)==pid,"wait failed"); + check(WIFSIGNALED(status) && WTERMSIG(status)==SIGABRT,"invalid attention constructor accepted"); +} + +int main() { + auto backend=ggml_backend_cpu_init(); auto c=ggml_init({2*1024*1024,nullptr,true}); + try { + check(backend && c,"initialization failed"); + for(auto selected:{static_cast(nullptr),backend}) { + check(!detail::hip_softmax_capable(selected) && !detail::hip_av_capable(selected),"CPU/null advertised HIP attention"); + check(detail::hip_softmax_launches(selected)==0 && detail::hip_av_launches(selected)==0,"CPU/null reported HIP launches"); + auto q=ggml_new_tensor_3d(c,GGML_TYPE_F32,4,2,3); + auto g=ggml_new_graph(c); ggml_build_forward_expand(g,detail::attention(c,q,q,q,selected)); + int softmax=0,matmul=0; + for(int i=0;iop; + check(op!=GGML_OP_SOFT_MAX_VISION_F32 && op!=GGML_OP_MUL_MAT_VISION_AV_F32,"CPU generic attention changed"); + softmax+=op==GGML_OP_SOFT_MAX; matmul+=op==GGML_OP_MUL_MAT; + } + check(softmax==1 && matmul==2,"CPU attention graph operation count changed"); + } + for(int n:{16,128,782,2048,2049,2560,2562,4096}) { + auto v=ggml_new_tensor_3d(c,GGML_TYPE_F32,64,16,n); + auto x=ggml_new_tensor_3d(c,GGML_TYPE_F32,n,n,16); + auto p=ggml_soft_max_vision_f32(c,x); + auto y=ggml_mul_mat_vision_av_f32(c,v,p); + check(p->op==GGML_OP_SOFT_MAX_VISION_F32 && p->src[0]==x && p->type==GGML_TYPE_F32 && + ggml_are_same_shape(p,x),"softmax constructor shape changed"); + check(y->op==GGML_OP_MUL_MAT_VISION_AV_F32 && y->src[0]==v && y->src[1]==p && + y->type==GGML_TYPE_F32 && y->ne[0]==64 && y->ne[1]==n && y->ne[2]==16 && y->ne[3]==1, + "AV constructor layout changed"); + check(!ggml_backend_supports_op(backend,p) && !ggml_backend_supports_op(backend,y),"CPU advertised HIP operations"); + } + for(int mode=0;mode<6;++mode) reject(false,mode); + for(int mode=0;mode<14;++mode) reject(true,mode); + ggml_free(c); ggml_backend_free(backend); + std::cout<<"PASS: HIP attention ABI, bounded shapes, CPU/null preservation, invalid input rejection\n"; + return 0; + } catch(const std::exception &error) { + std::cerr<<"FAIL: "< +#include +#include +#include +#include +#include +#include +#include +#include + +namespace fs=std::filesystem; +constexpr int ROWS=782, WIDTH=1024, HEADS=16, DIM=64; +constexpr size_t LIMIT=256ULL*1024*1024; +static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } +static uint32_t bits(float x) { uint32_t out; std::memcpy(&out,&x,4); return out; } +static std::vector load(const fs::path &path,size_t count,bool bf16=false) { + check(fs::is_regular_file(path) && fs::file_size(path)==count*4,"wrong fixture size"); + std::vector out(count); std::ifstream f(path,std::ios::binary); + f.read(reinterpret_cast(out.data()),count*4); check(bool(f),"fixture read failed"); + for(float x:out) check(std::isfinite(x) && (!bf16 || !(bits(x)&65535)),"invalid fixture values"); + return out; +} +static size_t compare(const std::vector &a,const std::vector &b) { + check(a.size()==b.size(),"comparison size mismatch"); size_t count=0; + for(size_t i=0;i &values) { + std::ofstream f(path,std::ios::binary); + f.write(reinterpret_cast(values.data()),values.size()*4); check(bool(f),"output write failed"); +} +struct Backend { + ggml_backend_t value=nullptr; + ~Backend() { if(value) ggml_backend_free(value); } +}; +struct Graph { + ggml_backend_t backend; + ggml_context *ctx=nullptr; ggml_cgraph *graph=nullptr; ggml_gallocr_t allocator=nullptr; + std::map outputs; + std::vector *>> inputs; + explicit Graph(ggml_backend_t b):backend(b) { + ctx=ggml_init({1024*1024,nullptr,true}); check(ctx,"metadata allocation failed"); + graph=ggml_new_graph(ctx); + } + ~Graph() { + ggml_backend_synchronize(backend); + if(allocator) ggml_gallocr_free(allocator); + if(ctx) ggml_free(ctx); + } + ggml_tensor *input(const std::vector &values,int64_t n0,int64_t n1,int64_t n2=1) { + check(values.size()==size_t(n0*n1*n2),"input shape mismatch"); + auto t=ggml_new_tensor_3d(ctx,GGML_TYPE_F32,n0,n1,n2); + ggml_set_input(t); inputs.emplace_back(t,&values); return t; + } + void output(const std::string &name,ggml_tensor *t) { + auto copy=ggml_dup(ctx,t); ggml_set_output(copy); + check(outputs.emplace(name,copy).second,"duplicate output"); + ggml_build_forward_expand(graph,copy); + } + std::map> execute(const fs::path &path,const std::string &prefix) { + for(int i=0;idata(),0,values->size()*4); + check(ggml_backend_graph_compute(backend,graph)==GGML_STATUS_SUCCESS,"graph execution failed"); + ggml_backend_synchronize(backend); + std::map> result; + for(auto &[name,t]:outputs) { + std::vector values(ggml_nelements(t)); + ggml_backend_tensor_get(t,values.data(),0,values.size()*4); + for(float x:values) check(std::isfinite(x),"nonfinite graph result"); + save(path/(prefix+name+".f32"),values); result.emplace(name,std::move(values)); + } + std::cout< cosine,sine; + detail::rotary_tables({23,34},cosine,sine,backend.value); + check(compare(cosine,load(source/"cos.f32",cosine.size()))==0 && + compare(sine,load(source/"sin.f32",sine.size()))==0,"rotary source differs"); + save(out/"cos.f32",cosine); save(out/"sin.f32",sine); + Graph g(backend.value); + auto input=g.input(qkv,3072,ROWS); + auto cos=g.input(cosine,32,1,ROWS),sin=g.input(sine,32,1,ROWS); + auto slice=[&](int offset) { + return ggml_cont(g.ctx,ggml_view_3d(g.ctx,input,DIM,HEADS,ROWS,DIM*4,3072*4,offset*WIDTH*4)); + }; + auto q=detail::rotate(g.ctx,slice(0),cos,sin),k=detail::rotate(g.ctx,slice(1),cos,sin),v=slice(2); + auto attention=detail::attention(g.ctx,q,k,v,backend.value); + g.output("attention",attention); g.output("q",q); g.output("k",k); g.output("v",v); + ggml_tensor *probabilities=nullptr,*precast=nullptr; + int softmax_count=0,av_count=0; + for(int i=0;iop==GGML_OP_SOFT_MAX_VISION_F32) { probabilities=node; ++softmax_count; } + if(node->op==GGML_OP_MUL_MAT_VISION_AV_F32) { precast=node; ++av_count; } + check(node->op!=GGML_OP_SOFT_MAX,"generic softmax in HIP attention"); + } + check(softmax_count==1 && av_count==1 && probabilities && precast,"attention operation count changed"); + g.output("scores",probabilities->src[0]); g.output("probabilities",probabilities); g.output("precast_av",precast); + const auto actual=g.execute(out,""); + for(const auto &[name,values]:actual) { + const size_t different=compare(values,load(source/(name+".f32"),values.size())); + std::cout< #include static void check(bool ok,const char *why) { if(!ok)throw std::runtime_error(why); } -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107,"operation ABI changed"); +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed"); static void rejected(int mode,bool with_bias=true) { pid_t pid=fork(); check(pid>=0,"fork failed"); if(pid==0) { diff --git a/server/tools/ds4v_vision/linear_source.cpp b/server/tools/ds4v_vision/linear_source.cpp index 038b56ed5..144d875ed 100644 --- a/server/tools/ds4v_vision/linear_source.cpp +++ b/server/tools/ds4v_vision/linear_source.cpp @@ -14,7 +14,7 @@ #include #include #include -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107,"operation ABI changed unexpectedly"); +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed unexpectedly"); static void check(bool b,const char *s) { if(!b) throw std::runtime_error(s); } static uint32_t bits(float v) { uint32_t b; std::memcpy(&b,&v,4); return b; } static std::vector load(const std::filesystem::path&p,size_t n) { diff --git a/server/tools/ds4v_vision/linear_unbiased_source.cpp b/server/tools/ds4v_vision/linear_unbiased_source.cpp index 1c1631bdc..5d102a6f2 100644 --- a/server/tools/ds4v_vision/linear_unbiased_source.cpp +++ b/server/tools/ds4v_vision/linear_unbiased_source.cpp @@ -18,7 +18,7 @@ #include #include -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107, +static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109, "operation ABI changed unexpectedly"); static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } static size_t product(size_t a,size_t b) { diff --git a/server/tools/ds4v_vision/norm_contract.cpp b/server/tools/ds4v_vision/norm_contract.cpp index a17f5eee2..4567d88c5 100644 --- a/server/tools/ds4v_vision/norm_contract.cpp +++ b/server/tools/ds4v_vision/norm_contract.cpp @@ -11,7 +11,7 @@ static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && - GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_COUNT==107,"operation ABI changed"); + GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed"); static void rejected(int mode) { const pid_t pid=fork(); check(pid>=0,"fork failed"); if(pid==0) { diff --git a/server/tools/ds4v_vision/probe.cpp b/server/tools/ds4v_vision/probe.cpp index 0cf671048..ed0ab2645 100644 --- a/server/tools/ds4v_vision/probe.cpp +++ b/server/tools/ds4v_vision/probe.cpp @@ -78,20 +78,28 @@ int main(int argc,char ** argv) { const auto lt_before=detail::hip_bias_launches(backend); const auto norm_before=detail::hip_norm_launches(backend); const auto rotary_before=detail::hip_rotary_launches(backend); + const auto softmax_before=detail::hip_softmax_launches(backend); + const auto av_before=detail::hip_av_launches(backend); auto started=std::chrono::steady_clock::now(); if(!runtime.encode(patches,grid,output,error,true,observer)) throw std::runtime_error(error); const auto lt_launches=detail::hip_bias_launches(backend)-lt_before; const auto norm_launches=detail::hip_norm_launches(backend)-norm_before; const auto rotary_launches=detail::hip_rotary_launches(backend)-rotary_before; + const auto softmax_launches=detail::hip_softmax_launches(backend)-softmax_before; + const auto av_launches=detail::hip_av_launches(backend)-av_before; const auto external=detail::hip_bias_workspace(backend); const bool hip_requested=device=="hip:0" || device=="hip:1"; if((external!=0)!=hip_requested) throw std::runtime_error("requested HIP backend lacks BF16 linear capability"); if(lt_launches!=(hip_requested ? 131u : 0u)) throw std::runtime_error("unexpected actual HIP BF16 linear dispatch count"); if(norm_launches!=(hip_requested ? 65u : 0u)) throw std::runtime_error("unexpected actual HIP vision normalization dispatch count"); if(rotary_launches!=(hip_requested ? 1u : 0u)) throw std::runtime_error("unexpected actual HIP vision rotary table preparation count"); + if(softmax_launches!=(hip_requested ? 32u : 0u)) throw std::runtime_error("unexpected actual HIP vision softmax dispatch count"); + if(av_launches!=(hip_requested ? 32u : 0u)) throw std::runtime_error("unexpected actual HIP vision attention product dispatch count"); std::cout<<"hip_vision_linear_launches="< Date: Fri, 4 Sep 2026 22:08:38 -0400 Subject: [PATCH 064/123] test(ds4): expose BF16 norm affine compatibility regression --- server/src/deepseek4/deepseek4_graph.cpp | 4 +- server/src/deepseek4/deepseek4_norm.h | 10 ++++ server/tools/ds4_bf16_affine/CMakeLists.txt | 12 ++++ server/tools/ds4_bf16_affine/probe.cpp | 63 +++++++++++++++++++++ 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 server/src/deepseek4/deepseek4_norm.h create mode 100644 server/tools/ds4_bf16_affine/CMakeLists.txt create mode 100644 server/tools/ds4_bf16_affine/probe.cpp diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index 8ac1c06a9..633fa3ece 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -9,6 +9,7 @@ // 6. MoE FFN (hash routing + top-k + shared expert + clamped SwiGLU) #include "deepseek4_internal.h" +#include "deepseek4_norm.h" #include "deepseek4_hc_cuda.h" #include "deepseek4_roctx.h" #include "internal.h" @@ -554,8 +555,7 @@ static bool build_cached_decode_output_graph( static ggml_tensor * build_rms_norm(ggml_context * ctx, ggml_tensor * x, ggml_tensor * weight, float eps) { - ggml_tensor * normed = ggml_rms_norm(ctx, x, eps); - return ggml_mul(ctx, normed, weight); + return detail::build_rms_norm(ctx, x, weight, eps); } // ─── Helper: Clamped SwiGLU ───────────────────────────────────────────── diff --git a/server/src/deepseek4/deepseek4_norm.h b/server/src/deepseek4/deepseek4_norm.h new file mode 100644 index 000000000..68c0e42f8 --- /dev/null +++ b/server/src/deepseek4/deepseek4_norm.h @@ -0,0 +1,10 @@ +#pragma once +#include "ggml.h" + +namespace dflash::common::detail { +inline ggml_tensor * build_rms_norm(ggml_context * ctx, ggml_tensor * x, + ggml_tensor * weight, float eps) { + ggml_tensor * normed = ggml_rms_norm(ctx, x, eps); + return ggml_mul(ctx, normed, weight); +} +} diff --git a/server/tools/ds4_bf16_affine/CMakeLists.txt b/server/tools/ds4_bf16_affine/CMakeLists.txt new file mode 100644 index 000000000..40b6f3c01 --- /dev/null +++ b/server/tools/ds4_bf16_affine/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4_bf16_affine LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(GGML_BUILD "" CACHE PATH "Immutable existing GGML build") +add_executable(ds4_bf16_affine probe.cpp) +target_include_directories(ds4_bf16_affine PRIVATE ../../src ../../deps/llama.cpp/ggml/include) +foreach(lib ggml/src/libggml-base.so.0 ggml/src/libggml-cpu.so.0 ggml/src/ggml-hip/libggml-hip.so.0) + if(NOT EXISTS "${GGML_BUILD}/${lib}") + message(FATAL_ERROR "Missing immutable library ${GGML_BUILD}/${lib}") + endif() + target_link_libraries(ds4_bf16_affine PRIVATE "${GGML_BUILD}/${lib}") +endforeach() diff --git a/server/tools/ds4_bf16_affine/probe.cpp b/server/tools/ds4_bf16_affine/probe.cpp new file mode 100644 index 000000000..9e906163f --- /dev/null +++ b/server/tools/ds4_bf16_affine/probe.cpp @@ -0,0 +1,63 @@ +#include "deepseek4/deepseek4_norm.h" +#include "ggml-alloc.h" +#include "ggml-cpu.h" +#include "ggml-cuda.h" +#include +#include +#include +#include +#include +#include +#include +#include +static void check(bool ok,const char * msg) { if(!ok) throw std::runtime_error(msg); } +static float decode(uint16_t v) { uint32_t b=uint32_t(v)<<16;float f;std::memcpy(&f,&b,4);return f; } +int main(int argc,char ** argv) { + if(argc!=3) { std::cerr<<"usage: ds4_bf16_affine cpu|hip:0 contract|execute\n";return 2; } + const bool gpu=std::string(argv[1])=="hip:0",contract=std::string(argv[2])=="contract"; + if(!gpu && std::string(argv[1])!="cpu") return 2; + if(!contract && std::string(argv[2])!="execute") return 2; + ggml_backend_t backend=gpu?ggml_backend_cuda_init(0):ggml_backend_cpu_init(); + if(!backend) return 1; + if(!gpu) ggml_backend_cpu_set_n_threads(backend,2); + std::cout<<"backend="<src[1]==control && half_graph->src[1]==f16,"F32/F16 graph changed"); + const bool affine_f32=y->src[1]->type==GGML_TYPE_F32; + std::cout<<"n="<src[1])==4*k && y->src[1]->src[0]==w,"not vector-only cast"); + std::vector input(k*n),weight(k);std::vector raw(k),after(k); + for(int i=0;i a(k*n),b(k*n);ggml_backend_tensor_get(y,a.data(),0,a.size()*4);ggml_backend_tensor_get(z,b.data(),0,b.size()*4);ggml_backend_tensor_get(w,after.data(),0,after.size()*2); + check(raw==after,"BF16 payload mutated");double maxerr=0;size_t different=0; + for(int row=0;row Date: Fri, 4 Sep 2026 22:09:39 -0400 Subject: [PATCH 065/123] fix(ds4): widen BF16 norm vectors at the affine boundary --- server/src/deepseek4/deepseek4_norm.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/deepseek4/deepseek4_norm.h b/server/src/deepseek4/deepseek4_norm.h index 68c0e42f8..a04ef2075 100644 --- a/server/src/deepseek4/deepseek4_norm.h +++ b/server/src/deepseek4/deepseek4_norm.h @@ -4,6 +4,9 @@ namespace dflash::common::detail { inline ggml_tensor * build_rms_norm(ggml_context * ctx, ggml_tensor * x, ggml_tensor * weight, float eps) { + // HIP binary broadcast does not accept BF16 affine operands. Widen only + // this small vector; stored weights and matrix products remain unchanged. + if (weight->type == GGML_TYPE_BF16) weight = ggml_cast(ctx, weight, GGML_TYPE_F32); ggml_tensor * normed = ggml_rms_norm(ctx, x, eps); return ggml_mul(ctx, normed, weight); } From 03edb38deafb6a47a64620a891394ff51a8911bf Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 06:30:17 -0400 Subject: [PATCH 066/123] feat(ds4v): integrate bounded image prompts into HIP serving --- docs/ds4v-image-serving.md | 85 +++ server/CMakeLists.txt | 20 + server/cmake/Ds4vImageCodecs.cmake | 52 ++ server/src/common/backend_args.h | 1 + server/src/common/backend_factory.cpp | 8 + server/src/common/image_prompt.h | 23 + server/src/common/model_backend.h | 20 + server/src/deepseek4/deepseek4_backend.cpp | 403 ++++++++++++- server/src/deepseek4/deepseek4_backend.h | 24 +- server/src/deepseek4/deepseek4_graph.cpp | 147 ++++- .../deepseek4/deepseek4_image_admission.cpp | 376 ++++++++++++ .../src/deepseek4/deepseek4_image_admission.h | 125 ++++ .../deepseek4/deepseek4_image_assembly.cpp | 203 +++++++ .../src/deepseek4/deepseek4_image_assembly.h | 50 ++ server/src/deepseek4/deepseek4_image_budget.h | 23 + server/src/deepseek4/deepseek4_image_spans.h | 57 ++ server/src/deepseek4/deepseek4_internal.h | 16 +- server/src/deepseek4/deepseek4_loader.cpp | 48 ++ server/src/internal.h | 1 + server/src/server/http_server.cpp | 62 +- server/src/server/http_server.h | 3 + server/src/server/image_input.cpp | 53 ++ server/src/server/image_input.h | 19 +- server/src/server/server_main.cpp | 3 + server/test/test_server_unit.cpp | 195 +++++++ server/tests/test_deepseek4_unit.cpp | 551 +++++++++++++++++- .../tools/ds4v_image_assembly/CMakeLists.txt | 12 + server/tools/ds4v_image_assembly/test.cpp | 145 +++++ .../ds4v_image_integration/CMakeLists.txt | 11 + server/tools/ds4v_image_integration/test.cpp | 228 ++++++++ .../ds4v_preprocess_probe/CMakeLists.txt | 35 +- 31 files changed, 2919 insertions(+), 80 deletions(-) create mode 100644 docs/ds4v-image-serving.md create mode 100644 server/cmake/Ds4vImageCodecs.cmake create mode 100644 server/src/common/image_prompt.h create mode 100644 server/src/deepseek4/deepseek4_image_admission.cpp create mode 100644 server/src/deepseek4/deepseek4_image_admission.h create mode 100644 server/src/deepseek4/deepseek4_image_assembly.cpp create mode 100644 server/src/deepseek4/deepseek4_image_assembly.h create mode 100644 server/src/deepseek4/deepseek4_image_budget.h create mode 100644 server/src/deepseek4/deepseek4_image_spans.h create mode 100644 server/tools/ds4v_image_assembly/CMakeLists.txt create mode 100644 server/tools/ds4v_image_assembly/test.cpp create mode 100644 server/tools/ds4v_image_integration/CMakeLists.txt create mode 100644 server/tools/ds4v_image_integration/test.cpp diff --git a/docs/ds4v-image-serving.md b/docs/ds4v-image-serving.md new file mode 100644 index 000000000..f5b5debeb --- /dev/null +++ b/docs/ds4v-image-serving.md @@ -0,0 +1,85 @@ +# DS4V image serving + +The DS4V integration accepts JPEG and PNG images through OpenAI chat +completions when the matching projector is supplied with `--mmproj`. +The current implementation has passed its remote HIP build and CPU integration +checks. Private paired-model HTTP qualification is still pending; a successful +projector export or standalone encoder check does not establish that result. + +## Supported configuration + +The initial serving path requires Linux HIP, a DeepSeek4 decoder with the +supported DS4V dimensions, and two distinct local HIP devices. The decoder uses +sparse prefill with in-process heterogeneous expert ownership: +`DFLASH_DS4_MOE_TP=1`, `DFLASH_DS4_MOE_TP_INPROC=1`, and +`DFLASH_DS4_MOE_TP_GPU` selecting the secondary device. Set `--target-device` +to the primary device, `--ds4-prefill sparse`, and `--mmproj` to the +[exported projector](ds4v-mmproj.md). Device ordinals must match the host's +actual topology. + +Layer splitting, remote expert IPC, all-on-secondary placement, dense prefill, +concurrent sequence scheduling, and upstream forwarding do not support images. +`/props` reports the effective capability in +`capabilities.image_input_supported` after backend initialization. +Without `--mmproj`, text serving follows its existing path and image requests +are rejected. + +## Request contract + +Use `POST /v1/chat/completions` with user-message content parts in display order: + +```json +{ + "messages": [{ + "role": "user", + "content": [ + {"type": "text", "text": "Describe this image."}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}} + ] + }], + "max_tokens": 128 +} +``` + +Only base64 JPEG/PNG data URLs are supported. Remote URLs, images outside user +content arrays, and image parts through other API formats are rejected. +Requests permit at most four images, 16 MiB encoded bytes per image, and +32 MiB combined encoded bytes. Decoder pixel and aspect limits also apply. +The reserved DS4 image marker cannot be supplied as ordinary text. + +One image request may be outstanding per backend. Its admission lease remains +with the immutable payload through queueing and generation; another image +request is rejected until that payload is released. This bounds simultaneous +preprocessing and prepared-image memory. Text requests retain the normal queue. + +The server expands image markers after final rendering and tokenization. +Expanded image tokens count toward context and usage. Image blocks remain +whole during prefill, image rows use their learned routing bias, and raw +attention is bidirectional within each image's visible span. The projector's +tile permutation is applied once when assembling rows with named sentinel +embeddings. All chunks are capped at 1,024 tokens while a projector is loaded. + +Image requests use autoregressive decoding and bypass token-only prefix, +disk, and agent-turn caches, prompt compression, and speculative capture. +Their image payload survives request copies and retry paths. Failed or cancelled +multi-image encoding publishes no partial embedding matrices. + +## Memory and verification + +The projector is validated and loaded before expert placement. Admission counts +actual selected owner tensor sizes, allocation alignment, MIX tables, copy +staging, future KV, and explicit execution reserves. Host and integrated-device +charges share one physical-memory budget. Before image decoding, the server +checks host availability; before encoding, it synchronizes and releases +disposable decoder, owner, and draft graphs and checks live device/host +availability again. KV, saved snapshots, and draft weights remain reflected in +that live measurement. Reservations are conservative policy, not a guarantee +against unrelated concurrent allocations. + +The remote checks include the server unit suite, decoder loader and image-batch +admission tests, synthetic allocation/UMA accounting, preprocessing/codec tests, +and standalone mixed embedding and cancellation tests. Native HIP encoder +comparisons for corn and carrots pass the unchanged feature/embedding gates; +corn also matches the source HIP output exactly and repeats byte for byte. +Full image HTTP behavior, paired runtime resource peaks, and performance require +their separate private serving proof. diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 7db269d12..679a3ce00 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -491,6 +491,14 @@ add_library(dflash_common STATIC src/deepseek4/deepseek4_target_shard_ipc_daemon.cpp src/deepseek4/deepseek4_dspark.cpp src/deepseek4/deepseek4_dspark_spec.cpp + src/deepseek4/deepseek4_vision.cpp + src/deepseek4/deepseek4_vision_decode.cpp + src/deepseek4/deepseek4_vision_preprocess.cpp + src/deepseek4/deepseek4_image_prompt.cpp + src/deepseek4/deepseek4_image_assembly.cpp + src/deepseek4/deepseek4_image_admission.cpp + src/deepseek4/deepseek4_image_policy.cpp + src/server/image_input.cpp src/flashprefill_q8.cpp src/kv_cache.cpp src/kv_quant.cpp @@ -858,6 +866,16 @@ if(DFLASH27B_ENABLE_BSA) endif() endif() +# Production uses the same pinned decoder sources and options as the accepted +# preprocessing probe. The decoder itself has no codec feature macro. +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/Ds4vImageCodecs.cmake") +install(FILES tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md + DESTINATION share/licenses/ds4v + RENAME THIRD_PARTY_NOTICES.md) +install(FILES "${DS4V_JPEG_SOURCE_DIR}/LICENSE.md" + "${DS4V_JPEG_SOURCE_DIR}/README.ijg" + DESTINATION share/licenses/ds4v/libjpeg-turbo) + target_link_libraries(dflash_common PUBLIC ggml @@ -865,6 +883,8 @@ target_link_libraries(dflash_common ggml-base nlohmann_json::nlohmann_json PRIVATE + ds4v_libjpeg + ds4v_lodepng ${CMAKE_DL_LIBS} ) # OpenMP for parallel MoE expert compute kernel (saturate memory bandwidth). diff --git a/server/cmake/Ds4vImageCodecs.cmake b/server/cmake/Ds4vImageCodecs.cmake new file mode 100644 index 000000000..55065dd2d --- /dev/null +++ b/server/cmake/Ds4vImageCodecs.cmake @@ -0,0 +1,52 @@ +# Shared decoder dependencies used by production and the accepted preprocessing +# probe. Keep archive pins and codec options identical to the qualified build. +# License texts remain in tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md +# and the unmodified upstream archives. +include_guard(GLOBAL) + +include(ExternalProject) +include(FetchContent) + +set(DS4V_JPEG_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/libjpeg-turbo-prefix) +set(DS4V_JPEG_ARCHIVE_NAME jpeg) +if(MSVC OR CMAKE_C_SIMULATE_ID STREQUAL "MSVC") + set(DS4V_JPEG_ARCHIVE_NAME jpeg-static) +endif() +set(DS4V_JPEG_ARCHIVE + ${DS4V_JPEG_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}${DS4V_JPEG_ARCHIVE_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}) +file(MAKE_DIRECTORY ${DS4V_JPEG_PREFIX}/include) +ExternalProject_Add(libjpeg_turbo_external + URL https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.1.4.1/libjpeg-turbo-3.1.4.1.tar.gz + URL_HASH SHA256=ecae8008e2cc9ade2f2c1bb9d5e6d4fb73e7c433866a056bd82980741571a022 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE + CMAKE_ARGS + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX=${DS4V_JPEG_PREFIX} + -DCMAKE_INSTALL_LIBDIR=lib + -DENABLE_SHARED=OFF + -DENABLE_STATIC=ON + -DWITH_TOOLS=OFF + -DWITH_TESTS=OFF + -DWITH_SIMD=OFF + -DWITH_TURBOJPEG=OFF + BUILD_COMMAND ${CMAKE_COMMAND} --build --parallel 2 + BUILD_BYPRODUCTS ${DS4V_JPEG_ARCHIVE}) +add_library(ds4v_libjpeg STATIC IMPORTED GLOBAL) +set_target_properties(ds4v_libjpeg PROPERTIES + IMPORTED_LOCATION ${DS4V_JPEG_ARCHIVE} + INTERFACE_INCLUDE_DIRECTORIES ${DS4V_JPEG_PREFIX}/include) +add_dependencies(ds4v_libjpeg libjpeg_turbo_external) + +FetchContent_Declare(lodepng + URL https://github.com/lvandeve/lodepng/archive/ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a.tar.gz + URL_HASH SHA256=c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803 + DOWNLOAD_EXTRACT_TIMESTAMP TRUE) +FetchContent_MakeAvailable(lodepng) +add_library(ds4v_lodepng STATIC ${lodepng_SOURCE_DIR}/lodepng.cpp) +target_include_directories(ds4v_lodepng PUBLIC ${lodepng_SOURCE_DIR}) + +# Expose the original JPEG notices for production installation after the +# dependency build has downloaded its hash-verified source archive. +ExternalProject_Get_Property(libjpeg_turbo_external SOURCE_DIR) +set(DS4V_JPEG_SOURCE_DIR "${SOURCE_DIR}") +unset(SOURCE_DIR) diff --git a/server/src/common/backend_args.h b/server/src/common/backend_args.h index 611dd8d77..57062eaf6 100644 --- a/server/src/common/backend_args.h +++ b/server/src/common/backend_args.h @@ -41,6 +41,7 @@ struct BackendFeatureConfig { struct BackendArgs { // Required const char * model_path = nullptr; // target .gguf + const char * mmproj_path = nullptr; // Optional: speculative decode draft model (qwen35 only) const char * draft_path = nullptr; diff --git a/server/src/common/backend_factory.cpp b/server/src/common/backend_factory.cpp index d462be899..d63dd9f73 100644 --- a/server/src/common/backend_factory.cpp +++ b/server/src/common/backend_factory.cpp @@ -215,6 +215,13 @@ std::unique_ptr create_backend( } const std::string & arch = plan.arch(); + if (args.mmproj_path && *args.mmproj_path && + (arch != "deepseek4" || args.device.is_layer_split() || + args.remote_target_shard.enabled() || args.max_concurrency != 1 || + plan.target_backend() != PlacementBackend::Hip)) { + std::fprintf(stderr, "[backend_factory] --mmproj requires a local single-request DeepSeek4 HIP backend\n"); + return nullptr; + } if (arch.empty()) { std::fprintf(stderr, "[backend_factory] failed to detect architecture from %s\n", @@ -437,6 +444,7 @@ std::unique_ptr create_backend( !args.remote_target_shard.enabled()) { DeepSeek4BackendConfig cfg; cfg.model_path = args.model_path; + cfg.mmproj_path = args.mmproj_path ? args.mmproj_path : ""; cfg.device = args.device; cfg.stream_fd = args.stream_fd; cfg.max_ctx = args.device.max_ctx; diff --git a/server/src/common/image_prompt.h b/server/src/common/image_prompt.h new file mode 100644 index 000000000..8af806acc --- /dev/null +++ b/server/src/common/image_prompt.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include + +namespace dflash::common { + +struct EncodedImage { + std::string mime_type; + std::vector bytes; +}; + +class ImagePromptPayload { +public: + virtual ~ImagePromptPayload() = default; + virtual bool matches(const std::vector & tokens) const = 0; +}; + +using ImagePromptHandle = std::shared_ptr; + +} // namespace dflash::common diff --git a/server/src/common/model_backend.h b/server/src/common/model_backend.h index 221e54ab5..a0f84cf45 100644 --- a/server/src/common/model_backend.h +++ b/server/src/common/model_backend.h @@ -23,6 +23,7 @@ #include "ggml.h" #include "ggml-backend.h" #include "sampler.h" +#include "image_prompt.h" #include "concurrency/seq_engine.h" #include "placement/draft_residency.h" @@ -169,6 +170,7 @@ struct BudgetHook { struct GenerateRequest { std::vector prompt; + ImagePromptHandle images; int n_gen = 0; SamplerCfg sampler; bool do_sample = false; @@ -296,6 +298,24 @@ struct GenerateResult { struct ModelBackend { virtual ~ModelBackend() = default; + virtual bool supports_images() const { return false; } + virtual bool prepare_images(std::vector & tokens, + std::vector images, + uint64_t context_capacity, + uint64_t output_reserve, + ImagePromptHandle & payload, + std::string & error) const { + (void) tokens; + (void) context_capacity; + (void) output_reserve; + if (!images.empty()) { + error = "this backend does not support image input"; + return false; + } + payload.reset(); + return true; + } + // Print the "[-daemon] ready ..." banner on stdout. virtual void print_ready_banner() const = 0; diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index c8750bb28..0d97cb634 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -4,6 +4,11 @@ #include "deepseek4_backend.h" #include "deepseek4_budget_hook.h" #include "deepseek4_internal.h" +#include "deepseek4_image_spans.h" +#include "deepseek4_image_budget.h" +#include "deepseek4_image_assembly.h" +#include "deepseek4_image_admission.h" +#include "deepseek4_vision_decode.h" #include "common/dynamic_backend.h" #include "common/peer_access.h" #include "common/platform_env.h" @@ -30,6 +35,47 @@ namespace dflash::common { +class DeepSeek4ImagePrompt final : public ImagePromptPayload { +public: + bool matches(const std::vector & tokens) const override { + return tokens == prepared_.tokens; + } + vision::ImageSpanView spans() const { return {spans_.data(), spans_.size()}; } + +private: + friend class DeepSeek4Backend; + DeepSeek4ImagePrompt(const DeepSeek4Backend * owner, + vision::PreparedImagePrompt prepared, + std::vector encoded, std::shared_ptr lease) + : owner_(owner), prepared_(std::move(prepared)), encoded_(std::move(encoded)), + lease_(std::move(lease)) { + for (const auto & image : prepared_.images) spans_.push_back(image.layout.span); + } + + bool embed_chunk(const CpuEmbedder & embedder, size_t position, + int count, float * output) const { + if (!output || count <= 0 || embedder.n_embd <= 0) return false; + std::vector result; + std::string error; + const bool ok = vision::embed_image_prompt_chunk( + prepared_, materialized_, embedder.n_vocab, size_t(embedder.n_embd), + position, size_t(count), + [&](const int32_t * ids, size_t n, float * rows) { + return n <= size_t(std::numeric_limits::max()) && + embedder.embed(ids, int(n), rows); + }, result, error); + if (ok) std::copy(result.begin(), result.end(), output); + return ok; + } + + const DeepSeek4Backend * const owner_; + const vision::PreparedImagePrompt prepared_; + const std::vector encoded_; + const std::shared_ptr lease_; + std::vector spans_; + mutable std::vector> materialized_; +}; + namespace { using Clock = std::chrono::steady_clock; @@ -655,6 +701,7 @@ static bool compute_ds4_hybrid_budget_info(const DeepSeek4Weights & w, ggml_backend_t backend, int max_ctx, bool all_cold, + bool with_vision, Ds4HybridBudgetInfo & out, std::string * err) { out = {}; @@ -680,9 +727,12 @@ static bool compute_ds4_hybrid_budget_info(const DeepSeek4Weights & w, // In all-cold mode the KV cache is owned by the secondary (Strix) // backend, so it must not consume the primary GPU's expert budget. const uint64_t main_charge = all_cold ? 0 : out.kv_bytes; - if (out.gpu_total > out.core_bytes + main_charge + out.warm_bytes + out.safety_bytes) { - out.expert_budget = out.gpu_total - out.core_bytes - main_charge - out.warm_bytes - out.safety_bytes; - } + const uint64_t retained_workspace = with_vision && + (vision::detail::hip_bias_launches(backend) || vision::detail::hip_av_launches(backend)) + ? vision::detail::hip_bias_workspace(backend) : 0; + out.expert_budget = vision::remaining_expert_budget( + out.gpu_total, out.core_bytes, main_charge, out.warm_bytes, out.safety_bytes, + with_vision ? vision::SCRATCH_RESERVATION : 0, retained_workspace); if (out.expert_budget > out.mem.total_expert_bytes) { out.expert_budget = out.mem.total_expert_bytes; } @@ -747,6 +797,181 @@ DeepSeek4Backend::~DeepSeek4Backend() { shutdown(); } +bool DeepSeek4Backend::prepare_images( + std::vector & tokens, std::vector images, + uint64_t context_capacity, uint64_t output_reserve, + ImagePromptHandle & payload, std::string & error) const { + if (images.empty()) { + for (int32_t token : tokens) { + if (token == 129264 || token < 0 || token >= 129280) { + error = "unbound image marker or invalid token in rendered prompt"; + return false; + } + } + payload.reset(); + return true; + } + if (!image_capable_) { + error = "image input requires a validated --mmproj projector and heterogeneous HIP sparse prefill"; + return false; + } + try { + if (images.size() > 4) { + error = "too many images in request"; + return false; + } + auto lease = image_request_gate_.try_acquire(); + if (!lease) { + error = "an image request is already in progress; retry after it completes"; + return false; + } + if (!vision::check_deepseek4_image_host_preparation(4ULL * 1024 * 1024 * 1024, error)) { + return false; + } + std::vector patches; + patches.reserve(images.size()); + size_t encoded_bytes = 0; + for (const auto & image : images) { + if (image.bytes.size() > 16ULL * 1024 * 1024 || + encoded_bytes > 32ULL * 1024 * 1024 - image.bytes.size()) { + error = "images exceed request byte limit"; + return false; + } + encoded_bytes += image.bytes.size(); + auto decoded = vision::decode_image({image.bytes.data(), image.bytes.size()}); + if (!decoded) { error = decoded.status.message; return false; } + auto processed = vision::preprocess_rgb(decoded.image.view(), 0); + if (!processed) { error = processed.status.message; return false; } + patches.push_back({processed.image.plan, std::move(processed.image.patches_bf16)}); + } + vision::ImagePromptLimits limits; + limits.context_capacity = context_capacity; + limits.output_reserve = output_reserve; + limits.max_expanded_tokens = std::min(context_capacity, vision::MAX_PREPARED_PROMPT_TOKENS); + auto prepared = vision::prepare_image_prompt(tokens, patches, limits); + if (!prepared) { error = prepared.message; return false; } + auto binding = std::shared_ptr( + new DeepSeek4ImagePrompt(this, std::move(prepared), std::move(images), std::move(lease))); + if (!vision::valid_image_spans(binding->spans(), binding->prepared_.tokens.size())) { + error = "invalid prepared image spans"; + return false; + } + std::vector expanded = binding->prepared_.tokens; + tokens.swap(expanded); + payload = std::move(binding); + return true; + } catch (const std::bad_alloc &) { + error = "image preparation allocation failed"; + return false; + } +} + +bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, + const DaemonIO & io, std::string & error) { + if (io.is_cancelled()) return false; + if (images.owner_ != this || !vision_ || parked_) { + error = "image binding does not belong to the loaded backend"; + return false; + } + ggml_backend_synchronize(backend_); + if (expert_backend_) ggml_backend_synchronize(expert_backend_); + if (spec_backend_) ggml_backend_synchronize(spec_backend_); + deepseek4_release_image_scratch(cache_, moe_hybrid_.get()); + reset_deepseek4_dspark_runtime_cache(); + auto reserves = image_reserves_; + const uint64_t resident_workspace = + (vision::detail::hip_bias_launches(backend_) || vision::detail::hip_av_launches(backend_)) + ? vision::detail::hip_bias_workspace(backend_) : 0; + if (resident_workspace > vision::SCRATCH_RESERVATION) { + error = "resident vision workspace exceeds its reservation"; + return false; + } + // Current free memory already reflects weights, KV, optional drafter, + // snapshots and retained backend pools. Charge only upcoming work here. + reserves.primary_future_bytes = vision::SCRATCH_RESERVATION - resident_workspace + + 128ULL * 1024 * 1024; + vision::ImageAdmissionReport report; + MoeHybridConfig runtime_cfg = make_ds4_parent_worker_cfg(w_); + runtime_cfg.materialize_cold_experts = true; + runtime_cfg.cold_expert_backend = MoeHybridColdBackend::Gpu; + if (!vision::check_deepseek4_image_runtime_admission(runtime_cfg, + backend_, expert_backend_, reserves, report, error)) { + std::fprintf(stderr, + "[deepseek4] image runtime admission failed: primary required/free=%.3f/%.3f GiB " + "cold required/free=%.3f/%.3f GiB host required/available=%.3f/%.3f GiB: %s\n", + gib(report.primary_required_bytes), gib(report.primary_free_bytes), + gib(report.cold_required_bytes), gib(report.cold_free_bytes), + gib(report.host_required_bytes), gib(report.host_available_bytes), error.c_str()); + return false; + } + if (!images.materialized_.empty()) return true; + struct ReleaseScratch { + vision::VisionRuntime & runtime; + ~ReleaseScratch() { runtime.release_scratch(); } + } release{*vision_}; + try { + vision::ImageSentinels sentinels; + if (!vision_->sentinel(vision::Sentinel::Start, sentinels.start, error) || + !vision_->sentinel(vision::Sentinel::Pad, sentinels.pad, error) || + !vision_->sentinel(vision::Sentinel::Newline, sentinels.newline, error) || + !vision_->sentinel(vision::Sentinel::End, sentinels.end, error)) return false; + return vision::materialize_image_rows( + images.prepared_.images, sentinels, size_t(w_.n_embd), + [&](const vision::PromptImage & image, vision::ImageRaster & raster, + std::string & encode_error) { + std::vector patches(image.input.patches_bf16.size()); + for (size_t i = 0; i < patches.size(); ++i) { + const uint32_t bits = uint32_t(image.input.patches_bf16[i]) << 16; + std::memcpy(&patches[i], &bits, sizeof(bits)); + } + vision::VisionOutput output; + if (!vision_->encode(patches, + {int(image.input.plan.vit_rows), int(image.input.plan.vit_cols)}, + output, encode_error)) return false; + if (output.rows <= 0 || output.columns != w_.n_embd) { + encode_error = "vision output shape differs from decoder dimensions"; + return false; + } + raster = {size_t(output.rows), size_t(output.columns), std::move(output.embeddings)}; + return true; + }, [&] { return io.is_cancelled(); }, images.materialized_, error); + } catch (const std::bad_alloc &) { + error = "image materialization allocation failed"; + return false; + } +} + +bool DeepSeek4Backend::load_vision() { + if (cfg_.mmproj_path.empty()) return true; + if (w_.n_layer != 43 || w_.n_embd != 4096 || w_.n_vocab != 129280 || + w_.n_expert != 256 || w_.n_expert_used != 6 || w_.n_hash_layer != 3 || w_.n_swa != 128) { + std::fprintf(stderr, "[deepseek4] projector requires the supported DS4V decoder dimensions\n"); + return false; + } + for (const auto & layer : w_.layers) { + const auto bias = layer.ffn_gate_bias_vl; + if (!bias || bias->type != GGML_TYPE_F32 || bias->ne[0] != 256 || + ggml_nelements(bias) != 256) return false; + std::array values; + ggml_backend_tensor_get(bias, values.data(), 0, sizeof(values)); + if (!std::all_of(values.begin(), values.end(), [](float v) { return std::isfinite(v); })) { + std::fprintf(stderr, "[deepseek4] nonfinite image router bias\n"); + return false; + } + } + auto runtime = std::make_unique(); + std::string error; + if (!runtime->load(cfg_.mmproj_path, backend_, w_.n_embd, w_.n_vocab, error)) { + std::fprintf(stderr, "[deepseek4] projector load failed: %s\n", error.c_str()); + return false; + } + std::fprintf(stderr, + "[deepseek4] vision weights=%.3f GiB scratch reservation=%.3f GiB before expert placement\n", + gib(runtime->weight_bytes()), gib(vision::SCRATCH_RESERVATION)); + vision_ = std::move(runtime); + return true; +} + bool DeepSeek4Backend::requires_monolithic_model() const { return cfg_.fused_decode || cfg_.fused_verify_f16_kv || prefill_attention_mode_is_approximate(cfg_.prefill_mode); @@ -787,6 +1012,18 @@ bool DeepSeek4Backend::load_model() { // disable the requested split before the TP runtime can initialize. const bool force_full = env_flag_enabled("DFLASH_DS4_FORCE_FULL_LOAD"); const bool heterogeneous_tp = env_flag_enabled("DFLASH_DS4_MOE_TP"); + if (!cfg_.mmproj_path.empty()) { + const auto tp = ds4_moe_tp_config(cfg_.device.gpu); + if (target_backend != PlacementBackend::Hip || cfg_.device.is_layer_split() || + cfg_.prefill_mode != PrefillAttentionMode::Sparse || + !tp.requested || !tp.in_process || !tp.backend_valid || + tp.secondary_backend != PlacementBackend::Hip || + tp.secondary_gpu == cfg_.device.gpu || tp.all_on_secondary || force_full || + env_flag_enabled("DFLASH_DS4_DENSE_TP_MASK")) { + std::fprintf(stderr, "[deepseek4] --mmproj requires sparse prefill with distinct local HIP expert owners\n"); + return false; + } + } const bool need_monolithic = requires_monolithic_model() && !heterogeneous_tp; if (target_backend == PlacementBackend::Hip && @@ -1115,6 +1352,7 @@ bool DeepSeek4Backend::init() { std::fprintf(stderr, "[deepseek4] DFLASH_DS4_SPEC set but DFLASH_DS4_DRAFT gguf missing\n"); } } + image_capable_ = vision_ != nullptr; return true; } @@ -1189,7 +1427,7 @@ bool DeepSeek4Backend::compute_uniform_hybrid_placement(const DeepSeek4Weights & Ds4HybridBudgetInfo budget; const Ds4MoeTpConfig tp = ds4_moe_tp_config(cfg_.device.gpu); if (!compute_ds4_hybrid_budget_info(w, backend_, max_ctx, - tp.all_on_secondary, budget, err)) { + tp.all_on_secondary, vision_ != nullptr, budget, err)) { return false; } @@ -1408,12 +1646,15 @@ bool DeepSeek4Backend::compute_uniform_hybrid_placement(const DeepSeek4Weights & bool DeepSeek4Backend::init_hybrid_model() { TargetLoadPlan plan; plan.skip_expert_tensors = true; + plan.load_ds4_image_bias = !cfg_.mmproj_path.empty(); if (!load_deepseek4_gguf_partial(cfg_.model_path, backend_, plan, w_)) { std::fprintf(stderr, "[deepseek4] failed to partially load model for hybrid mode: %s\n", cfg_.model_path); return false; } + if (!load_vision()) return false; + std::string err; const int max_ctx = cfg_.max_ctx > 0 ? cfg_.max_ctx : 8192; if (!compute_uniform_hybrid_placement( @@ -1423,6 +1664,11 @@ bool DeepSeek4Backend::init_hybrid_model() { } if (moe_placement_.total_hot >= w_.n_layer * w_.n_expert) { + if (vision_) { + std::fprintf(stderr, "[deepseek4] image prefill requires resident cold experts on the secondary HIP device\n"); + return false; + } + vision_.reset(); free_deepseek4_weights(w_); if (!load_deepseek4_gguf(cfg_.model_path, backend_, w_)) { std::fprintf(stderr, "[deepseek4] failed to reload full model after placement: %s\n", @@ -1470,6 +1716,7 @@ bool DeepSeek4Backend::init_hybrid_model() { std::fprintf(stderr, "[deepseek4] %s experts cannot decode from hybrid/cold " "placement; falling back to monolithic full load\n", m.what); + vision_.reset(); free_deepseek4_weights(w_); if (!load_deepseek4_gguf(cfg_.model_path, backend_, w_)) { std::fprintf(stderr, @@ -1545,6 +1792,55 @@ bool DeepSeek4Backend::init_hybrid_model() { hybrid_cfg.materialize_cold_experts = true; hybrid_cfg.cold_expert_backend = MoeHybridColdBackend::Gpu; } + if (vision_) { +#if defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP) + hipDeviceProp_t primary_properties{}, cold_properties{}; + if (hipGetDeviceProperties(&primary_properties, cfg_.device.gpu) != hipSuccess || + hipGetDeviceProperties(&cold_properties, tp.secondary_gpu) != hipSuccess) { + std::fprintf(stderr, "[deepseek4] cannot classify image owner memory domains\n"); + return fail_hybrid_init(); + } + const bool unified = std::getenv("GGML_CUDA_ENABLE_UNIFIED_MEMORY") != nullptr; + const uint64_t resident_workspace = + (vision::detail::hip_bias_launches(backend_) || vision::detail::hip_av_launches(backend_)) + ? vision::detail::hip_bias_workspace(backend_) : 0; + if (resident_workspace > vision::SCRATCH_RESERVATION) return fail_hybrid_init(); + constexpr uint64_t mib = 1024ULL * 1024; + vision::ImageAdmissionReserves reserves; + reserves.primary_domain = primary_properties.integrated || unified + ? vision::ImageMemoryDomain::HostShared : vision::ImageMemoryDomain::Dedicated; + reserves.cold_domain = cold_properties.integrated || unified + ? vision::ImageMemoryDomain::HostShared : vision::ImageMemoryDomain::Dedicated; + reserves.duplicate_hot_on_cold = env_flag_enabled("DFLASH_MOE_DUPLICATE_HOT_ON_COLD"); + reserves.primary_future_bytes = estimate_ds4_cache_bytes(w_, max_ctx) + + vision::SCRATCH_RESERVATION - resident_workspace + (256 + 512) * mib; + reserves.cold_future_bytes = 512 * mib; + reserves.cold_runtime_reservation_bytes = 2048 * mib; + reserves.max_chunk_tokens = 1024; + // The image gate retains one request through preprocessing and serving. + // These are named headroom reservations; admission also accounts for + // exact selected storage, copy growth, and shared physical host RAM. + reserves.host_request_bytes = 4096 * mib; + reserves.host_loader_overhead_bytes = 1024 * mib; + reserves.host_runtime_bytes = 512 * mib; + vision::ImageAdmissionReport report; + const bool admitted = vision::check_deepseek4_image_admission( + w_, moe_placement_, hybrid_cfg, backend_, expert_backend_, reserves, report, err); + std::fprintf(stderr, + "[deepseek4] image memory admission: primary required/free=%.3f/%.3f GiB " + "cold required/free=%.3f/%.3f GiB host+UMA required/available=%.3f/%.3f GiB " + "cold activation estimate/reservation=%.3f/%.3f GiB result=%s\n", + gib(report.primary_required_bytes), gib(report.primary_free_bytes), + gib(report.cold_required_bytes), gib(report.cold_free_bytes), + gib(report.host_required_bytes), gib(report.host_available_bytes), + gib(report.cold_activation_estimate_bytes), gib(report.cold_runtime_reservation_bytes), + admitted ? "admitted" : err.c_str()); + if (!admitted) return fail_hybrid_init(); + image_reserves_ = reserves; +#else + return fail_hybrid_init(); +#endif + } if (!build_deepseek4_moe_hybrid_storage_from_file_with_mmap( cfg_.model_path, backend_, w_, moe_placement_, &hybrid_cfg, *hybrid, &err, expert_backend_)) { @@ -1671,6 +1967,7 @@ bool DeepSeek4Backend::init_hybrid_model() { void DeepSeek4Backend::print_ready_banner() const { std::printf("[deepseek4-daemon] ready layers=%d ctx=%d experts=%d/%d\n", w_.n_layer, cache_.max_ctx, w_.n_expert_used, w_.n_expert); + if (image_capable_) std::printf("[deepseek4] image input ready mmproj=%s\n", cfg_.mmproj_path.c_str()); std::fflush(stdout); } @@ -1701,6 +1998,7 @@ bool DeepSeek4Backend::park(ParkTarget target) { } moe_placement_ = {}; moe_decode_placement_ = {}; + vision_.reset(); free_deepseek4_weights(w_); parked_ = true; if (spec_drafter_) { @@ -1720,6 +2018,7 @@ bool DeepSeek4Backend::unpark(ParkTarget target) { if (want_target_model && parked_) { if (!load_model()) { std::fprintf(stderr, "[deepseek4] unpark: failed to restore target model\n"); + vision_.reset(); free_deepseek4_weights(w_); stream_engine_.destroy(); moe_hybrid_.reset(); @@ -1738,6 +2037,7 @@ bool DeepSeek4Backend::unpark(ParkTarget target) { "[deepseek4] unpark: failed to recreate KV cache (ctx=%d)\n", max_ctx); free_deepseek4_cache(cache_); + vision_.reset(); free_deepseek4_weights(w_); stream_engine_.destroy(); moe_hybrid_.reset(); @@ -1753,6 +2053,7 @@ bool DeepSeek4Backend::unpark(ParkTarget target) { if (env_flag_enabled("DFLASH_DS4_MOE_TP") && !init_moe_tensor_parallel()) { free_deepseek4_cache(cache_); + vision_.reset(); free_deepseek4_weights(w_); expert_runtime_.reset(); stream_engine_.destroy(); @@ -1771,6 +2072,7 @@ bool DeepSeek4Backend::unpark(ParkTarget target) { std::fflush(stdout); } if (!validate_prefill_mode()) { + vision_.reset(); free_deepseek4_weights(w_); stream_engine_.destroy(); moe_hybrid_.reset(); @@ -1862,7 +2164,10 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, const DaemonIO & io, int kv_offset, int snap_slot, - int snap_pos) { + int snap_pos, + const DeepSeek4ImagePrompt * images) { + const bool capture_spec = !images && spec_enabled_ && spec_drafter_; + if (images) spec_feat_window_.clear(); const InferencePhase phase = deepseek4_roctx_prefill_phase( prefill_attention_mode_name(cfg_.prefill_mode)); const DeepSeek4RoctxPhaseScope roctx_phase(phase); @@ -1889,7 +2194,9 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, // Bound the layer-major graph to the topology validated by the prefill // kernels. Smaller tail chunks use the same scheduler or its reference // fallback. - const int layer_major_cap = DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS; + const int layer_major_cap = vision_ + ? std::min(1024, DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS) + : DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS; // Only sparse prefill has a qualified batched mixed-owner HC path. Dense // hybrid execution remains tokenwise; batching it would skip per-token HC // post-mixing and corrupt the hidden state. @@ -1921,15 +2228,35 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, base_chunk, chunk, kv_offset + n_total); } int pos = kv_offset; + const int image_capacity = std::min(1024, + deepseek4_hybrid_prefill_chunk_tokens(layer_major_cap, kv_offset + n_total)); const bool save_snapshot = - snap_slot >= 0 && snap_slot < PREFIX_SLOTS && + !images && snap_slot >= 0 && snap_slot < PREFIX_SLOTS && snap_pos > kv_offset && snap_pos <= kv_offset + n_total; + if (images) { + if (kv_offset != 0 || !images->matches(tokens)) return -1; + for (int offset = 0; offset < n_total;) { + const int proposed = deepseek4_hybrid_prefill_step_tokens(chunk, offset, n_total - offset); + const int count = vision::atomic_image_chunk(images->spans(), uint64_t(offset), + proposed, uint64_t(n_total - offset), image_capacity); + bool has_images = false; + std::string error; + if (!count || !deepseek4_validate_image_batch(w_, cache_, moe_hybrid_.get(), + tokens.data() + offset, count, offset, images->spans(), has_images, error)) { + std::fprintf(stderr, "[deepseek4] image chunk admission failed: %s\n", + error.empty() ? "complete image exceeds configured chunk capacity" : error.c_str()); + return -1; + } + offset += count; + } + } // New sequence: clear the cache buffer so compressor state double-buffers // and compressed-KV rows start from zeros, exactly like a fresh server. // Without this, the first flush windows of a request pool over the // previous request's leftover state rows and outputs from the 2nd/3rd // request on can drift by a token or two. if (kv_offset == 0) { + cache_has_images_ = images != nullptr; reset_deepseek4_cache(cache_); } last_logits_.clear(); @@ -1938,7 +2265,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, int spec_snap_from = n_total; int spec_snap_to = 0; int spec_old_rows_for_final = 0; - if (spec_enabled_ && spec_drafter_) { + if (capture_spec) { const int feat_row = spec_drafter_->n_target_layers * w_.n_embd; const int snap_tokens = save_snapshot ? snap_pos - kv_offset : n_total; spec_final_from = std::max(0, n_total - w_.n_swa); @@ -1999,7 +2326,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, // there and a full 256-expert duplicate stack makes that tail far more // expensive than slightly rebalancing the preceding chunk. const int tail_tokens = n_total - (i + n_tok); - if (moe_hybrid_ && spec_enabled_ && spec_drafter_ && + if (moe_hybrid_ && capture_spec && tail_tokens > 0 && tail_tokens < 512 && n_tok >= 1024 - tail_tokens) { n_tok -= 512 - tail_tokens; @@ -2010,7 +2337,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, snap_pos > pos && snap_pos < pos + n_tok) { n_tok = snap_pos - pos; } - if (spec_enabled_ && spec_drafter_) { + if (capture_spec) { const bool batch_final_capture = supports_batched_spec_feature_capture( w_.moe_hybrid, cache_.prefill_mode, n_tok); @@ -2020,6 +2347,12 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, spec_snap_from, spec_snap_to); } + if (images) { + n_tok = vision::atomic_image_chunk(images->spans(), uint64_t(pos), n_tok, + uint64_t(n_total - i), image_capacity); + if (!n_tok) return -1; + } + // Bulk prompt graphs and the final DSpark feature-capture graph have // different HC/owner arena shapes. Once all earlier chunks are // complete, retire their reusable prefill arenas before entering the @@ -2027,7 +2360,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, // than 300 MiB and needlessly makes the highest-throughput expert // placement fail only on the last chunk. const bool entering_final_capture_band = - spec_enabled_ && spec_drafter_ && i > 0 && + capture_spec && i > 0 && i + n_tok > spec_final_from; if (entering_final_capture_band && !capture_band_scratch_released) { @@ -2041,7 +2374,10 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, // Embed tokens std::vector embed(w_.n_embd * n_tok); const auto embed_t0 = Clock::now(); - w_.embedder.embed(tokens.data() + i, n_tok, embed.data()); + const bool embedded = images + ? images->embed_chunk(w_.embedder, size_t(i), n_tok, embed.data()) + : w_.embedder.embed(tokens.data() + i, n_tok, embed.data()); + if (!embedded) return -1; DeepSeek4StepTelemetry step_tel; if (timing) step_tel.embed_us = elapsed_us(embed_t0, Clock::now()); @@ -2055,7 +2391,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, const bool capture_snapshot = !snapshot_saved && i < spec_snap_to && i + n_tok > spec_snap_from; - if (spec_enabled_ && spec_drafter_ && + if (capture_spec && (capture_final || capture_snapshot)) { spec_hooks.capture_layer_ids = &spec_drafter_->capture_layer_ids; spec_hooks.capture_out = &spec_cap; @@ -2093,7 +2429,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, /*allow_decode_graph_reuse=*/true, hp, moe_hybrid_.get(), expert_runtime_.compute ? &expert_runtime_ : nullptr, - routing_stats_.get()); + routing_stats_.get(), images ? images->spans() : vision::ImageSpanView{}); } else if (moe_hybrid_) { ok = deepseek4_step(backend_, cfg_.device.gpu, w_, cache_, embed.data(), n_tok, pos, logits, moe_hybrid_.get(), tokens.data() + i, @@ -2142,7 +2478,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, std::fprintf(stderr, "[deepseek4] failed to save snapshot slot=%d pos=%d\n", snap_slot, snap_pos); - } else if (spec_enabled_ && spec_drafter_) { + } else if (capture_spec) { // Discard checkpoint-only rows once their snapshot is saved. // Retain just the already-captured prefix of the final SWA // window, so distant checkpoints do not bridge a huge gap in @@ -2159,7 +2495,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, } keep_spec_feature_tail(spec_feat_window_, (size_t) std::max(0, w_.n_swa)); - if (timing && spec_enabled_ && spec_drafter_ && + if (timing && capture_spec && !spec_feat_window_.empty()) { size_t nonfinite = 0; double square_sum = 0.0; @@ -2186,7 +2522,7 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, if (timing) { log_step_tel("prefill", n_total, steps, elapsed_s(phase_t0), tel_acc); } - if (spec_enabled_ && spec_drafter_) { + if (capture_spec) { deepseek4_release_prefill_scratch(cache_, moe_hybrid_.get()); } return pos; @@ -2355,13 +2691,35 @@ GenerateResult DeepSeek4Backend::generate_from_state( return result; } + const auto * images = dynamic_cast(req.images.get()); + if (req.images) { + if (!images || images->owner_ != this || !images->matches(req.prompt) || + kv_offset != 0 || req.snap_slot >= 0 || req.snap_pos >= 0 || + req.prompt.size() + uint64_t(std::max(0, req.n_gen)) > uint64_t(cache_.max_ctx) || + !moe_hybrid_ || !expert_backend_ || expert_runtime_.compute) { + result.fail(GenerateErrorCode::PrefillFailed, "image request binding, context, or execution mode is invalid"); + return result; + } + std::string error; + if (!materialize_images(*images, out_io, error)) { + if (out_io.is_cancelled()) { result.succeed(); return result; } + result.fail(GenerateErrorCode::PrefillFailed, error.empty() ? "image materialization failed" : error); + return result; + } + } else if (std::any_of(req.prompt.begin(), req.prompt.end(), [&](int32_t token) { + return token < 0 || token >= w_.n_vocab || token == 129264; + })) { + result.fail(GenerateErrorCode::PrefillFailed, "unbound image marker or invalid prompt token"); + return result; + } + // Prefill only the suffix that is not already represented by a restored // snapshot. An exact full-prompt hit can decode immediately from the // logits and speculative feature window saved with the cache state. int committed = kv_offset; if (kv_offset == 0) { committed = do_prefill(req.prompt, out_io, 0, - req.snap_slot, req.snap_pos); + req.snap_slot, req.snap_pos, images); } else if (kv_offset < (int) req.prompt.size()) { std::vector suffix(req.prompt.begin() + kv_offset, req.prompt.end()); @@ -2412,7 +2770,7 @@ GenerateResult DeepSeek4Backend::generate_from_state( } } if (spec_enabled_ && spec_drafter_ && req.n_gen > 0 && - !req.force_ar_decode && !budget_requires_ar && !sampling_requires_ar) { + !req.images && !req.force_ar_decode && !budget_requires_ar && !sampling_requires_ar) { if (last_logits_.empty()) { result.fail(GenerateErrorCode::DecodeFailed, "spec: no prefill logits"); return result; @@ -2497,6 +2855,7 @@ GenerateResult DeepSeek4Backend::generate_from_state( // ── Snapshots ─────────────────────────────────────────────────────────── bool DeepSeek4Backend::snapshot_save(int slot) { + if (cache_has_images_) return false; if (slot < 0 || slot >= PREFIX_SLOTS || !snap_backend_ || cache_.cur_pos <= 0 || last_logits_pos_ != cache_.cur_pos || w_.n_vocab <= 0 || @@ -2571,12 +2930,17 @@ bool DeepSeek4Backend::snapshot_restore(int slot) { last_logits_ = std::move(restored_logits); spec_feat_window_ = std::move(restored_features); last_logits_pos_ = cache_.cur_pos; + cache_has_images_ = false; return true; } GenerateResult DeepSeek4Backend::restore_and_generate_impl( int slot, const GenerateRequest & req, const DaemonIO & io) { GenerateResult result; + if (req.images) { + result.fail(GenerateErrorCode::PrefillFailed, "image requests cannot restore token-only snapshots"); + return result; + } if (!snapshot_used(slot)) { result.fail(GenerateErrorCode::InvalidSnapshotSlot); return result; @@ -2639,6 +3003,7 @@ void DeepSeek4Backend::shutdown() { routing_stats_out_path_.clear(); moe_placement_ = {}; moe_decode_placement_ = {}; + vision_.reset(); free_deepseek4_weights(w_); if (snap_backend_) { ggml_backend_free(snap_backend_); snap_backend_ = nullptr; } if (backend_) { ggml_backend_free(backend_); backend_ = nullptr; } diff --git a/server/src/deepseek4/deepseek4_backend.h b/server/src/deepseek4/deepseek4_backend.h index 583132042..a57ec01ac 100644 --- a/server/src/deepseek4/deepseek4_backend.h +++ b/server/src/deepseek4/deepseek4_backend.h @@ -15,6 +15,10 @@ #include "../common/moe_hybrid_stream.h" #include "deepseek4_internal.h" #include "deepseek4_dspark.h" +#include "deepseek4_vision.h" +#include "deepseek4_image_prompt.h" +#include "deepseek4_image_assembly.h" +#include "deepseek4_image_admission.h" #include "ggml.h" #include "ggml-backend.h" @@ -26,6 +30,8 @@ namespace dflash::common { +class DeepSeek4ImagePrompt; + // Bounds the sparse heterogeneous prefill arena once accumulated attention // context dominates its memory footprint. Decode batching is unaffected. int deepseek4_hybrid_prefill_chunk_tokens( @@ -53,6 +59,13 @@ class DeepSeek4Backend : public ModelBackend { // ModelBackend interface void print_ready_banner() const override; + bool supports_images() const override { return image_capable_; } + bool prepare_images(std::vector & tokens, + std::vector images, + uint64_t context_capacity, + uint64_t output_reserve, + ImagePromptHandle & payload, + std::string & error) const override; bool park(ParkTarget target) override; bool unpark(ParkTarget target) override; @@ -88,6 +101,11 @@ class DeepSeek4Backend : public ModelBackend { DeepSeek4Weights w_; DeepSeek4Cache cache_; bool parked_ = false; + bool image_capable_ = false; + bool cache_has_images_ = false; + std::unique_ptr vision_; + vision::ImageRequestGate image_request_gate_; + vision::ImageAdmissionReserves image_reserves_; // Sampler SamplerCfg sampler_; @@ -141,7 +159,11 @@ class DeepSeek4Backend : public ModelBackend { // Prefill prompt tokens in chunks, return absolute committed position. int do_prefill(const std::vector & tokens, const DaemonIO & io, - int kv_offset = 0, int snap_slot = -1, int snap_pos = -1); + int kv_offset = 0, int snap_slot = -1, int snap_pos = -1, + const DeepSeek4ImagePrompt * images = nullptr); + bool load_vision(); + bool materialize_images(const DeepSeek4ImagePrompt & images, + const DaemonIO & io, std::string & error); // Generate after either a fresh prefill or a restored prefix. kv_offset is // the number of prompt tokens already represented by cache_ and the diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index 633fa3ece..db42dd83a 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -10,6 +10,8 @@ #include "deepseek4_internal.h" #include "deepseek4_norm.h" +#include "deepseek4_image_policy.h" +#include "deepseek4_vision.h" #include "deepseek4_hc_cuda.h" #include "deepseek4_roctx.h" #include "internal.h" @@ -1628,7 +1630,8 @@ static ggml_tensor * build_mla_attention( std::vector & i32_array_inputs, std::vector & i64_array_inputs, std::vector * f32_array_inputs = nullptr, - DeepSeek4AttentionImpl attention_impl = DeepSeek4AttentionImpl::Explicit) { + DeepSeek4AttentionImpl attention_impl = DeepSeek4AttentionImpl::Explicit, + vision::ImageSpanView image_spans = {}) { const int n_embd = w.n_embd; const int head_dim = w.head_dim; @@ -1702,7 +1705,7 @@ static ggml_tensor * build_mla_attention( // steps on this path; DFLASH_DS4_NO_CAUSAL_VERIFY=1 restores the legacy // (bidirectional) behavior for A/B comparison. const bool causal_batch = (n_tokens > 1) && !cached_inputs && f32_array_inputs && - !ds4_env_flag("DFLASH_DS4_NO_CAUSAL_VERIFY"); + (image_spans.size || !ds4_env_flag("DFLASH_DS4_NO_CAUSAL_VERIFY")); const bool layer_major_batch = causal_batch && attention_impl != DeepSeek4AttentionImpl::Explicit; ggml_tensor * old_rows_scratch = nullptr; @@ -2007,6 +2010,7 @@ static ggml_tensor * build_mla_attention( // [n_kv,n_query] F16; the explicit path broadcasts the same values over // heads in F32. ggml_tensor * score_mask = nullptr; + int raw_score_capacity = w.n_swa; const bool exact_two_band = attention_impl == DeepSeek4AttentionImpl::DenseFlash && causal_batch && @@ -2028,15 +2032,29 @@ static ggml_tensor * build_mla_attention( const int pos_i = kv_start + i; float * col = mvals.data() + (size_t) i * n_attn; const int min_pos = pos_i - w.n_swa + 1; + const auto * image = vision::image_block_at(image_spans, uint64_t(pos_i)); + const int64_t image_begin = image ? int64_t(image->visible_begin) : -1; + const int64_t image_end = image ? int64_t(image->visible_end) : -1; for (int r = 0; r < n_prior_rows; ++r) { const int prior_pos = kv_start - n_prior_rows + r; - if (prior_pos < min_pos) col[r] = -1e30f; + bool visible = prior_pos >= min_pos; + if (image_spans.size && !vision::raw_key_visible( + pos_i, prior_pos, w.n_swa, image_begin, image_end, visible)) return nullptr; + if (!visible) col[r] = -1e30f; } for (int t = 0; t < n_tokens; ++t) { const int current_pos = kv_start + t; - if (t > i || current_pos < min_pos) { - col[n_prior_rows + t] = -1e30f; + bool visible = t <= i && current_pos >= min_pos; + if (image_spans.size && !vision::raw_key_visible( + pos_i, current_pos, w.n_swa, image_begin, image_end, visible)) return nullptr; + if (!visible) col[n_prior_rows + t] = -1e30f; + } + if (image_spans.size) { + int first = n_raw, last = -1; + for (int r = 0; r < n_raw; ++r) { + if (col[r] == 0.0f) { first = std::min(first, r); last = r; } } + raw_score_capacity = std::max(raw_score_capacity, last - first + 1); } if (n_comp_attn > 0) { const int vis = ds4_comp_rows_used(lc.comp_kv, lc.n_comp, ratio, pos_i); @@ -2256,7 +2274,7 @@ static ggml_tensor * build_mla_attention( // dense attention unchanged while allowing the D=512 value pass to // skip the two masked envelopes without guessing DS4 cache layout. ggml_flash_attn_ext_set_ds4_sparse( - context, n_raw, w.n_swa, + context, n_raw, raw_score_capacity, indexer_topk ? -w.n_indexer_top_k : attention_impl == DeepSeek4AttentionImpl::SparseFlash @@ -5683,7 +5701,8 @@ static bool eval_ds4_layer_range_hybrid_ffn( MoeHybridRoutingStats * routing_stats, std::vector & out, DeepSeek4StepTelemetry * telemetry, - const MoeHybridDeviceOutputs * device_outputs = nullptr) { + const MoeHybridDeviceOutputs * device_outputs = nullptr, + int kv_start = 0, vision::ImageSpanView image_spans = {}) { const bool trace_prefill = ds4_env_flag("DFLASH_DS4_PREFILL_TRACE"); if (trace_prefill) { std::fprintf(stderr, @@ -5852,6 +5871,13 @@ static bool eval_ds4_layer_range_hybrid_ffn( ggml_backend_tensor_get(L.ffn_exp_probs_b, bias.data(), 0, sizeof(float) * bias.size()); } + std::vector image_bias; + if (image_spans.size) { + if (!L.ffn_gate_bias_vl) return false; + image_bias.resize(size_t(w.n_expert)); + ggml_backend_tensor_get(L.ffn_gate_bias_vl, image_bias.data(), 0, + sizeof(float) * image_bias.size()); + } const auto route_select_t0 = Ds4TimingClock::now(); for (int t = 0; t < n_tokens; ++t) { @@ -5862,6 +5888,21 @@ static bool eval_ds4_layer_range_hybrid_ffn( float * token_weights = weights.data() + (size_t)t * (size_t)route_width; + if (vision::image_block_at(image_spans, uint64_t(kv_start + t))) { + vision::ImageExpertSelection selection; + std::string error; + if (!vision::select_image_experts(token_probs, image_bias.data(), + size_t(w.n_expert), size_t(route_width), selection, error, + w.expert_weight_scale)) { + std::fprintf(stderr, "[deepseek4] image routing failed: %s\n", error.c_str()); + return false; + } + std::copy_n(selection.indices.data(), route_width, token_ids_out); + std::copy_n(selection.weights.data(), route_width, token_weights); + observe_active_routing(routing_stats, layer, token_ids_out, token_weights, route_width); + continue; + } + if (hash_routed) { const int32_t tok = token_ids[t]; if (tok < 0 || tok >= w.n_vocab) return false; @@ -6860,6 +6901,64 @@ static bool initialize_layer_range_cache( runtime.owns_output = owns_output; return true; } +bool deepseek4_validate_image_batch( + const DeepSeek4Weights & w, const DeepSeek4Cache & cache, + const MoeHybridStorage * hybrid, const int32_t * tokens, + int count, int position, vision::ImageSpanView spans, + bool & has_images, std::string & error) { + has_images = false; + const auto fail = [&](const char * message) { error = message; return false; }; + if (count <= 0 || position < 0 || int64_t(position) + count > cache.max_ctx || + !vision::valid_image_spans(spans, uint64_t(std::max(0, cache.max_ctx)))) + return fail("invalid image batch bounds"); + const uint64_t end = uint64_t(position) + uint64_t(count); + for (size_t i = 0; i < spans.size; ++i) { + const auto & span = spans.data[i]; + if (span.block_begin >= end || span.block_end <= uint64_t(position)) continue; + if (span.block_begin < uint64_t(position) || span.block_end > end) + return fail("prefill batch would split an image block"); + has_images = true; + } + if (has_images && !tokens) return fail("image batch requires bound token IDs"); + if (tokens) { + for (int i = 0; i < count; ++i) { + const bool image_row = vision::image_block_at(spans, uint64_t(position) + uint64_t(i)); + const int32_t token = tokens[i]; + if (image_row ? (token < w.n_vocab || int64_t(token) >= int64_t(w.n_vocab) + 5) + : (token < 0 || token >= w.n_vocab)) + return fail("token IDs do not match image block positions"); + } + } + if (!has_images) return true; + if (!hybrid || !w.moe_hybrid || !hybrid->materialized_cold_experts || + hybrid->cold_backend_kind != MoeHybridColdBackend::Gpu || !hybrid->cold_backend || + cache.prefill_mode != PrefillAttentionMode::Sparse || count <= 4 || + count > DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS || w.n_layer != 43 || + w.layers.size() != 43 || cache.layers.size() != 43 || + w.compress_ratios.size() != 43 || hybrid->layers.size() != 43) + return fail("image batch requires the heterogeneous sparse decoder path"); + for (int il = 0; il < w.n_layer; ++il) { + const auto & layer = w.layers[size_t(il)]; + const auto & state = cache.layers[size_t(il)]; + const auto bias = layer.ffn_gate_bias_vl; + const int ratio = int(w.compress_ratios[size_t(il)]); + if (!bias || bias->type != GGML_TYPE_F32 || bias->ne[0] != 256 || + ggml_nelements(bias) != 256 || !state.raw_kv || + ratio != (il < 2 ? 0 : il % 2 == 0 ? 4 : 128)) + return fail("image decoder is missing a validated router bias or attention state"); + if (ratio && (!layer.attn_compressor_ape || !layer.attn_compressor_kv || + !layer.attn_compressor_gate || !layer.attn_compressor_norm || !state.comp_kv || + !state.attn_compressor.state_kv || !state.attn_compressor.state_score)) + return fail("image decoder has incomplete attention compressor state"); + if (ratio == 4 && (!layer.indexer_compressor_ape || !layer.indexer_compressor_kv || + !layer.indexer_compressor_gate || !layer.indexer_compressor_norm || + !state.index_comp_kv || !state.indexer_compressor.state_kv || + !state.indexer_compressor.state_score)) + return fail("image decoder has incomplete indexer compressor state"); + } + return true; +} + bool deepseek4_step_layer_range( ggml_backend_t backend, int device, @@ -6878,9 +6977,22 @@ bool deepseek4_step_layer_range( Ds4VerifyHooks * verify_hooks, MoeHybridStorage * moe_hybrid, MoeExpertComputeRuntime * expert_runtime, - MoeHybridRoutingStats * routing_stats) { + MoeHybridRoutingStats * routing_stats, + vision::ImageSpanView image_spans) { const auto step_t0 = Ds4TimingClock::now(); + bool image_batch = false; + std::string image_error; + if (!deepseek4_validate_image_batch(w, cache, moe_hybrid, token_ids, + n_tokens, kv_start, image_spans, image_batch, image_error) || + (image_batch && (!embed || layer_begin != 0 || layer_end != w.n_layer || + !out_logits || verify_hooks || expert_runtime || + !vision::detail::hip_bias_workspace(backend) || moe_hybrid->cold_backend == backend))) { + std::fprintf(stderr, "[deepseek4] image prefill rejected before evaluation: %s\n", + image_error.empty() ? "unsupported execution path" : image_error.c_str()); + return false; + } + if (!deepseek4_cuda_hc_set_device(device)) { std::fprintf(stderr, "[deepseek4] failed to select HC device %d for layer range [%d,%d)\n", @@ -6964,7 +7076,7 @@ bool deepseek4_step_layer_range( // to this forward call; decode graph replay is restored on every return. ScopedCudaGraphOverrides heterogeneous_prefill_eager_scope( heterogeneous_sparse_prefill && - ds4_env_flag("DFLASH_DS4_HYBRID_PREFILL_EAGER")); + (image_batch || ds4_env_flag("DFLASH_DS4_HYBRID_PREFILL_EAGER"))); // A dynamic batch may be supplied by callers other than the DSpark // verifier. Split it whenever it spans a learned-compressor boundary: @@ -7016,7 +7128,7 @@ bool deepseek4_step_layer_range( out_logits ? &chunk_out : nullptr, token_ids ? token_ids + off : nullptr, telemetry, allow_decode_graph_reuse, chunk_hooks_ptr, - moe_hybrid, expert_runtime, routing_stats)) { + moe_hybrid, expert_runtime, routing_stats, image_spans)) { return false; } hc_all.insert(hc_all.end(), chunk_hc.begin(), chunk_hc.end()); @@ -7677,7 +7789,9 @@ bool deepseek4_step_layer_range( i32_inputs, i32_array_inputs, i64_array_inputs, &f32_array_inputs, - attention_impl); + attention_impl, + image_batch ? image_spans : vision::ImageSpanView{}); + if (!attn_out) { ggml_free(ctx); return false; } ggml_set_output(attn_out); ggml_build_forward_expand(gf, attn_out); @@ -8159,7 +8273,8 @@ bool deepseek4_step_layer_range( token_ids, hash_routing_tables_range[(size_t)il], *moe_hybrid, expert_runtime, routing_stats, ffn_out_host, telemetry, - ffn_device_join ? &owner_outputs : nullptr)) { + ffn_device_join ? &owner_outputs : nullptr, + kv_start, image_batch ? image_spans : vision::ImageSpanView{})) { std::fprintf(stderr, "[deepseek4-moe-tp] layer-range FFN failed layer %d\n", il); @@ -8598,6 +8713,14 @@ void deepseek4_release_prefill_scratch( } } +void deepseek4_release_image_scratch(DeepSeek4Cache & c, + MoeHybridStorage * moe_hybrid) { + deepseek4_release_prefill_scratch(c, moe_hybrid); + delete c.layer_range_cache; + c.layer_range_cache = nullptr; + if (moe_hybrid) moe_hybrid->release_graph_caches(); +} + namespace { ggml_tensor * clone_snapshot_tensor(ggml_context * ctx, diff --git a/server/src/deepseek4/deepseek4_image_admission.cpp b/server/src/deepseek4/deepseek4_image_admission.cpp new file mode 100644 index 000000000..d1daf4ebb --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_admission.cpp @@ -0,0 +1,376 @@ +#include "deepseek4_image_admission.h" + +#include "deepseek4_internal.h" +#include "common/moe_hybrid_placement.h" +#include "common/moe_hybrid_types.h" +#include "ggml-backend.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace dflash::vision { +namespace { +constexpr uint64_t MAX = std::numeric_limits::max(); + +bool fail(std::string & error, const std::string & message) { + error = message; + return false; +} + +bool add(uint64_t & sum, uint64_t value) { + if (value > MAX - sum) return false; + sum += value; + return true; +} + +bool mul(uint64_t a, uint64_t b, uint64_t & result) { + if (a && b > MAX / a) return false; + result = a * b; + return true; +} + +bool aligned(uint64_t value, uint64_t alignment, uint64_t & result) { + if (!alignment) return false; + result = value; + return add(result, (alignment - value % alignment) % alignment); +} + +bool enabled(const char * name) { + const char * raw = std::getenv(name); + return raw && *raw && std::strcmp(raw, "0") != 0; +} + +bool gpu_device(ggml_backend_t backend) { + if (!backend || !ggml_backend_get_device(backend)) return false; + const auto type = ggml_backend_dev_type(ggml_backend_get_device(backend)); + return type == GGML_BACKEND_DEVICE_TYPE_GPU || type == GGML_BACKEND_DEVICE_TYPE_IGPU; +} + +bool selected_allocation(const ggml_tensor & source, uint64_t count, + ggml_backend_buffer_type_t buft, + uint64_t & payload, uint64_t & allocation, + std::string & error) { + payload = allocation = 0; + if (!count) return true; + if (!mul(uint64_t(source.nb[2]), count, payload) || payload > SIZE_MAX) { + return fail(error, "selected expert tensor payload overflow"); + } + // Storage recreates each owner tensor contiguously with only ne[2] changed. + // The metadata copy avoids allocating even a temporary GGML context. + ggml_tensor selected = source; + selected.ne[2] = int64_t(count); + selected.ne[3] = 1; + selected.nb[3] = size_t(payload); + selected.data = nullptr; + selected.buffer = nullptr; + selected.view_src = nullptr; + selected.view_offs = 0; + const uint64_t requested = ggml_backend_buft_get_alloc_size(buft, &selected); + if (requested < payload || + !aligned(requested, ggml_backend_buft_get_alignment(buft), allocation) || + allocation > ggml_backend_buft_get_max_size(buft)) { + return fail(error, "selected expert tensor allocation size is invalid or exceeds backend limit"); + } + return true; +} + +bool owner_activation_estimate(const common::MoeHybridConfig & config, int tokens, + uint64_t alignment, uint64_t & bytes, std::string & error) { + if (tokens <= 0 || tokens > 1024 || config.n_embd <= 0 || + config.n_ff_exp <= 0 || config.n_ff_shexp < 0 || config.n_expert_used <= 0) { + return fail(error, "image owner estimate requires positive dimensions and chunk capacity at most 1024"); + } + const uint64_t d = uint64_t(config.n_embd); + const uint64_t f = uint64_t(config.n_ff_exp); + const uint64_t shared_f = uint64_t(config.n_ff_shexp); + uint64_t pairs = 0, routed_width = 0, shared_width = 0, part = 0, elements = 0; + // Sum materialized tensors without lifetime reuse in the expert-major graph: + // packed input/output, matmul/scaled output, route gather (5*D per route); + // gate/up, optional scales/contiguous copies/clamps and GLU (<=9*F). + // Input/shared branch/reduction contributes <=4*D+9*shared_F per token. + // This intentionally leaves additional arena/pool/cached-graph headroom to + // the named reservation rather than inventing a backend-wide upper bound. + if (!mul(uint64_t(tokens), uint64_t(config.n_expert_used), pairs) || + !mul(5, d, routed_width) || !mul(9, f, part) || !add(routed_width, part) || + !mul(4, d, shared_width) || !mul(9, shared_f, part) || !add(shared_width, part) || + !mul(routed_width, pairs, elements) || !mul(shared_width, uint64_t(tokens), part) || + !add(elements, part) || !mul(elements, sizeof(float), bytes) || + !mul(12, pairs, part) || !add(bytes, part) || + !mul(16384, alignment, part) || !add(bytes, part)) { + return fail(error, "owner activation estimate overflow"); + } + return true; +} + +bool host_available(uint64_t & bytes, std::string & error) { +#if defined(__linux__) + std::ifstream input("/proc/meminfo"); + if (!input) return fail(error, "cannot read host MemAvailable"); + std::string line; + bool found = false; + while (std::getline(input, line)) { + if (line.compare(0, 13, "MemAvailable:") != 0) continue; + if (found) return fail(error, "duplicate host MemAvailable field"); + std::istringstream fields(line.substr(13)); + uint64_t kb = 0; + std::string unit, extra; + if (!(fields >> kb >> unit) || unit != "kB" || (fields >> extra) || + !mul(kb, 1024, bytes)) return fail(error, "invalid host MemAvailable"); + found = true; + } + return found || fail(error, "host MemAvailable is missing"); +#else + (void) bytes; + return fail(error, "host admission currently requires Linux MemAvailable"); +#endif +} + +bool device_free(ggml_backend_t backend, uint64_t & available, std::string & error) { + size_t free = 0, total = 0; + ggml_backend_dev_memory(ggml_backend_get_device(backend), &free, &total); + if (!total) return fail(error, "device memory query returned no capacity"); + // GGML's UMA query can legitimately report free RAM above dedicated total. + available = free; + return true; +} +} // namespace + +bool estimate_deepseek4_image_storage( + const common::DeepSeek4Weights & w, const common::MoeHybridPlacement & placement, + const common::MoeHybridConfig & config, ggml_backend_t primary, ggml_backend_t cold, + bool duplicate, ImageStorageEstimate & out, std::string & error) { + out = {}; + error.clear(); + if (!gpu_device(primary) || !gpu_device(cold) || + ggml_backend_get_device(primary) == ggml_backend_get_device(cold)) { + return fail(error, "image storage admission requires two distinct GPU owners"); + } + if (config.cold_expert_backend != common::MoeHybridColdBackend::Gpu || + !config.materialize_hot_experts || !config.materialize_cold_experts || + w.n_expert <= 0 || w.n_layer <= 0 || size_t(w.n_layer) != w.layers.size() || + config.n_layer != w.n_layer || config.n_expert != w.n_expert || + config.n_embd != w.n_embd || config.n_ff_exp != w.n_ff_exp || + config.n_expert_used != w.n_expert_used || !placement.matches(config) || + !placement.valid(&error)) { + return fail(error, "image storage admission requires valid materialized DS4 placement"); + } + if (duplicate != enabled("DFLASH_MOE_DUPLICATE_HOT_ON_COLD")) { + return fail(error, "image admission duplicate mode differs from storage environment"); + } + const auto hot_buft = ggml_backend_get_default_buffer_type(primary); + const auto cold_buft = ggml_backend_get_default_buffer_type(cold); + if (!hot_buft || !cold_buft) return fail(error, "owner buffer type is missing"); + for (int layer_index = 0; layer_index < w.n_layer; ++layer_index) { + const auto & layer = w.layers[size_t(layer_index)]; + const uint64_t hot_count = uint64_t(placement.hot_counts[size_t(layer_index)]); + const uint64_t cold_count = duplicate ? uint64_t(w.n_expert) : uint64_t(w.n_expert) - hot_count; + if (enabled("DFLASH_DS4_DECODE_ALL_COLD") && cold_count != uint64_t(w.n_expert)) { + return fail(error, "decode-all-cold requires a full physical cold stack before allocation"); + } + if ((layer.ffn_gate_shexp && layer.ffn_gate_shexp->ne[1] > config.n_ff_shexp) || + (layer.ffn_up_shexp && layer.ffn_up_shexp->ne[1] > config.n_ff_shexp)) { + return fail(error, "shared expert metadata exceeds the configured activation width"); + } + uint64_t hot_layer = 0, cold_layer = 0; + uint64_t hot_buffer = 0, cold_buffer = 0; + const auto count_buffer = [&](uint64_t allocation, uint64_t maximum, + uint64_t & current, uint64_t & count) { + if (current && allocation > maximum - current) { + if (!add(count, 1)) return false; + current = 0; + } + return add(current, allocation); + }; + const std::array surfaces{ + layer.ffn_gate_exps, layer.ffn_up_exps, layer.ffn_down_exps}; + for (const auto * tensor : surfaces) { + if (!tensor) continue; + if (tensor->ne[0] <= 0 || tensor->ne[1] <= 0 || tensor->ne[2] != w.n_expert || + tensor->ne[3] != 1 || tensor->nb[2] == 0 || !ggml_is_contiguous(tensor)) { + return fail(error, "expert metadata must be positive contiguous [in,out,E,1]"); + } + uint64_t full_payload = 0; + if (!mul(uint64_t(tensor->nb[2]), uint64_t(w.n_expert), full_payload) || + full_payload > SIZE_MAX || full_payload != ggml_nbytes(tensor)) { + return fail(error, "expert metadata payload overflow or stride mismatch"); + } + uint64_t hot_payload = 0, hot_alloc = 0, cold_payload = 0, cold_alloc = 0; + if (!selected_allocation(*tensor, hot_count, hot_buft, hot_payload, hot_alloc, error) || + !selected_allocation(*tensor, cold_count, cold_buft, cold_payload, cold_alloc, error)) return false; + if (!add(out.hot_payload_bytes, hot_payload) || !add(out.cold_payload_bytes, cold_payload) || + !add(hot_layer, hot_alloc) || !add(cold_layer, cold_alloc)) { + return fail(error, "expert owner storage sum overflow"); + } + if (!count_buffer(hot_alloc, ggml_backend_buft_get_max_size(hot_buft), + hot_buffer, out.hot_buffer_count) || + !count_buffer(cold_alloc, ggml_backend_buft_get_max_size(cold_buft), + cold_buffer, out.cold_buffer_count)) { + return fail(error, "owner split-buffer accounting overflow"); + } + out.largest_copy_bytes = std::max({out.largest_copy_bytes, hot_payload, cold_payload}); + uint64_t per_expert_table = 0; + if (tensor->type == GGML_TYPE_Q3_1_ROCMFP3_MIX) per_expert_table = 33; + if (tensor->type == GGML_TYPE_Q2_1_ROCMFP2_MIX) per_expert_table = 17; + if (per_expert_table) { + uint64_t hot_table = 0, cold_table = 0, host_table = 0; + if (!mul(per_expert_table, hot_count, hot_table) || + !mul(per_expert_table, cold_count, cold_table) || + !add(out.hot_mix_table_bytes, hot_table) || !add(out.cold_mix_table_bytes, cold_table) || + !mul(per_expert_table + (per_expert_table == 33 ? 1 : 0), + uint64_t(w.n_expert), host_table) || + !add(host_table, std::max(hot_table, cold_table)) || + !add(host_table, uint64_t(w.n_expert))) { + return fail(error, "MIX table accounting overflow"); + } + // File entry books/modes (+ P4 rotation bytes), compact selected + // books/modes, and conservatively one byte per seen expert. + out.host_mix_payload_peak_bytes = std::max(out.host_mix_payload_peak_bytes, host_table); + if (!add(out.mix_device_allocation_count, 2 * uint64_t(hot_count > 0) + + 2 * uint64_t(cold_count > 0))) { + return fail(error, "MIX allocation count overflow"); + } + } + } + if (!add(out.hot_allocation_bytes, hot_layer) || !add(out.cold_allocation_bytes, cold_layer) || + !add(out.hot_buffer_count, uint64_t(hot_buffer > 0)) || + !add(out.cold_buffer_count, uint64_t(cold_buffer > 0))) { + return fail(error, "owner allocation accounting overflow"); + } + } + if (!out.hot_payload_bytes && !out.cold_payload_bytes) return fail(error, "no expert storage metadata found"); + // The qualified Linux libstdc++ vector grows to at most old_size + max(old_size, + // requested_delta). Retained old storage during growth adds at most another M. + // Hot and cold staging vectors have disjoint scopes; layers are copied serially. +#if defined(__GLIBCXX__) + if (!mul(out.largest_copy_bytes, 3, out.host_copy_peak_bytes)) { + return fail(error, "host expert staging peak overflow"); + } +#else + return fail(error, "host expert staging growth bound requires the qualified libstdc++ runtime"); +#endif + return true; +} + +bool check_deepseek4_image_admission( + const common::DeepSeek4Weights & w, const common::MoeHybridPlacement & placement, + const common::MoeHybridConfig & config, ggml_backend_t primary, ggml_backend_t cold, + const ImageAdmissionReserves & reserves, ImageAdmissionReport & out, std::string & error) { + out = {}; + if (!estimate_deepseek4_image_storage(w, placement, config, primary, cold, + reserves.duplicate_hot_on_cold, out.storage, error)) return false; + out.storage_estimated = true; + out.cold_runtime_reservation_bytes = reserves.cold_runtime_reservation_bytes; + if (!owner_activation_estimate(config, reserves.max_chunk_tokens, + ggml_backend_buft_get_alignment(ggml_backend_get_default_buffer_type(cold)), + out.cold_activation_estimate_bytes, error)) return false; + if (reserves.primary_domain == ImageMemoryDomain::Unknown || + reserves.cold_domain == ImageMemoryDomain::Unknown) { + return fail(error, "actual owner host-memory sharing must be classified before admission"); + } + if (!device_free(primary, out.primary_free_bytes, error) || + !device_free(cold, out.cold_free_bytes, error) || + !host_available(out.host_available_bytes, error)) return false; + const ImageStorageEstimate storage = out.storage; + const uint64_t activation_bytes = out.cold_activation_estimate_bytes; + const ImageMemorySnapshot snapshot{out.primary_free_bytes, out.cold_free_bytes, out.host_available_bytes}; + return assess_deepseek4_image_admission(storage, activation_bytes, reserves, snapshot, out, error); +} + +bool check_deepseek4_image_host_preparation(uint64_t required_bytes, std::string & error) { + error.clear(); + if (!required_bytes) return fail(error, "image preparation requires a nonzero host reservation"); + uint64_t available = 0; + if (!host_available(available, error)) return false; + if (required_bytes > available) { + return fail(error, "insufficient host memory for image preparation: required=" + + std::to_string(required_bytes) + " available=" + std::to_string(available)); + } + return true; +} + +bool check_deepseek4_image_runtime_admission( + const common::MoeHybridConfig & config, ggml_backend_t primary, ggml_backend_t cold, + const ImageAdmissionReserves & reserves, ImageAdmissionReport & out, std::string & error) { + out = {}; + error.clear(); + if (!gpu_device(primary) || !gpu_device(cold) || + ggml_backend_get_device(primary) == ggml_backend_get_device(cold)) { + return fail(error, "image runtime admission requires two distinct GPU owners"); + } + uint64_t activation_bytes = 0; + const auto cold_buft = ggml_backend_get_default_buffer_type(cold); + if (!cold_buft || !owner_activation_estimate(config, reserves.max_chunk_tokens, + ggml_backend_buft_get_alignment(cold_buft), activation_bytes, error)) return false; + ImageMemorySnapshot snapshot; + if (!device_free(primary, snapshot.primary_free_bytes, error) || + !device_free(cold, snapshot.cold_free_bytes, error) || + !host_available(snapshot.host_available_bytes, error)) return false; + ImageAdmissionReserves runtime_reserves = reserves; + runtime_reserves.host_loader_overhead_bytes = 0; + return assess_deepseek4_image_admission({}, activation_bytes, runtime_reserves, snapshot, out, error); +} + +bool assess_deepseek4_image_admission( + const ImageStorageEstimate & storage, uint64_t cold_activation_estimate_bytes, + const ImageAdmissionReserves & reserves, const ImageMemorySnapshot & snapshot, + ImageAdmissionReport & out, std::string & error) { + const ImageStorageEstimate storage_copy = storage; + out = {}; + error.clear(); + out.storage = storage_copy; + out.storage_estimated = true; + out.cold_activation_estimate_bytes = cold_activation_estimate_bytes; + out.cold_runtime_reservation_bytes = reserves.cold_runtime_reservation_bytes; + out.primary_free_bytes = snapshot.primary_free_bytes; + out.cold_free_bytes = snapshot.cold_free_bytes; + out.host_available_bytes = snapshot.host_available_bytes; + const auto known_domain = [](ImageMemoryDomain domain) { + return domain == ImageMemoryDomain::Dedicated || domain == ImageMemoryDomain::HostShared; + }; + if (!known_domain(reserves.primary_domain) || !known_domain(reserves.cold_domain)) { + return fail(error, "actual owner host-memory sharing must be classified before admission"); + } + out.primary_required_bytes = out.storage.hot_allocation_bytes; + out.cold_required_bytes = out.storage.cold_allocation_bytes; + if (!add(out.primary_required_bytes, out.storage.hot_mix_table_bytes) || + !add(out.primary_required_bytes, reserves.primary_future_bytes) || + !add(out.cold_required_bytes, out.storage.cold_mix_table_bytes) || + !add(out.cold_required_bytes, reserves.cold_future_bytes) || + !add(out.cold_required_bytes, reserves.cold_runtime_reservation_bytes)) { + return fail(error, "device admission charge overflow"); + } + uint64_t loading = out.storage.host_copy_peak_bytes; + if (!add(loading, out.storage.host_mix_payload_peak_bytes) || + !add(loading, reserves.host_loader_overhead_bytes)) { + return fail(error, "host model-loading charge overflow"); + } + // Loading and serving are separate phases. Conservatively keep runtime + // host buffers alongside whichever phase has the larger temporary peak. + out.host_required_bytes = std::max(loading, reserves.host_request_bytes); + if (!add(out.host_required_bytes, reserves.host_runtime_bytes) || + (reserves.primary_domain == ImageMemoryDomain::HostShared && + !add(out.host_required_bytes, out.primary_required_bytes)) || + (reserves.cold_domain == ImageMemoryDomain::HostShared && + !add(out.host_required_bytes, out.cold_required_bytes))) { + return fail(error, "combined host/UMA admission charge overflow"); + } + out.known_charges_fit = out.primary_required_bytes <= out.primary_free_bytes && + out.cold_required_bytes <= out.cold_free_bytes && out.host_required_bytes <= out.host_available_bytes; + if (!out.known_charges_fit) return fail(error, "insufficient primary, cold-owner, or combined host/UMA headroom"); + if (reserves.cold_runtime_reservation_bytes < out.cold_activation_estimate_bytes) { + return fail(error, "cold-owner runtime reservation is below the metadata-derived activation estimate"); + } + const bool loading_storage = storage_copy.hot_allocation_bytes || storage_copy.cold_allocation_bytes || + storage_copy.hot_mix_table_bytes || storage_copy.cold_mix_table_bytes || + storage_copy.host_copy_peak_bytes || storage_copy.host_mix_payload_peak_bytes; + if (!reserves.host_request_bytes || (loading_storage && !reserves.host_loader_overhead_bytes)) { + return fail(error, "request/decode and metadata/host-safety reservations are required"); + } + return true; +} +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_image_admission.h b/server/src/deepseek4/deepseek4_image_admission.h new file mode 100644 index 000000000..f6eeb5aca --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_admission.h @@ -0,0 +1,125 @@ +#pragma once + +#include +#include + +struct ggml_backend; +namespace dflash::common { +struct DeepSeek4Weights; +struct MoeHybridPlacement; +struct MoeHybridConfig; +} + +namespace dflash::vision { + +enum class ImageMemoryDomain { Unknown, Dedicated, HostShared }; + +struct ImageAdmissionReserves { + // GGML currently reports integrated HIP devices as GPU. The caller must + // classify the actual devices using HIP properties and unified-memory mode. + ImageMemoryDomain primary_domain = ImageMemoryDomain::Unknown; + ImageMemoryDomain cold_domain = ImageMemoryDomain::Unknown; + bool duplicate_hot_on_cold = false; + + // Only allocations not already reflected by the free-memory snapshot: + // KV, the remaining vision reservation, runtime arenas/pools, and existing + // allocator/safety margins. Do not charge preloaded core/projector twice. + uint64_t primary_future_bytes = 0; + uint64_t cold_future_bytes = 0; + // Named cold-runtime arena/pool reservation, additional to cold_future_bytes. + // This is a conservative headroom policy tested by the resource guard, not + // a claim that every backend temporary has a formal upper bound. + uint64_t cold_runtime_reservation_bytes = 0; + int max_chunk_tokens = 1024; + + // Full request/decode/preprocess host peak for the configured request limits + // and concurrency, plus any future host-only runtime/staging allocations. + uint64_t host_request_bytes = 0; + // GGUF metadata parser copies, embedded-table backing, allocator overhead, + // and the existing host safety reserve. Owner table payloads and expert + // copy staging are computed separately below. + uint64_t host_loader_overhead_bytes = 0; + uint64_t host_runtime_bytes = 0; +}; + +struct ImageStorageEstimate { + uint64_t hot_payload_bytes = 0; + uint64_t cold_payload_bytes = 0; + uint64_t hot_allocation_bytes = 0; + uint64_t cold_allocation_bytes = 0; + uint64_t hot_mix_table_bytes = 0; + uint64_t cold_mix_table_bytes = 0; + uint64_t largest_copy_bytes = 0; + uint64_t host_copy_peak_bytes = 0; + uint64_t host_mix_payload_peak_bytes = 0; + uint64_t hot_buffer_count = 0; + uint64_t cold_buffer_count = 0; + uint64_t mix_device_allocation_count = 0; +}; + +struct ImageAdmissionReport { + ImageStorageEstimate storage; + uint64_t primary_free_bytes = 0; + uint64_t cold_free_bytes = 0; + uint64_t host_available_bytes = 0; + uint64_t primary_required_bytes = 0; + uint64_t cold_required_bytes = 0; + uint64_t host_required_bytes = 0; + bool storage_estimated = false; + bool known_charges_fit = false; + uint64_t cold_activation_estimate_bytes = 0; + uint64_t cold_runtime_reservation_bytes = 0; +}; + +struct ImageMemorySnapshot { + uint64_t primary_free_bytes = 0; + uint64_t cold_free_bytes = 0; + uint64_t host_available_bytes = 0; +}; + +// Pure assessment shared by live admission and deterministic CPU tests. All +// figures describe allocations still to come relative to this one snapshot. +bool assess_deepseek4_image_admission( + const ImageStorageEstimate & storage, uint64_t cold_activation_estimate_bytes, + const ImageAdmissionReserves & reserves, const ImageMemorySnapshot & snapshot, + ImageAdmissionReport & result, std::string & error); + +// Reads metadata and backend allocation-size functions only; no device buffers, +// expert copies, graph execution, file mapping, or table registration occur. +// Supported scope matches DS4's materialized same-runtime two-GPU owner path +// with zero cache slots. Duplicate mode must match the storage environment. +bool estimate_deepseek4_image_storage( + const common::DeepSeek4Weights & weights, + const common::MoeHybridPlacement & placement, + const common::MoeHybridConfig & config, + ggml_backend * primary, ggml_backend * cold, + bool duplicate_hot_on_cold, ImageStorageEstimate & result, std::string & error); + +// Invoke after owner initialization and final placement, immediately before +// build_deepseek4_moe_hybrid_storage_from_file_with_mmap. Queries current device +// free memory and Linux MemAvailable, combining new UMA and host charges. +// Returns false on missing reservations or insufficient capacity. The report +// separates the metadata-derived activation estimate from reserved headroom. +// A successful snapshot is admission, not an allocation reservation or a proof +// against concurrent external allocations; guarded runtime verification remains +// necessary. Host cgroup/process limits must be included by caller policy. +bool check_deepseek4_image_admission( + const common::DeepSeek4Weights & weights, + const common::MoeHybridPlacement & placement, + const common::MoeHybridConfig & config, + ggml_backend * primary, ggml_backend * cold, + const ImageAdmissionReserves & reserves, + ImageAdmissionReport & result, std::string & error); + +// CPU-only predecode check for the client thread after taking the image lease. +// Does not touch backend state or query a GPU. +bool check_deepseek4_image_host_preparation(uint64_t required_bytes, std::string & error); + +// Recheck after synchronizing owners and releasing disposable graph caches. +// Existing experts/tables/core/KV/snapshots are represented only by live free +// memory. No model-loader or expert-copy charge is added a second time. +bool check_deepseek4_image_runtime_admission( + const common::MoeHybridConfig & config, ggml_backend * primary, ggml_backend * cold, + const ImageAdmissionReserves & reserves, ImageAdmissionReport & result, std::string & error); + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_image_assembly.cpp b/server/src/deepseek4/deepseek4_image_assembly.cpp new file mode 100644 index 000000000..55bfd30b5 --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_assembly.cpp @@ -0,0 +1,203 @@ +#include "deepseek4_image_assembly.h" +#include "deepseek4_image_spans.h" +#include +#include +#include +#include +#include + +namespace dflash::vision { +namespace { +void require(bool valid, const char * message) { + if (!valid) throw std::runtime_error(message); +} +size_t elements(size_t rows, size_t dimension) { + require(dimension && rows <= std::numeric_limits::max() / dimension, + "invalid image matrix dimensions"); + return rows * dimension; +} +bool finite(const std::vector & values) { + return std::all_of(values.begin(), values.end(), [](float v) { return std::isfinite(v); }); +} +void layout_valid(const ImageLayout & layout) { + const auto & span = layout.span; + require(valid_image_spans({&span, 1}, span.block_end) && + span.block_end - span.block_begin == layout.types.size(), "invalid image layout span"); + size_t images = 0; + for (size_t row = 0; row < layout.types.size(); ++row) { + const uint64_t pos = span.block_begin + row; + const auto type = layout.types[row]; + if (pos == span.visible_begin) { + require(type == ImageTokenType::Start, "image start identity mismatch"); + } else if (pos + 1 == span.visible_end) { + require(type == ImageTokenType::End, "image end identity mismatch"); + } else if (pos < span.visible_begin || pos >= span.visible_end) { + require(type == ImageTokenType::Pad, "image outer padding identity mismatch"); + } else { + require(type == ImageTokenType::Pad || type == ImageTokenType::Newline || + type == ImageTokenType::Image, "invalid image token identity"); + } + images += type == ImageTokenType::Image; + } + require(images > 0 && images == layout.permutation.size(), "image permutation cardinality mismatch"); + std::vector seen(images, false); + for (int64_t index : layout.permutation) { + require(index >= 0 && uint64_t(index) < images && !seen[size_t(index)], + "image permutation must contain each raster row exactly once"); + seen[size_t(index)] = true; + } +} +void sentinels_valid(const ImageSentinels & s, size_t dimension) { + for (const auto * row : {&s.start, &s.pad, &s.newline, &s.end}) { + require(row->size() == dimension && dimension && finite(*row), "invalid image sentinel row"); + } +} +void cancelled(const ImageCancelled & callback) { + require(!callback || !callback(), "image materialization cancelled"); +} +} // namespace + +std::shared_ptr ImageRequestGate::try_acquire() const { + bool expected = false; + if (!active_->compare_exchange_strong(expected, true, std::memory_order_acq_rel)) return {}; + // shared_ptr invokes the deleter if control-block allocation throws too. + return std::shared_ptr(active_.get(), [active = active_](void *) { + active->store(false, std::memory_order_release); + }); +} + +bool assemble_image_rows(const ImageLayout & layout, const ImageRaster & raster, + const ImageSentinels & sentinels, size_t dimension, + std::vector & output, std::string & error) { + error.clear(); + try { + layout_valid(layout); + sentinels_valid(sentinels, dimension); + require(raster.rows == layout.permutation.size() && raster.columns == dimension && + raster.values.size() == elements(raster.rows, dimension) && finite(raster.values), + "invalid projected image raster"); + std::vector result(elements(layout.types.size(), dimension)); + size_t index = 0; + for (size_t row = 0; row < layout.types.size(); ++row) { + const float * source = nullptr; + switch (layout.types[row]) { + case ImageTokenType::Start: source = sentinels.start.data(); break; + case ImageTokenType::Pad: source = sentinels.pad.data(); break; + case ImageTokenType::Newline: source = sentinels.newline.data(); break; + case ImageTokenType::End: source = sentinels.end.data(); break; + case ImageTokenType::Image: + source = raster.values.data() + size_t(layout.permutation[index++]) * dimension; + break; + } + require(source != nullptr, "unknown image token type"); + std::copy_n(source, dimension, result.data() + row * dimension); + } + output.swap(result); + return true; + } catch (const std::exception & e) { error = e.what(); return false; } + catch (...) { error = "image assembly failed"; return false; } +} + +bool materialize_image_rows(const std::vector & images, + const ImageSentinels & sentinels, size_t dimension, + const ImageEncode & encode, const ImageCancelled & is_cancelled, + ImageRows & output, std::string & error) { + error.clear(); + try { + require(!images.empty() && images.size() <= 4 && bool(encode), "invalid image encode request"); + cancelled(is_cancelled); + sentinels_valid(sentinels, dimension); + uint64_t previous_end = 0; + for (const auto & image : images) { + layout_valid(image.layout); + require(image.input.plan.aligner_rows && image.input.plan.aligner_cols && + uint64_t(image.input.plan.aligner_rows) * image.input.plan.aligner_cols == + image.layout.permutation.size(), "prepared aligner shape does not match raster rows"); + require(image.layout.span.block_begin >= previous_end, "overlapping image blocks"); + previous_end = image.layout.span.block_end; + } + ImageRows result; + result.reserve(images.size()); + for (const auto & image : images) { + cancelled(is_cancelled); + ImageRaster raster; + if (!encode(image, raster, error)) { + if (error.empty()) error = "image encode failed"; + return false; + } + cancelled(is_cancelled); + std::vector rows; + if (!assemble_image_rows(image.layout, raster, sentinels, dimension, rows, error)) return false; + result.push_back(std::move(rows)); + } + cancelled(is_cancelled); + output.swap(result); + return true; + } catch (const std::exception & e) { error = e.what(); return false; } + catch (...) { error = "image encode callback failed"; return false; } +} + +bool embed_image_prompt_chunk(const PreparedImagePrompt & prompt, const ImageRows & rows, + int32_t vocabulary, size_t dimension, size_t position, + size_t count, const TextEmbed & embed, + std::vector & output, std::string & error) { + error.clear(); + try { + require(bool(prompt) && vocabulary > 0 && count && position <= prompt.tokens.size() && + count <= prompt.tokens.size() - position && rows.size() == prompt.images.size() && + prompt.images.size() <= 4, "invalid mixed embedding request"); + const size_t end = position + count; + uint64_t previous_end = 0; + for (size_t i = 0; i < prompt.images.size(); ++i) { + const auto & layout = prompt.images[i].layout; + const auto & span = layout.span; + layout_valid(layout); + require(span.block_begin >= previous_end && span.block_end <= prompt.tokens.size(), + "invalid mixed embedding spans"); + previous_end = span.block_end; + if (span.block_end <= position || span.block_begin >= end) continue; + require(span.block_begin >= position && span.block_end <= end, + "mixed embedding chunk splits an image block"); + require(rows[i].size() == elements(layout.types.size(), dimension) && finite(rows[i]), + "invalid materialized image matrix"); + for (size_t r = 0; r < layout.types.size(); ++r) { + const int64_t expected = int64_t(vocabulary) + int64_t(layout.types[r]); + require(int64_t(prompt.tokens[size_t(span.block_begin) + r]) == expected, + "external token does not match image layout"); + } + } + // Validate every ordinary ID before any callback can observe this chunk. + size_t image_index = 0; + for (size_t p = position; p < end; ++p) { + while (image_index < prompt.images.size() && prompt.images[image_index].layout.span.block_end <= p) + ++image_index; + if (image_index < prompt.images.size() && prompt.images[image_index].layout.span.block_begin <= p) continue; + require(prompt.tokens[p] >= 0 && prompt.tokens[p] < vocabulary, "unbound external token in text range"); + } + std::vector result(elements(count, dimension)); + size_t current = position; + image_index = 0; + while (current < end) { + while (image_index < prompt.images.size() && prompt.images[image_index].layout.span.block_end <= current) + ++image_index; + const size_t text_end = image_index < prompt.images.size() + ? std::min(end, size_t(prompt.images[image_index].layout.span.block_begin)) : end; + if (current < text_end) { + require(bool(embed) && embed(prompt.tokens.data() + current, text_end - current, + result.data() + (current - position) * dimension), "text embedding failed"); + current = text_end; + } + if (current == end) break; + const auto & block = prompt.images[image_index].layout.span; + std::copy(rows[image_index].begin(), rows[image_index].end(), + result.data() + (current - position) * dimension); + current = size_t(block.block_end); + } + require(finite(result), "nonfinite mixed embeddings"); + output.swap(result); + return true; + } catch (const std::exception & e) { error = e.what(); return false; } + catch (...) { error = "text embedding callback failed"; return false; } +} + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_image_assembly.h b/server/src/deepseek4/deepseek4_image_assembly.h new file mode 100644 index 000000000..abdce1140 --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_assembly.h @@ -0,0 +1,50 @@ +#pragma once + +#include "deepseek4_image_prompt.h" +#include +#include +#include + +namespace dflash::vision { + +// Bound decoded/prepared image memory to one outstanding request. The lease +// travels with the immutable payload and may be released by another thread. +class ImageRequestGate { +public: + std::shared_ptr try_acquire() const; +private: + std::shared_ptr> active_ = std::make_shared>(false); +}; + +struct ImageRaster { + size_t rows = 0, columns = 0; + std::vector values; +}; +struct ImageSentinels { + std::vector start, pad, newline, end; +}; +using ImageRows = std::vector>; +using ImageEncode = std::function; +using ImageCancelled = std::function; +using TextEmbed = std::function; + +// All outputs are replaced only on success. The permutation maps each Image +// token to one unique raster row; sentinel identities never use enum casts. +bool assemble_image_rows(const ImageLayout &, const ImageRaster &, const ImageSentinels &, + size_t dimension, std::vector & output, std::string & error); + +// Synchronous callbacks; cancellation is checked before/after every encode +// and before commit. Failure, cancellation, or callback exception leaves the +// caller's previous result intact. The caller owns GPU lifetime and scratch. +bool materialize_image_rows(const std::vector &, const ImageSentinels &, + size_t dimension, const ImageEncode &, const ImageCancelled &, + ImageRows & output, std::string & error); + +// Only complete image blocks may intersect a chunk. The normal embed callback +// receives validated ordinary token IDs only; mixed output commits atomically. +bool embed_image_prompt_chunk(const PreparedImagePrompt &, const ImageRows &, + int32_t vocabulary, size_t dimension, + size_t position, size_t count, const TextEmbed &, + std::vector & output, std::string & error); + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_image_budget.h b/server/src/deepseek4/deepseek4_image_budget.h new file mode 100644 index 000000000..72173a323 --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_budget.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +namespace dflash::vision { + +inline constexpr uint64_t SCRATCH_RESERVATION = 2ULL * 1024 * 1024 * 1024; + +inline uint64_t remaining_expert_budget(uint64_t total, uint64_t core, + uint64_t kv, uint64_t warm, uint64_t safety, + uint64_t vision_reservation, uint64_t already_resident_workspace = 0) { + if (already_resident_workspace > vision_reservation) return 0; + for (uint64_t charge : {core, kv, warm, safety, + vision_reservation - already_resident_workspace}) { + if (charge > total) return 0; + total -= charge; + } + return total; +} + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_image_spans.h b/server/src/deepseek4/deepseek4_image_spans.h new file mode 100644 index 000000000..8c2a04a95 --- /dev/null +++ b/server/src/deepseek4/deepseek4_image_spans.h @@ -0,0 +1,57 @@ +#pragma once + +#include "deepseek4_vision_preprocess.h" +#include +#include + +namespace dflash::vision { + +struct ImageSpanView { + const TokenSpan * data = nullptr; + size_t size = 0; +}; + +inline const TokenSpan * image_block_at(ImageSpanView spans, uint64_t position) { + for (size_t i = 0; i < spans.size; ++i) { + const auto & span = spans.data[i]; + if (position < span.block_begin) break; + if (position < span.block_end) return &span; + } + return nullptr; +} + +inline bool valid_image_spans(ImageSpanView spans, uint64_t prompt_size) { + if (spans.size > 4 || (spans.size && !spans.data)) return false; + uint64_t previous_end = 0; + for (size_t i = 0; i < spans.size; ++i) { + const auto & span = spans.data[i]; + if (span.block_begin < previous_end || span.block_begin > span.visible_begin || + span.visible_begin >= span.visible_end || span.visible_end > span.block_end || + span.block_end > prompt_size || span.block_end - span.block_begin > 384) { + return false; + } + previous_end = span.block_end; + } + return true; +} + +inline int atomic_image_chunk(ImageSpanView spans, uint64_t position, + int proposed, uint64_t remaining, int capacity) { + if (proposed <= 0 || capacity <= 0 || uint64_t(proposed) > remaining || + position > std::numeric_limits::max() - remaining) return 0; + uint64_t end = position + std::min(proposed, capacity); + for (size_t i = 0; i < spans.size; ++i) { + const auto & span = spans.data[i]; + if (span.block_end <= position) continue; + if (span.block_begin < position) return 0; + if (span.block_begin >= end) break; + if (end < span.block_end) { + end = span.block_begin == position ? span.block_end : span.block_begin; + break; + } + } + const uint64_t count = end - position; + return count && count <= remaining && count <= uint64_t(capacity) ? int(count) : 0; +} + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_internal.h b/server/src/deepseek4/deepseek4_internal.h index 66d80e417..31c04dc91 100644 --- a/server/src/deepseek4/deepseek4_internal.h +++ b/server/src/deepseek4/deepseek4_internal.h @@ -25,6 +25,7 @@ #include "internal.h" #include "common/layer_split_utils.h" #include "common/prefill_attention_mode.h" +#include "deepseek4_image_spans.h" namespace dflash::common { @@ -134,6 +135,7 @@ struct DeepSeek4Layer { // Router ggml_tensor * ffn_gate_inp = nullptr; // [n_embd, n_expert] router weights F16 ggml_tensor * ffn_exp_probs_b = nullptr; // [n_expert] optional routing bias + ggml_tensor * ffn_gate_bias_vl = nullptr; // Hash routing table (first n_hash_layer layers only) ggml_tensor * ffn_gate_tid2eid = nullptr; // [n_expert_used, n_vocab] I32 @@ -307,6 +309,7 @@ struct DeepSeek4RawRingSpan { struct DeepSeek4BackendConfig { const char * model_path = nullptr; + std::string mmproj_path; DevicePlacement device; int stream_fd = -1; int chunk = 512; // prefill chunk size @@ -345,6 +348,10 @@ void reset_deepseek4_cache(DeepSeek4Cache & c); // state and the DSpark feature tail remain live for the following decode. void deepseek4_release_prefill_scratch(DeepSeek4Cache & c, MoeHybridStorage * moe_hybrid); +// Retire all disposable decoder/owner graphs before the vision tower uses the +// shared scratch allowance. KV and saved snapshots are left intact. +void deepseek4_release_image_scratch(DeepSeek4Cache & c, + MoeHybridStorage * moe_hybrid); int deepseek4_previous_raw_ring_spans( int kv_start, int n_swa, @@ -419,7 +426,14 @@ bool deepseek4_step_layer_range( Ds4VerifyHooks * verify_hooks = nullptr, MoeHybridStorage * moe_hybrid = nullptr, MoeExpertComputeRuntime * expert_runtime = nullptr, - MoeHybridRoutingStats * routing_stats = nullptr); + MoeHybridRoutingStats * routing_stats = nullptr, + vision::ImageSpanView image_spans = {}); + +bool deepseek4_validate_image_batch( + const DeepSeek4Weights & w, const DeepSeek4Cache & cache, + const MoeHybridStorage * hybrid, const int32_t * tokens, + int count, int position, vision::ImageSpanView spans, + bool & has_images, std::string & error); bool build_deepseek4_moe_hybrid_storage_from_file( const std::string & path, diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index 2ff7526c8..2cbd6bf60 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -234,6 +234,19 @@ static bool is_expert_tensor(const char * name) { std::strstr(name, "ffn_down_exps") != nullptr; } +static int image_bias_layer(const char * name) { + constexpr const char * prefix = "layers."; + if (std::strncmp(name, prefix, 7) != 0) return -1; + const char * number = name + 7; + if (*number < '0' || *number > '9') return -1; + char * suffix = nullptr; + const long layer = std::strtol(number, &suffix, 10); + if (layer < 0 || layer >= 43 || + std::strcmp(suffix, ".ffn.gate.bias_vl") != 0 || + std::string(name) != "layers." + std::to_string(layer) + ".ffn.gate.bias_vl") return -1; + return int(layer); +} + static bool should_keep_ds4_tensor(const char * name, const TargetLoadPlan & plan) { int layer_id = -1; @@ -244,6 +257,12 @@ static bool should_keep_ds4_tensor(const char * name, is_expert_tensor(name); } + const int image_layer = image_bias_layer(name); + if (image_layer >= 0) { + return plan.load_ds4_image_bias && image_layer >= plan.layer_begin && + image_layer < plan.layer_end; + } + // Global tensors if (std::strcmp(name, "token_embd.weight") == 0 || std::strcmp(name, "output_norm.weight") == 0 || @@ -1574,6 +1593,29 @@ bool load_deepseek4_gguf_partial(const std::string & path, // ── Collect tensors for allocation ────────────────────────────────── const int n_tensors = gguf_get_n_tensors(gctx); + if (plan.load_ds4_image_bias && !plan.expert_metadata_only) { + bool valid = n_layer == 43 && n_embd == 4096 && n_vocab == 129280 && + n_expert == 256 && n_expert_used == 6 && + plan.layer_begin == 0 && plan.layer_end == 43; + std::array counts{}; + for (int ti = 0; ti < n_tensors; ++ti) { + const char * name = gguf_get_tensor_name(gctx, ti); + const int layer = image_bias_layer(name); + if (layer < 0) continue; + ++counts[size_t(layer)]; + const ggml_tensor * tensor = find_tensor(meta_ctx, name); + valid = valid && tensor && tensor->type == GGML_TYPE_F32 && + tensor->ne[0] == 256 && tensor->ne[1] == 1 && + tensor->ne[2] == 1 && tensor->ne[3] == 1; + } + valid = valid && std::all_of(counts.begin(), counts.end(), [](int count) { return count == 1; }); + if (!valid) { + set_last_error("DS4V requires the supported decoder and exactly 43 F32[256] image router biases"); + gguf_free(gctx); + if (meta_ctx) ggml_free(meta_ctx); + return false; + } + } const size_t data_offset = gguf_get_data_offset(gctx); ggml_backend_buffer_type_t buft = ggml_backend_get_default_buffer_type(backend); const size_t alignment = ggml_backend_buft_get_alignment(buft); @@ -1829,6 +1871,12 @@ bool load_deepseek4_gguf_partial(const std::string & path, for (auto & a : allocs) { const char * name = ggml_get_name(a.tensor); + const int image_layer = image_bias_layer(name); + if (plan.load_ds4_image_bias && image_layer >= 0 && image_layer < int(n_layer)) { + out.layers[size_t(image_layer)].ffn_gate_bias_vl = a.tensor; + continue; + } + // Global tensors if (std::strcmp(name, "token_embd.weight") == 0) { out.tok_embd = a.tensor; continue; } if (std::strcmp(name, "output_norm.weight") == 0) { out.out_norm = a.tensor; continue; } diff --git a/server/src/internal.h b/server/src/internal.h index 07d6b89a0..e9e9d23ec 100644 --- a/server/src/internal.h +++ b/server/src/internal.h @@ -268,6 +268,7 @@ struct TargetLoadPlan { bool skip_expert_tensors = false; // skip ffn_*_exps from GPU (for hybrid MoE split load) bool metadata_only = false; // parse tensor descriptors/scales without GPU allocation bool expert_metadata_only = false; // keep only routed expert tensor metadata; upload nothing + bool load_ds4_image_bias = false; }; // Load a Q4_K_M target model from a GGUF file on disk. diff --git a/server/src/server/http_server.cpp b/server/src/server/http_server.cpp index a5a8032ae..1ee8f308c 100644 --- a/server/src/server/http_server.cpp +++ b/server/src/server/http_server.cpp @@ -17,6 +17,7 @@ #endif #include "http_server.h" +#include "image_input.h" #include "admission.h" #include "sse_emitter.h" #include "prompt_normalize.h" @@ -912,6 +913,7 @@ json build_props_body(const ServerConfig & config, {"reasoning_supported", reasoning_supported}, {"speculative_supported", speculative_supported}, {"tools_supported", tools_supported}, + {"image_input_supported", config.image_input_enabled}, }}, }; return body; @@ -1128,6 +1130,8 @@ HttpServer::HttpServer(ModelBackend & backend, config.disk_cache_continued_interval, config.disk_cache_cold_max_tokens}, backend) { + config_.image_input_enabled = backend_.supports_images() && + config_.pflash_upstream_base.empty() && !backend_.seq_engine(); #ifdef DFLASH_HAS_CURL curl_global_init(CURL_GLOBAL_DEFAULT); #endif @@ -2061,7 +2065,7 @@ bool HttpServer::validate_request_context( SocketHandle fd, const ParsedRequest & req) { const int prompt_tokens = (int) req.prompt_tokens.size(); const bool pflash_will_run = - config_.pflash_mode != ServerConfig::PflashMode::OFF && + !req.images && config_.pflash_mode != ServerConfig::PflashMode::OFF && drafter_tokenizer_ != nullptr && (config_.pflash_mode == ServerConfig::PflashMode::ALWAYS || prompt_tokens >= config_.pflash_threshold); @@ -2134,11 +2138,28 @@ bool HttpServer::route_request(SocketHandle fd, const HttpRequest & hr) { bool count_tokens_only = false; try { const json body = json::parse(hr.body); - req.raw_body = body; if (!parse_common_request_fields(fd, body, req)) return true; if (!parse_endpoint_request( hr.path, body, req, count_tokens_only)) return false; + std::vector encoded_images; + json normalized; + std::string extraction_error; + const ImageRequestPolicy image_policy{ + req.format == ApiFormat::OPENAI_CHAT, + config_.image_input_enabled, + config_.arch == "deepseek4"}; + if (!prepare_request_images(req.messages, image_policy, normalized, + encoded_images, extraction_error)) { + send_error(fd, 400, extraction_error); + return true; + } + req.messages = std::move(normalized); + json redacted_body = body; + redact_image_urls(redacted_body); + req.raw_body = std::move(redacted_body); + redact_image_urls(req.messages); + const std::vector chat_messages = normalize_chat_messages(req.messages, req.format, tool_memory_); // Reasoning must be applied BEFORE rendering: the template injects @@ -2150,7 +2171,7 @@ bool HttpServer::route_request(SocketHandle fd, const HttpRequest & hr) { // PPP rearrange (optional): peel ephemeral system banners into a // following system message so the first chat boundary is stable. std::vector render_messages = chat_messages; - if (config_.ppp_enabled && config_.ppp_rearrange && !req.tools.empty()) { + if (encoded_images.empty() && config_.ppp_enabled && config_.ppp_rearrange && !req.tools.empty()) { auto layout = PinFriendlyPrompt::rearrange(chat_messages, true); if (layout.rearranged) { render_messages = std::move(layout.messages); @@ -2173,6 +2194,14 @@ bool HttpServer::route_request(SocketHandle fd, const HttpRequest & hr) { if (!render_and_tokenize_request(fd, render_messages, req)) return true; + std::string image_error; + if (!backend_.prepare_images(req.prompt_tokens, std::move(encoded_images), + uint64_t(std::max(0, config_.max_ctx)), uint64_t(std::max(0, req.max_output)), + req.images, image_error)) { + send_error(fd, 400, image_error); + return true; + } + // count_tokens: short-circuit after tokenization. Skip generation // entirely — Anthropic's contract is just {"input_tokens": N}. if (count_tokens_only) { @@ -2182,9 +2211,12 @@ bool HttpServer::route_request(SocketHandle fd, const HttpRequest & hr) { send_response(fd, 200, "application/json", response.dump() + "\n"); return true; } - } catch (const std::exception & e) { + } catch (const json::parse_error & e) { send_error(fd, 400, std::string("JSON parse error: ") + e.what()); return true; + } catch (const std::exception & e) { + send_error(fd, 400, std::string("Invalid request: ") + e.what()); + return true; } if (!validate_request_context(fd, req)) return true; @@ -2949,6 +2981,15 @@ HttpServer::PreparedPrompt HttpServer::prepare_prompt( const ParsedRequest & req) { PreparedPrompt prepared; prepared.tokens = req.prompt_tokens; + if (req.images) { + if (!config_.image_input_enabled || !req.images->matches(prepared.tokens)) { + prepared.error_status = 400; + prepared.error = "image request binding or serving mode is invalid"; + } else { + prepared.images = req.images; + } + return prepared; + } if (config_.pflash_mode != ServerConfig::PflashMode::OFF && drafter_tokenizer_ != nullptr) { @@ -3005,6 +3046,7 @@ HttpServer::PreparedPrompt HttpServer::prepare_prompt( bool HttpServer::forward_upstream( ServerJob * job, const ParsedRequest & req, const PreparedPrompt & prepared) { + if (req.images) return false; #ifdef DFLASH_HAS_CURL if (config_.pflash_upstream_base.empty()) return false; @@ -3080,6 +3122,7 @@ bool HttpServer::forward_upstream( HttpServer::GenerationCacheState HttpServer::prepare_generation_cache( const ParsedRequest & req, PreparedPrompt & prepared, GenerateRequest & generate_request) { + if (req.images) return {}; auto & effective_prompt = prepared.tokens; // Tool-heavy requests prefer the reusable system/tool boundary under eviction. const bool prefer_inline_snap = !req.tools.empty(); @@ -3486,6 +3529,7 @@ void HttpServer::finalize_generation_cache( const GenerationCacheState & cache, const GenerateResult & result, int completion_tokens, bool visible_output_seen, bool client_disconnected) { + if (req.images) return; const auto & effective_prompt = prepared.tokens; const bool generation_produced_output = result.ok() && completion_tokens > 0 && visible_output_seen && !client_disconnected; @@ -3635,7 +3679,7 @@ void HttpServer::remember_agent_turn( for (const auto & call : emitter.tool_calls()) call_ids.push_back(call.id); tool_memory_.remember(call_ids, assistant_content); - if (!replay_cache) return; + if (!replay_cache || req.images) return; if (!config_.agent_turn_cache || prefix_cache_.disabled()) return; // Cache only stateless-equivalent prompts. Compression and token rewrites // need a separate replay contract. @@ -3732,6 +3776,8 @@ void HttpServer::prepare_generation_inputs( : req.max_output; inputs.request.prompt = prepared.tokens; + inputs.request.images = prepared.images; + inputs.request.force_ar_decode = bool(prepared.images); inputs.request.n_gen = inputs.generation_cap; inputs.request.sampler = req.sampler; inputs.request.do_sample = req.sampler.needs_logit_processing(); @@ -3930,7 +3976,9 @@ void HttpServer::process_job(ServerJob * job) { // Track live status for /status page. RAII guard ensures idle on all paths. std::string prompt_excerpt; - if (!req.prompt_tokens.empty()) { + if (req.images) { + prompt_excerpt = req.rendered_prompt.substr(0, 200); + } else if (!req.prompt_tokens.empty()) { // Decode first ~40 tokens as a prompt excerpt (cheap, bounded). const int excerpt_len = (std::min)((int)req.prompt_tokens.size(), 40); std::vector excerpt_toks(req.prompt_tokens.begin(), @@ -4138,7 +4186,7 @@ void HttpServer::process_job(ServerJob * job) { // Record performance for /status page. if (result.ok()) { PerfRecord perf; - perf.prompt_tokens = (int)req.prompt_tokens.size(); + perf.prompt_tokens = effective_prompt_tokens; perf.completion_tokens = completion_tokens; // Use actual prefilled token count: on cache hit the backend only // prefills the delta beyond the cached prefix, so dividing the full diff --git a/server/src/server/http_server.h b/server/src/server/http_server.h index 52c36473b..dbdfe880b 100644 --- a/server/src/server/http_server.h +++ b/server/src/server/http_server.h @@ -179,6 +179,7 @@ struct ServerConfig { int fa_window = 0; int ddtree_budget = 0; bool speculative_enabled = false; + bool image_input_enabled = false; bool target_sharding = false; // Prefill chunk size (bargs.chunk). Exposed at /props.runtime.chunk so // bench/snapshot tooling can capture the full server config — needed @@ -269,6 +270,7 @@ bool canonical_assistant_content( struct ParsedRequest { ApiFormat format; std::vector prompt_tokens; // tokenized prompt + ImagePromptHandle images; std::string rendered_prompt; int max_output = 4096; bool stream = true; @@ -386,6 +388,7 @@ class HttpServer { struct PreparedPrompt { std::vector tokens; + ImagePromptHandle images; bool compressed = false; bool flowkv = false; int full_cache_served_tokens = -1; diff --git a/server/src/server/image_input.cpp b/server/src/server/image_input.cpp index 3e0a2044f..a5b4a2ce9 100644 --- a/server/src/server/image_input.cpp +++ b/server/src/server/image_input.cpp @@ -161,6 +161,59 @@ bool extract_chat_images(const nlohmann::json & messages, nlohmann::json & norma } } +static bool contains_image_content(const nlohmann::json & value) { + std::vector pending{&value}; + while (!pending.empty()) { + const auto & current = *pending.back(); + pending.pop_back(); + if (current.is_object()) { + auto type = current.find("type"); + if (type != current.end() && type->is_string() && + (*type == "image" || *type == "input_image" || *type == "image_url")) return true; + for (const auto & item : current.items()) { + if (item.key() == "image_url") return true; + pending.push_back(&item.value()); + } + } else if (current.is_array()) { + for (const auto & item : current) pending.push_back(&item); + } + } + return false; +} + +bool prepare_request_images(const nlohmann::json & messages, + const ImageRequestPolicy & policy, + nlohmann::json & normalized, + std::vector & images, + std::string & error, const ImageInputLimits & limits) { + normalized = nullptr; + images.clear(); + error.clear(); + const bool has_images = contains_image_content(messages); + if (has_images && !policy.chat_completions) { + error = "image input is supported only through /v1/chat/completions image_url parts"; + return false; + } + if (has_images && !policy.image_capable) { + error = "image input is unavailable for this backend or serving mode; configure a supported --mmproj projector"; + return false; + } + if (policy.chat_completions && (has_images || policy.reserve_placeholder)) { + nlohmann::json prepared; + std::vector extracted; + if (!extract_chat_images(messages, prepared, extracted, error, limits)) return false; + if (contains_image_content(prepared)) { + error = "images must be user content-array image_url parts"; + return false; + } + normalized = std::move(prepared); + images = std::move(extracted); + } else { + normalized = messages; + } + return true; +} + void redact_image_urls(nlohmann::json & value) { std::vector pending{&value}; while (!pending.empty()) { diff --git a/server/src/server/image_input.h b/server/src/server/image_input.h index 18840f2d5..e62f313c8 100644 --- a/server/src/server/image_input.h +++ b/server/src/server/image_input.h @@ -1,5 +1,7 @@ #pragma once +#include "common/image_prompt.h" + #include #include #include @@ -11,17 +13,18 @@ namespace dflash::common { inline constexpr char DS4_IMAGE_PLACEHOLDER[] = "<|deepseek_image|>"; -struct EncodedImage { - std::string mime_type; - std::vector bytes; -}; - struct ImageInputLimits { size_t image_bytes = 16 * 1024 * 1024; size_t request_bytes = 32 * 1024 * 1024; size_t image_count = 4; }; +struct ImageRequestPolicy { + bool chat_completions = false; + bool image_capable = false; + bool reserve_placeholder = false; +}; + bool parse_image_data_url(std::string_view url, EncodedImage & image, std::string & error, size_t max_bytes = 16 * 1024 * 1024); bool extract_chat_images(const nlohmann::json & messages, @@ -29,6 +32,12 @@ bool extract_chat_images(const nlohmann::json & messages, std::vector & images, std::string & error, const ImageInputLimits & limits = {}); +bool prepare_request_images(const nlohmann::json & messages, + const ImageRequestPolicy & policy, + nlohmann::json & normalized, + std::vector & images, + std::string & error, + const ImageInputLimits & limits = {}); void redact_image_urls(nlohmann::json & value); } // namespace dflash::common diff --git a/server/src/server/server_main.cpp b/server/src/server/server_main.cpp index e2db605fa..7fca0732b 100644 --- a/server/src/server/server_main.cpp +++ b/server/src/server/server_main.cpp @@ -75,6 +75,7 @@ static void print_usage(const char * prog) { "\n" "Options:\n" " --draft Draft model for speculative decode\n" + " --mmproj DS4V image projector GGUF (heterogeneous HIP sparse mode)\n" " --port Listen port (default: 8080)\n" " --host Bind address (default: 0.0.0.0)\n" " --max-ctx Max context length (default: 131072)\n" @@ -280,6 +281,8 @@ int main(int argc, char ** argv) { for (int i = 2; i < argc; i++) { if (std::strcmp(argv[i], "--draft") == 0 && i + 1 < argc) { bargs.draft_path = argv[++i]; + } else if (std::strcmp(argv[i], "--mmproj") == 0 && i + 1 < argc) { + bargs.mmproj_path = argv[++i]; } else if (std::strcmp(argv[i], "--port") == 0 && i + 1 < argc) { sconfig.port = std::atoi(argv[++i]); } else if (std::strcmp(argv[i], "--host") == 0 && i + 1 < argc) { diff --git a/server/test/test_server_unit.cpp b/server/test/test_server_unit.cpp index fabd44408..3f15f34bd 100644 --- a/server/test/test_server_unit.cpp +++ b/server/test/test_server_unit.cpp @@ -19,6 +19,7 @@ #include "server/utf8_utils.h" #include "server/api_types.h" #include "server/http_server.h" +#include "server/image_input.h" #include "server/chat_template.h" #include "common/sampler.h" #include "common/concurrency/seq_engine.h" @@ -55,6 +56,7 @@ #include #include #include +#include #include #include #include @@ -7468,3 +7470,196 @@ TEST_CASE(ServerUnitFixture, TEST_ASSERT(!consumed_all); TEST_ASSERT((emitted == std::vector{101, 2})); } + +namespace { +json image_transport_part(const char * url = "data:image/png;base64,iVBORw0KGgo=") { + return {{"type", "image_url"}, {"image_url", {{"url", url}}}}; +} + +class OwnedImageTestPayload final : public ImagePromptPayload { +public: + explicit OwnedImageTestPayload(std::vector tokens) : tokens_(std::move(tokens)) {} + bool matches(const std::vector & tokens) const override { return tokens == tokens_; } +private: + const std::vector tokens_; +}; + +struct ImageCarrierRetryBackend : EmptySpecRetryBackend { + const ImagePromptPayload * expected = nullptr; + std::vector expected_tokens; + GenerateResult generate_impl(const GenerateRequest & req, const DaemonIO & io) override { + TEST_ASSERT(req.images.get() == expected); + TEST_ASSERT(req.prompt == expected_tokens); + TEST_ASSERT(req.images->matches(req.prompt)); + return EmptySpecRetryBackend::generate_impl(req, io); + } +}; +} + +TEST_CASE(ServerUnitFixture, test_image_extraction_normalization_preserves_interleaved_order) { + const json messages = json::array({ + {{"role", "user"}, {"content", json::array({ + {{"type", "text"}, {"text", "before "}}, image_transport_part(), + {{"type", "text"}, {"text", " between "}}, + image_transport_part("data:image/jpeg;base64,/9j/"), + {{"type", "text"}, {"text", " after"}}})}}, + {{"role", "assistant"}, {"content", "acknowledged"}}, + {{"role", "user"}, {"content", "follow-up"}} + }); + json normalized; + std::vector images; + std::string error; + TEST_ASSERT(prepare_request_images(messages, {true, true, true}, normalized, images, error)); + TEST_ASSERT(images.size() == 2); + TEST_ASSERT(images[0].mime_type == "image/png"); + TEST_ASSERT(images[0].bytes == std::vector({137, 80, 78, 71, 13, 10, 26, 10})); + TEST_ASSERT(images[1].mime_type == "image/jpeg"); + TEST_ASSERT(images[1].bytes == std::vector({255, 216, 255})); + ToolMemory memory; + const auto chat = normalize_chat_messages(normalized, ApiFormat::OPENAI_CHAT, memory); + TEST_ASSERT(chat.size() == 3); + TEST_ASSERT(chat[0].role == "user"); + TEST_ASSERT(chat[0].content == std::string("before ") + DS4_IMAGE_PLACEHOLDER + + " between " + DS4_IMAGE_PLACEHOLDER + " after"); + TEST_ASSERT(chat[1].content == "acknowledged" && chat[2].content == "follow-up"); + json retained = {{"messages", messages}, {"metadata", {{"image_url", "private-url"}, {"label", "keep"}}}}; + redact_image_urls(retained); + TEST_ASSERT(retained.dump().find("base64") == std::string::npos); + TEST_ASSERT(retained.dump().find("private-url") == std::string::npos); + TEST_ASSERT(retained["metadata"]["label"] == "keep"); + TEST_ASSERT(retained["messages"][0]["content"][0]["text"] == "before "); + TEST_ASSERT(messages[0]["content"][1]["image_url"]["url"] == "data:image/png;base64,iVBORw0KGgo="); +} + +TEST_CASE(ServerUnitFixture, test_image_extraction_failure_does_not_publish_partial_images) { + const json messages = json::array({{{"role", "user"}, {"content", json::array({ + image_transport_part(), image_transport_part("https://example.invalid/private.png") + })}}}); + json normalized = {{"stale", true}}; + std::vector images{{"stale", {1}}}; + std::string error; + TEST_ASSERT(!extract_chat_images(messages, normalized, images, error)); + TEST_ASSERT(normalized.is_null()); + TEST_ASSERT(images.empty()); + TEST_ASSERT(!error.empty()); + TEST_ASSERT(error.find("example.invalid") == std::string::npos); +} + +TEST_CASE(ServerUnitFixture, test_default_backend_rejects_encoded_images_without_rewriting_tokens) { + MockBackend backend; + std::vector tokens{1, 2, 3}; + ImagePromptHandle payload; + std::string error; + TEST_ASSERT(!backend.supports_images()); + TEST_ASSERT(!backend.prepare_images(tokens, {{"image/png", {137, 80, 78, 71}}}, + 8192, 32, payload, error)); + TEST_ASSERT(tokens == std::vector({1, 2, 3})); + TEST_ASSERT(!payload && !error.empty()); + TEST_ASSERT(backend.prepare_images(tokens, {}, 8192, 32, payload, error)); + TEST_ASSERT(tokens == std::vector({1, 2, 3})); +} + +TEST_CASE(ServerUnitFixture, test_image_binding_survives_request_copies_and_common_ar_retry) { + static_assert(std::is_const_v); + GenerateRequest request; + std::weak_ptr lifetime; + { + ParsedRequest parsed; + parsed.prompt_tokens = {1, 129280, 129281, 2}; + parsed.images = std::make_shared(parsed.prompt_tokens); + lifetime = parsed.images; + ParsedRequest queued = parsed; + parsed.prompt_tokens.clear(); + parsed.images.reset(); + request.prompt = queued.prompt_tokens; + request.images = queued.images; + } + TEST_ASSERT(!lifetime.expired()); + auto changed = request.prompt; + changed[1] += 1; + TEST_ASSERT(!request.images->matches(changed)); + TEST_ASSERT(request.images->matches(request.prompt)); + ImageCarrierRetryBackend backend; + backend.expected = request.images.get(); + backend.expected_tokens = request.prompt; + request.n_gen = 1; + DaemonIO io; + TEST_ASSERT(backend.generate(request, io).ok()); + TEST_ASSERT(backend.generate_calls == 2 && backend.generate_saw_force_ar); + request.images.reset(); + TEST_ASSERT(lifetime.expired()); +} + +TEST_CASE(ServerUnitFixture, test_http_image_policy_rejects_unconsumed_images_including_mixed_valid) { + const json image = image_transport_part(); + const std::vector malformed_messages = { + {{"role", "user"}, {"content", image}}, + {{"role", "user"}, {"content", json::array({ + {{"type", "text"}, {"text", "visible"}, {"image_url", image["image_url"]}} + })}}, + {{"role", "user"}, {"content", json::array({ + {{"type", "container"}, {"nested", image}} + })}}, + {{"role", "user"}, {"content", "visible"}, {"attachment", image}}, + {{"role", "user"}, {"content", json::array({ + {{"type", "input_image"}, {"image_url", image["image_url"]}} + })}} + }; + for (const auto & malformed : malformed_messages) { + for (bool mixed : {false, true}) { + json messages = json::array(); + if (mixed) messages.push_back({{"role", "user"}, {"content", json::array({image})}}); + messages.push_back(malformed); + json normalized = "stale"; + std::vector images{{"stale", {1}}}; + std::string error; + TEST_ASSERT(!prepare_request_images(messages, {true, true, true}, normalized, images, error)); + TEST_ASSERT(normalized.is_null() && images.empty()); + TEST_ASSERT(!error.empty()); + TEST_ASSERT(error.find("base64") == std::string::npos); + } + } +} + +TEST_CASE(ServerUnitFixture, test_http_image_policy_requires_chat_endpoint_and_effective_capability) { + const json messages = json::array({{{"role", "user"}, {"content", json::array({image_transport_part()})}}}); + for (ImageRequestPolicy policy : { + ImageRequestPolicy{false, true, true}, + ImageRequestPolicy{true, false, true}, + ImageRequestPolicy{false, false, true}}) { + json normalized = "stale"; + std::vector images{{"stale", {1}}}; + std::string error; + TEST_ASSERT(!prepare_request_images(messages, policy, normalized, images, error)); + TEST_ASSERT(normalized.is_null() && images.empty()); + TEST_ASSERT(!error.empty()); + } + json normalized; + std::vector images; + std::string error; + TEST_ASSERT(prepare_request_images(messages, {true, true, true}, normalized, images, error)); + TEST_ASSERT(images.size() == 1); +} + +TEST_CASE(ServerUnitFixture, test_http_image_policy_preserves_text_without_image_capability) { + const json messages = json::array({ + {{"role", "system"}, {"content", "instructions"}}, + {{"role", "user"}, {"content", json::array({{{"type", "text"}, {"text", "ordinary text"}}})}} + }); + for (ImageRequestPolicy policy : { + ImageRequestPolicy{true, false, true}, + ImageRequestPolicy{false, false, true}, + ImageRequestPolicy{true, true, true}}) { + json normalized; + std::vector images; + std::string error; + TEST_ASSERT(prepare_request_images(messages, policy, normalized, images, error)); + TEST_ASSERT(normalized == messages && images.empty()); + } + json normalized; + std::vector images; + std::string error; + const json forged = json::array({{{"role", "user"}, {"content", DS4_IMAGE_PLACEHOLDER}}}); + TEST_ASSERT(!prepare_request_images(forged, {true, false, true}, normalized, images, error)); + TEST_ASSERT(normalized.is_null() && images.empty()); +} diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index 0d3d8d361..d53942f2f 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -1,6 +1,8 @@ #include "ggml.h" #include "ggml-alloc.h" #include "ggml-backend.h" +#include "../deps/llama.cpp/ggml/src/ggml-backend-impl.h" +#include "deepseek4/deepseek4_image_admission.h" #include "ggml-cpu.h" #if defined(GGML_USE_CUDA) || defined(GGML_USE_HIP) #include "ggml-cuda.h" @@ -100,6 +102,14 @@ struct DeepSeek4FixtureOptions { gguf_type compress_ratios_type = GGUF_TYPE_UINT32; int32_t eos_id = -1; int32_t eot_id = -1; + uint32_t block_count = 43; + bool image_biases = false; + int missing_image_bias = -1; + int malformed_image_bias = -1; + ggml_type image_bias_type = GGML_TYPE_F32; + int image_bias_width = 256; + int image_bias_rows = 1; + bool add_mtp_image_bias = false; }; static std::string make_temp_gguf_path(const char * prefix) { @@ -115,7 +125,7 @@ static std::string make_temp_gguf_path(const char * prefix) { static std::string write_deepseek4_loader_fixture(const DeepSeek4FixtureOptions & opts) { gguf_context * g = gguf_init_empty(); gguf_set_val_str(g, "general.architecture", "deepseek4"); - gguf_set_val_u32(g, "deepseek4.block_count", 43); + gguf_set_val_u32(g, "deepseek4.block_count", opts.block_count); gguf_set_val_u32(g, "deepseek4.embedding_length", 4096); if (opts.include_vocab_size) { gguf_set_val_u32(g, "deepseek4.vocab_size", opts.vocab_size); @@ -170,9 +180,29 @@ static std::string write_deepseek4_loader_fixture(const DeepSeek4FixtureOptions gguf_set_val_u32(g, "tokenizer.ggml.eot_token_id", (uint32_t)opts.eot_id); } + ggml_context * tensor_ctx = nullptr; + if (opts.image_biases) { + tensor_ctx = ggml_init({1u << 20, nullptr, false}); + for (int layer = 0; layer < (opts.add_mtp_image_bias ? 44 : 43); ++layer) { + if (layer == opts.missing_image_bias) continue; + const bool malformed = layer == opts.malformed_image_bias; + ggml_tensor * bias = ggml_new_tensor_2d(tensor_ctx, + malformed ? opts.image_bias_type : GGML_TYPE_F32, + malformed ? opts.image_bias_width : 256, + malformed ? opts.image_bias_rows : 1); + const std::string name = "layers." + std::to_string(layer) + ".ffn.gate.bias_vl"; + ggml_set_name(bias, name.c_str()); + std::memset(bias->data, 0, ggml_nbytes(bias)); + if (bias->type == GGML_TYPE_F32) { + std::fill_n(static_cast(bias->data), ggml_nelements(bias), float(layer + 1)); + } + gguf_add_tensor(g, bias); + } + } const std::string path = make_temp_gguf_path("fixture"); gguf_write_to_file(g, path.c_str(), /*only_meta=*/false); gguf_free(g); + if (tensor_ctx) ggml_free(tensor_ctx); return path; } @@ -1213,6 +1243,321 @@ struct ScopedEnvVar { std::string old_value; }; +// Metadata-only device: every allocation/graph callback is a test failure. +struct ImageAdmissionFakeOwner { + ggml_backend_buffer_type buft{}; + ggml_backend_device device{}; + ggml_backend backend{}; + size_t alignment = 128; + size_t padding = 64; + size_t maximum = SIZE_MAX; + size_t forced_allocation = 0; + size_t free_bytes = 8ULL * 1024 * 1024 * 1024; + size_t total_bytes = 8ULL * 1024 * 1024 * 1024; + size_t queries = 0; + size_t allocation_calls = 0; + size_t graph_calls = 0; + + ImageAdmissionFakeOwner() { + buft.context = this; + buft.device = &device; + buft.iface.get_name = [](ggml_backend_buffer_type_t) { return "image-admission-fake"; }; + buft.iface.get_alignment = [](ggml_backend_buffer_type_t b) { + return static_cast(b->context)->alignment; + }; + buft.iface.get_max_size = [](ggml_backend_buffer_type_t b) { + return static_cast(b->context)->maximum; + }; + buft.iface.get_alloc_size = [](ggml_backend_buffer_type_t b, const ggml_tensor * t) { + auto & owner = *static_cast(b->context); + ++owner.queries; + return owner.forced_allocation ? owner.forced_allocation : ggml_nbytes(t) + owner.padding; + }; + buft.iface.alloc_buffer = [](ggml_backend_buffer_type_t b, size_t) -> ggml_backend_buffer_t { + ++static_cast(b->context)->allocation_calls; + TEST_ASSERT_MSG(false, "admission must not allocate a device buffer"); + return nullptr; + }; + device.context = this; + device.iface.get_type = [](ggml_backend_dev_t) { return GGML_BACKEND_DEVICE_TYPE_GPU; }; + device.iface.get_buffer_type = [](ggml_backend_dev_t d) { + return &static_cast(d->context)->buft; + }; + device.iface.get_memory = [](ggml_backend_dev_t d, size_t * free, size_t * total) { + const auto & owner = *static_cast(d->context); + *free = owner.free_bytes; + *total = owner.total_bytes; + }; + backend.device = &device; + backend.context = this; + backend.iface.graph_compute = [](ggml_backend_t b, ggml_cgraph *) { + ++static_cast(b->context)->graph_calls; + TEST_ASSERT_MSG(false, "admission must not execute a graph"); + return GGML_STATUS_FAILED; + }; + } +}; + +static void test_image_storage_admission_metadata() { + std::fprintf(stderr, "test_image_storage_admission_metadata..."); + using namespace dflash::vision; + ScopedEnvVar duplicate_env("DFLASH_MOE_DUPLICATE_HOT_ON_COLD"); + ScopedEnvVar decode_env("DFLASH_DS4_DECODE_ALL_COLD"); + unsetenv("DFLASH_MOE_DUPLICATE_HOT_ON_COLD"); + unsetenv("DFLASH_DS4_DECODE_ALL_COLD"); + ggml_init_params params{}; + params.mem_size = 1024 * 1024; + params.no_alloc = true; + ggml_context * ctx = ggml_init(params); + TEST_ASSERT(ctx != nullptr); + if (!ctx) return; + DeepSeek4Weights weights; + weights.n_layer = 1; + weights.n_expert = 4; + weights.n_expert_used = 2; + weights.n_embd = 128; + weights.n_ff_exp = 128; + weights.layers.resize(1); + auto & layer = weights.layers[0]; + layer.ffn_gate_exps = ggml_new_tensor_3d(ctx, GGML_TYPE_Q2_1_ROCMFP2_MIX, 128, 128, 4); + layer.ffn_up_exps = ggml_new_tensor_3d(ctx, GGML_TYPE_Q2_1_ROCMFP2_MIX, 128, 128, 4); + layer.ffn_down_exps = ggml_new_tensor_3d(ctx, GGML_TYPE_Q3_1_ROCMFP3_MIX, 128, 128, 4); + MoeHybridConfig config; + config.n_layer = 1; + config.n_expert = 4; + config.n_expert_used = 2; + config.n_embd = 128; + config.n_ff_exp = 128; + config.cold_expert_backend = MoeHybridColdBackend::Gpu; + MoeHybridPlacement placement; + placement.n_layer = 1; + placement.n_expert = 4; + placement.n_expert_used = 2; + placement.total_hot = 1; + placement.hot_counts = {1}; + placement.hot_expert_ids = {{2}}; + ImageAdmissionFakeOwner hot, cold; + cold.alignment = 256; + cold.padding = 257; + std::string error; + ImageStorageEstimate initial; + TEST_ASSERT(estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, initial, error)); + // Pinned qtype-106 and105 formats occupy10 and14 bytes per32 weights. + // Three128x128 surfaces occupy5120+5120+7168 bytes per expert. + TEST_ASSERT(initial.hot_payload_bytes == 17408); + TEST_ASSERT(initial.cold_payload_bytes == 3 * 17408); + TEST_ASSERT(initial.hot_allocation_bytes == 17408 + 3 * 128); + TEST_ASSERT(initial.cold_allocation_bytes == 3 * 17408 + 3 * 512); + TEST_ASSERT(initial.hot_mix_table_bytes == 17 + 17 + 33); + TEST_ASSERT(initial.cold_mix_table_bytes == 3 * (17 + 17 + 33)); + TEST_ASSERT(initial.mix_device_allocation_count == 12); + TEST_ASSERT(initial.largest_copy_bytes == 3 * 7168); + TEST_ASSERT(initial.host_copy_peak_bytes == 3 * initial.largest_copy_bytes); + TEST_ASSERT(initial.host_mix_payload_peak_bytes == 34 * 4 + 33 * 3 + 4); + TEST_ASSERT(initial.hot_buffer_count == 1 && initial.cold_buffer_count == 1); + TEST_ASSERT(hot.queries == 3 && cold.queries == 3); + + // Changing the actual allocator's maximum splits buffers without losing + // padding charges or pretending the whole layer must fit one allocation. + hot.maximum = 10000; + cold.maximum = 23000; + ImageStorageEstimate split; + TEST_ASSERT(estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + TEST_ASSERT(split.hot_buffer_count == 3 && split.cold_buffer_count == 3); + TEST_ASSERT(split.hot_allocation_bytes == initial.hot_allocation_bytes); + TEST_ASSERT(split.cold_allocation_bytes == initial.cold_allocation_bytes); + cold.maximum = 22015; // one byte below the padded down-expert allocation + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + hot.maximum = cold.maximum = SIZE_MAX; + cold.forced_allocation = SIZE_MAX; + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + TEST_ASSERT(error.find("allocation size") != std::string::npos); + cold.forced_allocation = 0; + + setenv("DFLASH_MOE_DUPLICATE_HOT_ON_COLD", "1", 1); + ImageStorageEstimate duplicated; + TEST_ASSERT(estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, true, duplicated, error)); + TEST_ASSERT(duplicated.hot_payload_bytes == initial.hot_payload_bytes); + TEST_ASSERT(duplicated.cold_payload_bytes == initial.hot_payload_bytes + initial.cold_payload_bytes); + TEST_ASSERT(duplicated.cold_mix_table_bytes == 4 * (17 + 17 + 33)); + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, duplicated, error)); + unsetenv("DFLASH_MOE_DUPLICATE_HOT_ON_COLD"); + setenv("DFLASH_DS4_DECODE_ALL_COLD", "1", 1); + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + unsetenv("DFLASH_DS4_DECODE_ALL_COLD"); + + placement.total_hot = 0; + placement.hot_counts = {0}; + placement.hot_expert_ids = {{}}; + TEST_ASSERT(estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + TEST_ASSERT(split.hot_payload_bytes == 0 && split.hot_mix_table_bytes == 0); + TEST_ASSERT(split.cold_payload_bytes == initial.hot_payload_bytes + initial.cold_payload_bytes); + TEST_ASSERT(split.cold_mix_table_bytes == 4 * 67); + placement.total_hot = 1; + placement.hot_counts = {1}; + placement.hot_expert_ids = {{2}}; + const int64_t original_experts = layer.ffn_gate_exps->ne[2]; + layer.ffn_gate_exps->ne[2] = 3; + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + layer.ffn_gate_exps->ne[2] = original_experts; + const size_t original_stride = layer.ffn_gate_exps->nb[2]; + ++layer.ffn_gate_exps->nb[2]; + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + layer.ffn_gate_exps->nb[2] = original_stride; + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &hot.backend, false, split, error)); + config.materialize_cold_experts = false; + TEST_ASSERT(!estimate_deepseek4_image_storage(weights, placement, config, + &hot.backend, &cold.backend, false, split, error)); + config.materialize_cold_experts = true; + + // Actual wrapper sees the fake device's exhausted snapshot. It cannot pass + // regardless of the machine's MemAvailable; no positive case reads /proc. + ImageAdmissionReserves reserves; + reserves.primary_domain = ImageMemoryDomain::Dedicated; + reserves.cold_domain = ImageMemoryDomain::HostShared; + reserves.cold_runtime_reservation_bytes = 2ULL * 1024 * 1024 * 1024; + reserves.host_request_bytes = 1024 * 1024; + reserves.host_loader_overhead_bytes = 1024 * 1024; + cold.free_bytes = 0; + ImageAdmissionReport report; + TEST_ASSERT(!check_deepseek4_image_admission(weights, placement, config, + &hot.backend, &cold.backend, reserves, report, error)); + TEST_ASSERT(report.storage_estimated && !report.known_charges_fit); + TEST_ASSERT(report.cold_free_bytes == 0); + TEST_ASSERT(!check_deepseek4_image_runtime_admission(config, + &hot.backend, &cold.backend, reserves, report, error)); + TEST_ASSERT(report.storage.hot_allocation_bytes == 0 && report.storage.cold_allocation_bytes == 0); + TEST_ASSERT(report.cold_required_bytes == reserves.cold_runtime_reservation_bytes); + TEST_ASSERT(!check_deepseek4_image_host_preparation(0, error)); + TEST_ASSERT(!check_deepseek4_image_host_preparation(std::numeric_limits::max(), error)); + TEST_ASSERT(hot.allocation_calls == 0 && cold.allocation_calls == 0); + TEST_ASSERT(hot.graph_calls == 0 && cold.graph_calls == 0); + ggml_free(ctx); + std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); +} + +static void test_image_admission_resource_snapshots() { + std::fprintf(stderr, "test_image_admission_resource_snapshots..."); + using namespace dflash::vision; + ImageStorageEstimate storage; + storage.hot_allocation_bytes = 100; + storage.cold_allocation_bytes = 300; + storage.hot_mix_table_bytes = 10; + storage.cold_mix_table_bytes = 30; + storage.host_copy_peak_bytes = 50; + storage.host_mix_payload_peak_bytes = 5; + ImageAdmissionReserves reserves; + reserves.primary_domain = ImageMemoryDomain::Dedicated; + reserves.cold_domain = ImageMemoryDomain::HostShared; + reserves.primary_future_bytes = 20; + reserves.cold_future_bytes = 40; + reserves.cold_runtime_reservation_bytes = 200; + reserves.host_loader_overhead_bytes = 15; + reserves.host_request_bytes = 100; + reserves.host_runtime_bytes = 7; + ImageMemorySnapshot snapshot{130, 570, 677}; + ImageAdmissionReport report; + std::string error; + const auto assess = [&]() { + return assess_deepseek4_image_admission(storage, 150, reserves, snapshot, report, error); + }; + TEST_ASSERT(assess()); + TEST_ASSERT(report.primary_required_bytes == 130 && report.cold_required_bytes == 570); + TEST_ASSERT(report.host_required_bytes == 677); // max(load70,request100)+runtime7+UMA570 + --snapshot.host_available_bytes; + TEST_ASSERT(!assess()); // independent device checks would both pass + TEST_ASSERT(!report.known_charges_fit); + snapshot.host_available_bytes = 677; + --snapshot.cold_free_bytes; + TEST_ASSERT(!assess()); + snapshot.cold_free_bytes = 570; + --snapshot.primary_free_bytes; + TEST_ASSERT(!assess()); + snapshot.primary_free_bytes = 130; + reserves.cold_domain = ImageMemoryDomain::Dedicated; + snapshot.host_available_bytes = 107; + TEST_ASSERT(assess()); // dedicated VRAM must not also consume host admission + reserves.primary_domain = ImageMemoryDomain::HostShared; + reserves.cold_domain = ImageMemoryDomain::HostShared; + snapshot.host_available_bytes = 807; + TEST_ASSERT(assess()); + TEST_ASSERT(report.host_required_bytes == 807); + --snapshot.host_available_bytes; + TEST_ASSERT(!assess()); + reserves.primary_domain = ImageMemoryDomain::Unknown; + TEST_ASSERT(!assess()); + reserves.primary_domain = ImageMemoryDomain::Dedicated; + snapshot.host_available_bytes = 677; + reserves.host_request_bytes = 60; + TEST_ASSERT(assess()); + TEST_ASSERT(report.host_required_bytes == 647); // load70 exceeds request60 + reserves.host_request_bytes = 100; + reserves.cold_runtime_reservation_bytes = 149; + TEST_ASSERT(!assess()); + TEST_ASSERT(report.known_charges_fit); // rejection is missing runtime headroom, not physical exhaustion + reserves.cold_runtime_reservation_bytes = 200; + reserves.host_request_bytes = 0; + TEST_ASSERT(!assess()); + reserves.host_request_bytes = 100; + reserves.host_loader_overhead_bytes = 0; + TEST_ASSERT(!assess()); + reserves.host_loader_overhead_bytes = 15; + + // After startup materializes the experts/tables, their bytes disappear + // from free memory and from new allocations together. Retained KV/draft + // snapshots added later only shrink the next free-memory snapshot. + const ImageStorageEstimate resident_storage{}; + ImageAdmissionReserves runtime_reserves = reserves; + runtime_reserves.host_loader_overhead_bytes = 0; + ImageMemorySnapshot runtime_snapshot{20, 240, 347}; + const auto runtime_assess = [&]() { + return assess_deepseek4_image_admission(resident_storage, 150, + runtime_reserves, runtime_snapshot, report, error); + }; + TEST_ASSERT(runtime_assess()); + TEST_ASSERT(report.primary_required_bytes == 20 && report.cold_required_bytes == 240); + TEST_ASSERT(report.host_required_bytes == 347); + --runtime_snapshot.primary_free_bytes; + TEST_ASSERT(!runtime_assess()); + runtime_snapshot.primary_free_bytes = 20; + --runtime_snapshot.cold_free_bytes; + TEST_ASSERT(!runtime_assess()); + runtime_snapshot.cold_free_bytes = 240; + --runtime_snapshot.host_available_bytes; + TEST_ASSERT(!runtime_assess()); + + constexpr uint64_t max = std::numeric_limits::max(); + snapshot = {max, max, max}; + storage.hot_allocation_bytes = max; + TEST_ASSERT(!assess()); + TEST_ASSERT(error.find("overflow") != std::string::npos); + storage.hot_allocation_bytes = 100; + storage.host_copy_peak_bytes = max; + TEST_ASSERT(!assess()); + TEST_ASSERT(error.find("overflow") != std::string::npos); + storage.host_copy_peak_bytes = 50; + reserves.host_request_bytes = max - 100; + TEST_ASSERT(!assess()); // UMA sum overflows even though separate device budgets fit + TEST_ASSERT(error.find("overflow") != std::string::npos); + reserves.host_request_bytes = 100; + TEST_ASSERT(assess()); + TEST_ASSERT(assess_deepseek4_image_admission(report.storage, 150, reserves, + snapshot, report, error)); // report may safely be reused without losing its estimate + TEST_ASSERT(report.primary_required_bytes == 130); + std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); +} + static DeepSeek4LayerSplitAdapter make_test_adapter() { DeepSeek4LayerSplitAdapterConfig cfg; cfg.device.gpu = 0; @@ -1614,6 +1959,206 @@ static void test_loader_rejects_truncated_tensor_data(ggml_backend_t backend) { std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); } + +static void test_image_bias_loader_opt_in_contract(ggml_backend_t backend) { + std::fprintf(stderr, " test_image_bias_loader_opt_in_contract ..."); + DeepSeek4FixtureOptions valid; + valid.vocab_size = 129280; + valid.image_biases = true; + valid.add_mtp_image_bias = true; + const std::string path = write_deepseek4_loader_fixture(valid); + for (bool enabled : {false, true}) { + TargetLoadPlan plan; + plan.load_ds4_image_bias = enabled; + DeepSeek4Weights weights; + const bool ok = load_deepseek4_gguf_partial(path, backend, plan, weights); + TEST_ASSERT_MSG(ok, dflash27b_last_error()); + if (ok) { + TEST_ASSERT(weights.layers.size() == 43); + for (size_t i = 0; i < weights.layers.size(); ++i) { + const auto bias = weights.layers[i].ffn_gate_bias_vl; + TEST_ASSERT(bool(bias) == enabled); + if (bias) { + TEST_ASSERT(bias->type == GGML_TYPE_F32 && ggml_nelements(bias) == 256); + std::vector values(256); + ggml_backend_tensor_get(bias, values.data(), 0, values.size() * sizeof(float)); + TEST_ASSERT(std::all_of(values.begin(), values.end(), + [i](float value) { return value == float(i + 1); })); + } + } + if (weights.ctx) { + const auto mtp = ggml_get_tensor(weights.ctx, "layers.43.ffn.gate.bias_vl"); + TEST_ASSERT(mtp == nullptr || (mtp->buffer == nullptr && mtp->data == nullptr)); + } + } + free_deepseek4_weights(weights); + } + for (int boundary : {0, 1}) { + TargetLoadPlan plan; + plan.load_ds4_image_bias = true; + if (boundary == 0) plan.layer_begin = 1; + else plan.layer_end = 42; + DeepSeek4Weights weights; + TEST_ASSERT(!load_deepseek4_gguf_partial(path, backend, plan, weights)); + TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find("exactly 43 F32[256]") != std::string::npos, + dflash27b_last_error()); + TEST_ASSERT(weights.ctx == nullptr && weights.buf == nullptr); + free_deepseek4_weights(weights); + } + unlink(path.c_str()); + + std::vector invalid; + auto missing_all = valid; + missing_all.image_biases = false; + invalid.push_back(missing_all); + for (int layer : {0, 21, 42}) { + auto missing = valid; + missing.missing_image_bias = layer; + invalid.push_back(missing); + } + auto wrong_type = valid; + wrong_type.malformed_image_bias = 21; + wrong_type.image_bias_type = GGML_TYPE_F16; + invalid.push_back(wrong_type); + auto wrong_width = valid; + wrong_width.malformed_image_bias = 42; + wrong_width.image_bias_width = 255; + invalid.push_back(wrong_width); + auto matrix = valid; + matrix.malformed_image_bias = 0; + matrix.image_bias_width = 128; + matrix.image_bias_rows = 2; + invalid.push_back(matrix); + auto wrong_layers = valid; + wrong_layers.block_count = 42; + invalid.push_back(wrong_layers); + auto wrong_vocab = valid; + wrong_vocab.vocab_size = 129279; + invalid.push_back(wrong_vocab); + for (const auto & options : invalid) { + const std::string bad_path = write_deepseek4_loader_fixture(options); + TargetLoadPlan plan; + plan.load_ds4_image_bias = true; + DeepSeek4Weights weights; + TEST_ASSERT(!load_deepseek4_gguf_partial(bad_path, backend, plan, weights)); + TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find("exactly 43 F32[256]") != std::string::npos, + dflash27b_last_error()); + TEST_ASSERT(weights.ctx == nullptr && weights.buf == nullptr && weights.dense_split_buf == nullptr); + free_deepseek4_weights(weights); + plan.load_ds4_image_bias = false; + TEST_ASSERT_MSG(load_deepseek4_gguf_partial(bad_path, backend, plan, weights), dflash27b_last_error()); + for (const auto & layer : weights.layers) TEST_ASSERT(layer.ffn_gate_bias_vl == nullptr); + free_deepseek4_weights(weights); + unlink(bad_path.c_str()); + } + std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); +} + +static void test_image_batch_admission_before_execution(ggml_backend_t backend) { + std::fprintf(stderr, " test_image_batch_admission_before_execution ..."); + ggml_context * ctx = ggml_init({1u << 20, nullptr, false}); + ggml_tensor * bias = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 256); + ggml_tensor * state = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, 1); + static_cast(state->data)[0] = 123.0f; + DeepSeek4Weights weights; + weights.n_vocab = 129280; + weights.n_layer = 43; + weights.moe_hybrid = true; + weights.layers.resize(43); + weights.compress_ratios.resize(43); + DeepSeek4Cache cache; + cache.max_ctx = 32; + cache.cur_pos = 17; + cache.prefill_mode = PrefillAttentionMode::Sparse; + cache.layers.resize(43); + MoeHybridStorage hybrid; + hybrid.layers.resize(43); + hybrid.cold_backend_kind = MoeHybridColdBackend::Gpu; + hybrid.cold_backend = backend; + for (int i = 0; i < 43; ++i) { + weights.compress_ratios[size_t(i)] = i < 2 ? 0 : i % 2 == 0 ? 4 : 128; + auto & layer = weights.layers[size_t(i)]; + auto & layer_cache = cache.layers[size_t(i)]; + layer.ffn_gate_bias_vl = bias; + layer.attn_compressor_ape = layer.attn_compressor_kv = layer.attn_compressor_gate = layer.attn_compressor_norm = state; + layer.indexer_compressor_ape = layer.indexer_compressor_kv = layer.indexer_compressor_gate = layer.indexer_compressor_norm = state; + layer_cache.raw_kv = layer_cache.comp_kv = layer_cache.index_comp_kv = state; + layer_cache.attn_compressor.state_kv = layer_cache.attn_compressor.state_score = state; + layer_cache.indexer_compressor.state_kv = layer_cache.indexer_compressor.state_score = state; + } + const dflash::vision::TokenSpan span{1, 2, 5, 6}; + const dflash::vision::ImageSpanView spans{&span, 1}; + std::vector tokens{7, 129280, 129281, 129282, 129283, 129284, 8}; + bool has_images = false; + std::string error; + auto validate = [&]() { + error.clear(); + return deepseek4_validate_image_batch(weights, cache, &hybrid, tokens.data(), + int(tokens.size()), 0, spans, has_images, error); + }; + TEST_ASSERT(validate() && has_images); + for (size_t position : {size_t(0), size_t(1), size_t(5), size_t(6)}) { + const int32_t original = tokens[position]; + tokens[position] = position == 0 || position == 6 ? 129280 : 7; + TEST_ASSERT(!validate()); + TEST_ASSERT(error.find("token IDs") != std::string::npos); + tokens[position] = original; + } + tokens[2] = 129285; + TEST_ASSERT(!validate()); + tokens[2] = 129281; + TEST_ASSERT(!deepseek4_validate_image_batch(weights, cache, &hybrid, tokens.data(), + 5, 0, spans, has_images, error)); + TEST_ASSERT(!deepseek4_validate_image_batch(weights, cache, &hybrid, tokens.data() + 2, + 5, 2, spans, has_images, error)); + TEST_ASSERT(!deepseek4_validate_image_batch(weights, cache, &hybrid, nullptr, + 7, 0, spans, has_images, error)); + const auto check_missing_tensor = [&](ggml_tensor * & tensor) { + ggml_tensor * saved = tensor; + tensor = nullptr; + TEST_ASSERT(!validate()); + tensor = saved; + TEST_ASSERT(validate()); + }; + for (int i : {0, 2, 3, 42}) { + auto & layer = weights.layers[size_t(i)]; + auto & layer_cache = cache.layers[size_t(i)]; + check_missing_tensor(layer.ffn_gate_bias_vl); + check_missing_tensor(layer_cache.raw_kv); + if (i >= 2) { + check_missing_tensor(layer.attn_compressor_gate); + check_missing_tensor(layer_cache.attn_compressor.state_score); + } + if (i >= 2 && i % 2 == 0) { + check_missing_tensor(layer.indexer_compressor_kv); + check_missing_tensor(layer_cache.indexer_compressor.state_kv); + } + } + weights.layers[42].ffn_gate_bias_vl = ggml_new_tensor_1d(ctx, GGML_TYPE_F16, 256); + TEST_ASSERT(!validate()); + weights.layers[42].ffn_gate_bias_vl = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, 256, 2); + TEST_ASSERT(!validate()); + weights.layers[42].ffn_gate_bias_vl = bias; + TEST_ASSERT(validate()); + weights.compress_ratios[42] = 128; + TEST_ASSERT(!validate()); + weights.compress_ratios[42] = 4; + hybrid.materialized_cold_experts = false; + TEST_ASSERT(!validate()); + hybrid.materialized_cold_experts = true; + cache.layers.pop_back(); + TEST_ASSERT(!validate()); + TEST_ASSERT(cache.cur_pos == 17 && static_cast(state->data)[0] == 123.0f); + const dflash::vision::TokenSpan invalid_span{1, 2, 7, 6}; + TEST_ASSERT(!deepseek4_validate_image_batch(weights, cache, &hybrid, tokens.data(), + 7, 0, {&invalid_span, 1}, has_images, error)); + const int32_t text[] = {1, 2}; + TEST_ASSERT(deepseek4_validate_image_batch(weights, cache, nullptr, text, 2, 6, + spans, has_images, error) && !has_images); + ggml_free(ctx); + std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); +} + static void test_dspark_loader_contract_and_bounds(ggml_backend_t backend) { std::fprintf(stderr, " test_dspark_loader_contract_and_bounds ..."); @@ -4148,6 +4693,10 @@ int main() { test_loader_rejects_zero_vocab_size(backend); test_loader_reads_tokenizer_special_ids(backend); test_loader_rejects_truncated_tensor_data(backend); + test_image_bias_loader_opt_in_contract(backend); + test_image_batch_admission_before_execution(backend); + test_image_storage_admission_metadata(); + test_image_admission_resource_snapshots(); test_dspark_loader_contract_and_bounds(backend); test_dspark_confidence_uses_separate_hidden(backend); test_safe_compressor_batch_tokens(); diff --git a/server/tools/ds4v_image_assembly/CMakeLists.txt b/server/tools/ds4v_image_assembly/CMakeLists.txt new file mode 100644 index 000000000..21b13d1ef --- /dev/null +++ b/server/tools/ds4v_image_assembly/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4v_image_assembly LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +add_library(ds4v_image_assembly STATIC ../../src/deepseek4/deepseek4_image_assembly.cpp) +target_include_directories(ds4v_image_assembly PUBLIC ../../src) +target_compile_options(ds4v_image_assembly PRIVATE -Wall -Wextra -Werror) +add_executable(test_ds4v_image_assembly test.cpp) +target_link_libraries(test_ds4v_image_assembly PRIVATE ds4v_image_assembly) +target_compile_options(test_ds4v_image_assembly PRIVATE -Wall -Wextra -Werror) +enable_testing() +add_test(NAME ds4v_image_assembly COMMAND test_ds4v_image_assembly) diff --git a/server/tools/ds4v_image_assembly/test.cpp b/server/tools/ds4v_image_assembly/test.cpp new file mode 100644 index 000000000..fcaedfc80 --- /dev/null +++ b/server/tools/ds4v_image_assembly/test.cpp @@ -0,0 +1,145 @@ +#include "deepseek4/deepseek4_image_assembly.h" +#include +#include +#include + +using namespace dflash::vision; +static void check(bool ok, const char * message) { + if (!ok) throw std::runtime_error(message); +} +static PromptImage fixture(uint64_t position) { + PromptImage image; + image.input.plan.aligner_rows = 2; + image.input.plan.aligner_cols = 2; + image.layout.span = {position, position + 1, position + 9, position + 10}; + image.layout.types = {ImageTokenType::Pad, ImageTokenType::Start, + ImageTokenType::Image, ImageTokenType::Image, ImageTokenType::Newline, + ImageTokenType::Image, ImageTokenType::Image, ImageTokenType::Pad, + ImageTokenType::End, ImageTokenType::Pad}; + image.layout.permutation = {2, 0, 3, 1}; + return image; +} + +int main() { + try { + ImageRequestGate gate; + auto lease = gate.try_acquire(); + check(bool(lease) && !gate.try_acquire(), "concurrent image request was admitted"); + auto retained_lease = lease; + lease.reset(); + check(!gate.try_acquire(), "request copy released image admission early"); + retained_lease.reset(); + lease = gate.try_acquire(); + check(bool(lease), "completed request did not release image admission"); + { + ImageRequestGate transient; + retained_lease = transient.try_acquire(); + } + retained_lease.reset(); + const ImageSentinels sentinels{{10,11}, {20,21}, {30,31}, {40,41}}; + const ImageRaster raster{4, 2, {100,101,200,201,300,301,400,401}}; + const std::vector expected{20,21,10,11,300,301,100,101,30,31, + 400,401,200,201,20,21,40,41,20,21}; + std::string error; + std::vector output{999}; + check(assemble_image_rows(fixture(1).layout, raster, sentinels, 2, output, error), "assembly rejected"); + check(output == expected, "sentinel identity or raster permutation applied incorrectly"); + auto bad_assembly = [&](ImageLayout layout, ImageRaster values, ImageSentinels marks) { + output = {999}; + check(!assemble_image_rows(layout, values, marks, 2, output, error), "malformed assembly accepted"); + check(output == std::vector{999} && !error.empty(), "assembly failure changed output"); + }; + auto layout = fixture(1).layout; + layout.permutation = {2,0,2,1}; bad_assembly(layout,raster,sentinels); + layout = fixture(1).layout; layout.permutation[0] = -1; bad_assembly(layout,raster,sentinels); + layout = fixture(1).layout; layout.permutation[0] = 4; bad_assembly(layout,raster,sentinels); + layout = fixture(1).layout; layout.types[4] = static_cast(99); bad_assembly(layout,raster,sentinels); + layout = fixture(1).layout; layout.types[1] = ImageTokenType::End; bad_assembly(layout,raster,sentinels); + layout = fixture(1).layout; layout.span.visible_end--; bad_assembly(layout,raster,sentinels); + auto values = raster; values.columns = 3; bad_assembly(fixture(1).layout,values,sentinels); + values = raster; values.values.pop_back(); bad_assembly(fixture(1).layout,values,sentinels); + values = raster; values.values[0] = std::numeric_limits::quiet_NaN(); bad_assembly(fixture(1).layout,values,sentinels); + auto marks = sentinels; marks.pad[0] = std::numeric_limits::infinity(); bad_assembly(fixture(1).layout,raster,marks); + marks = sentinels; marks.end.pop_back(); bad_assembly(fixture(1).layout,raster,marks); + + std::vector images{fixture(1),fixture(12)}; + const ImageRows old{{777},{888}}; + ImageRows result = old; + int calls = 0; + ImageEncode fail_second = [&](const PromptImage &, ImageRaster & out, std::string & reason) { + if (++calls == 2) { reason = "second image failed"; return false; } + out = raster; return true; + }; + check(!materialize_image_rows(images,sentinels,2,fail_second,{},result,error) && + calls == 2 && result == old && error == "second image failed", "second-image failure is not atomic"); + calls = 0; + bool stop = false; + ImageEncode cancel_after_first = [&](const PromptImage &, ImageRaster & out, std::string &) { + ++calls; out = raster; stop = true; return true; + }; + check(!materialize_image_rows(images,sentinels,2,cancel_after_first,[&] { return stop; },result,error) && + calls == 1 && result == old && !error.empty(), "cancellation launched a remaining encode"); + calls = 0; + check(!materialize_image_rows(images,sentinels,2,cancel_after_first,[] { return true; },result,error) && + calls == 0 && result == old, "pre-cancelled materialization launched encode"); + ImageEncode good = [&](const PromptImage &, ImageRaster & out, std::string &) { + out = raster; + for (float & v : out.values) v += 1000.0f * calls; + ++calls; return true; + }; + check(materialize_image_rows(images,sentinels,2,good,{},result,error) && calls == 2, + "valid materialization failed"); + check(result[0] == expected && result[1][4] == 1300 && result[1][0] == 20, + "equal-layout images lost distinct raster values"); + calls = 0; + auto wrong_plan = images; wrong_plan[1].input.plan.aligner_cols = 3; + ImageRows retained = result; + check(!materialize_image_rows(wrong_plan,sentinels,2,good,{},result,error) && calls == 0 && + result == retained, "bad second-image shape was not rejected before encoding"); + check(!materialize_image_rows(images,sentinels,2, + [](const PromptImage &, ImageRaster &, std::string &) -> bool { throw 1; },{},result,error) && + result == retained, "callback exception changed output"); + + PreparedImagePrompt prompt; + prompt.images = images; + prompt.tokens.assign(23, 7); + for (const auto & image : images) { + for (size_t r = 0; r < image.layout.types.size(); ++r) + prompt.tokens[size_t(image.layout.span.block_begin) + r] = 100 + int32_t(image.layout.types[r]); + } + size_t text_calls = 0, text_tokens = 0; + TextEmbed embed = [&](const int32_t * ids, size_t count, float * out) { + ++text_calls; text_tokens += count; + for (size_t i = 0; i < count; ++i) { + check(ids[i] >= 0 && ids[i] < 100, "external ID reached ordinary embedder"); + out[2*i] = float(ids[i]); out[2*i+1] = float(ids[i]+1); + } + return true; + }; + check(embed_image_prompt_chunk(prompt,result,100,2,0,23,embed,output,error), "mixed prompt failed"); + check(text_calls == 3 && text_tokens == 3 && output.size() == 46 && + output[0] == 7 && output[6] == 300 && output[28] == 1300 && output[44] == 7, + "mixed embedding order or offsets are wrong"); + check(embed_image_prompt_chunk(prompt,result,100,2,1,10,{},output,error) && output == expected, + "exact image block slice failed"); + auto reject_chunk = [&](const PreparedImagePrompt & p, size_t start, size_t count) { + output = {999}; text_calls = 0; + check(!embed_image_prompt_chunk(p,result,100,2,start,count,embed,output,error) && + output == std::vector{999} && text_calls == 0 && !error.empty(), + "invalid chunk reached embedder or changed output"); + }; + reject_chunk(prompt,2,9); reject_chunk(prompt,0,5); + auto invalid = prompt; invalid.tokens[0] = 100; reject_chunk(invalid,0,23); + invalid = prompt; invalid.tokens[3] = 100; reject_chunk(invalid,0,23); + invalid = prompt; invalid.tokens[22] = -1; reject_chunk(invalid,0,23); + output = {999}; + check(!embed_image_prompt_chunk(prompt,result,100,2,0,23, + [](const int32_t *,size_t,float *) { return false; },output,error) && + output == std::vector{999}, "text callback failure changed output"); + std::cout << "PASS image assembly identities/permutation, atomic materialization/cancellation, mixed embedding boundaries\n"; + return 0; + } catch (const std::exception & e) { + std::cerr << "FAIL: " << e.what() << '\n'; + return 1; + } +} diff --git a/server/tools/ds4v_image_integration/CMakeLists.txt b/server/tools/ds4v_image_integration/CMakeLists.txt new file mode 100644 index 000000000..dc3b65f76 --- /dev/null +++ b/server/tools/ds4v_image_integration/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.21) +project(ds4v_image_integration LANGUAGES CXX) + +add_executable(ds4v_image_integration_test test.cpp) +target_include_directories(ds4v_image_integration_test PRIVATE ../../src/deepseek4) +target_compile_features(ds4v_image_integration_test PRIVATE cxx_std_17) +if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") + target_compile_options(ds4v_image_integration_test PRIVATE -Wall -Wextra -Wpedantic -Werror) +endif() +enable_testing() +add_test(NAME ds4v_image_integration COMMAND ds4v_image_integration_test) diff --git a/server/tools/ds4v_image_integration/test.cpp b/server/tools/ds4v_image_integration/test.cpp new file mode 100644 index 000000000..cfab3842a --- /dev/null +++ b/server/tools/ds4v_image_integration/test.cpp @@ -0,0 +1,228 @@ +#include "deepseek4_image_budget.h" +#include "deepseek4_image_spans.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace { +using dflash::vision::ImageSpanView; +using dflash::vision::TokenSpan; +using dflash::vision::atomic_image_chunk; +using dflash::vision::image_block_at; +using dflash::vision::remaining_expert_budget; +using dflash::vision::valid_image_spans; +constexpr uint64_t MAX = std::numeric_limits::max(); +size_t checks = 0; + +void require(bool condition, const std::string & description) { + ++checks; + if (!condition) throw std::runtime_error(description); +} + +ImageSpanView view(const std::vector & spans) { + return {spans.data(), spans.size()}; +} + +bool bisects(const std::vector & spans, uint64_t point) { + for (const auto & span : spans) { + if (span.block_begin < point && point < span.block_end) return true; + } + return false; +} + +// Enumerate every legal endpoint, independently of the helper's interval walk. +// A small preferred chunk may expand only when no positive legal endpoint fits. +int oracle(const std::vector & spans, uint64_t position, + int preferred, uint64_t remaining, int capacity) { + if (preferred <= 0 || capacity <= 0 || uint64_t(preferred) > remaining || + remaining > MAX - position || bisects(spans, position)) return 0; + int below = 0; + int above = 0; + for (int length = 1; length <= capacity && uint64_t(length) <= remaining; ++length) { + if (bisects(spans, position + uint64_t(length))) continue; + if (length <= preferred) below = length; + else if (!above) above = length; + } + return below ? below : above; +} + +void check_oracle(const std::vector & spans, uint64_t position, + int preferred, uint64_t remaining, int capacity) { + const int expected = oracle(spans, position, preferred, remaining, capacity); + const int actual = atomic_image_chunk(view(spans), position, preferred, remaining, capacity); + require(actual == expected, "endpoint oracle mismatch at position=" + std::to_string(position) + + " preferred=" + std::to_string(preferred) + " capacity=" + std::to_string(capacity)); + if (actual) { + require(actual > 0 && actual <= capacity && uint64_t(actual) <= remaining, + "chunk must make bounded positive progress"); + require(!bisects(spans, position + uint64_t(actual)), "chunk endpoint splits an image block"); + } +} + +void validation_and_lookup() { + require(valid_image_spans({}, 0), "empty prompt/view is valid"); + require(!image_block_at({}, 0), "empty lookup is null"); + require(!valid_image_spans({nullptr, 1}, 100), "nonempty null view rejected"); + std::array too_many{}; + require(!valid_image_spans({too_many.data(), too_many.size()}, 100), "more than four images rejected"); + const std::vector spans{{10, 13, 18, 20}, {20, 20, 25, 25}, {30, 31, 33, 35}}; + require(valid_image_spans(view(spans), 35), "adjacent and separated blocks valid"); + require(!image_block_at(view(spans), 9), "text before block excluded"); + require(image_block_at(view(spans), 10) == &spans[0], "leading padding belongs to image block"); + require(image_block_at(view(spans), 12) == &spans[0], "leading padding is not ordinary text"); + require(image_block_at(view(spans), 13) == &spans[0], "visible begin included"); + require(image_block_at(view(spans), 18) == &spans[0], "trailing padding belongs to block"); + require(image_block_at(view(spans), 20) == &spans[1], "adjacent boundary belongs to next block"); + require(!image_block_at(view(spans), 25), "half-open end excluded"); + require(!image_block_at(view(spans), 29), "gap excluded"); + require(!image_block_at(view(spans), 35), "final end excluded"); + for (const auto & invalid : std::vector>{ + {{9, 8, 10, 11}}, {{9, 9, 9, 11}}, {{9, 9, 12, 11}}, + {{9, 9, 10, 36}}, {{0, 0, 1, 385}}, {{5, 5, 7, 9}, {8, 8, 10, 12}}, + {{10, 10, 11, 12}, {1, 1, 2, 3}}, {{MAX, 0, 1, 2}}}) { + require(!valid_image_spans(view(invalid), 35), "malformed/overlapping/out-of-range span rejected"); + } + const std::vector largest{{0, 3, 380, 384}}; + require(valid_image_spans(view(largest), 384), "384-token block admitted"); + const std::vector overlong{{0, 3, 380, 385}}; + require(!valid_image_spans(view(overlong), 385), "385-token block rejected even inside prompt"); + const std::vector upper{{MAX - 384, MAX - 381, MAX - 1, MAX}}; + require(valid_image_spans(view(upper), MAX), "valid upper-limit span does not overflow"); + require(atomic_image_chunk(view(upper), MAX - 384, 1, 384, 1024) == 384, + "upper-limit atomic extension reaches exact end"); + require(atomic_image_chunk({}, MAX - 3, 1, 4, 1024) == 0, "position plus remaining overflow rejected"); + require(atomic_image_chunk({}, 0, 0, 10, 1024) == 0, "zero proposed chunk rejected"); + require(atomic_image_chunk({}, 0, -1, 10, 1024) == 0, "negative proposed chunk rejected"); + require(atomic_image_chunk({}, 0, 1, 10, 0) == 0, "zero capacity rejected"); + require(atomic_image_chunk({}, 0, 1, 10, -1) == 0, "negative capacity rejected"); + require(atomic_image_chunk({}, 0, 11, 10, 1024) == 0, "caller must clamp preferred chunk to remaining"); +} + +void production_boundary_matrix() { + const std::vector spans{ + {100, 103, 479, 484}, {484, 484, 500, 500}, + {32640, 32644, 33020, 33024}, {33024, 33024, 33031, 33032}}; + constexpr uint64_t prompt = 35000; + require(valid_image_spans(view(spans), prompt), "four-image fixture valid"); + for (int preferred : {1, 128, 512, 1024, 4096}) { + for (uint64_t position : {uint64_t(0), uint64_t(99), uint64_t(100), uint64_t(101), + uint64_t(483), uint64_t(484), uint64_t(499), uint64_t(500), uint64_t(32639), + uint64_t(32640), uint64_t(32768), uint64_t(33023), uint64_t(33024), uint64_t(33032)}) { + check_oracle(spans, position, preferred, prompt - position, 1024); + } + uint64_t position = 0; + size_t steps = 0; + while (position < prompt) { + const int proposed = int(std::min(uint64_t(preferred), prompt - position)); + const int count = atomic_image_chunk(view(spans), position, proposed, prompt - position, 1024); + require(count > 0, "complete traversal must not stall when every block fits capacity"); + require(!bisects(spans, position + uint64_t(count)), "complete traversal preserves atomic blocks"); + require(count <= 1024, "preferred 4096 cannot exceed independent hard capacity 1024"); + position += uint64_t(count); + require(++steps <= prompt, "traversal terminates"); + } + require(position == prompt, "complete traversal consumes exact prompt"); + } + require(atomic_image_chunk(view(spans), 32640, 128, prompt - 32640, 1024) == 384, + "image crossing 32768 extends past context boundary atomically"); + require(atomic_image_chunk(view(spans), 100, 1, prompt - 100, 383) == 0, + "capacity smaller than complete block rejected"); + require(atomic_image_chunk(view(spans), 100, 1, 383, 1024) == 0, + "remaining prompt smaller than complete block rejected"); + require(atomic_image_chunk(view(spans), 99, 128, prompt - 99, 128) == 1, + "ordinary prefix remains consumable before oversized image"); + for (int proposed : {100, 484, 500, 1024}) check_oracle(spans, 0, proposed, prompt, 1024); +} + +void exhaustive_endpoints() { + constexpr uint64_t prompt = 14; + // All ordered pairs of blocks, including adjacency, varying image lengths, + // all positions, and capacities that can both fit and reject a whole image. + for (uint64_t a = 0; a < prompt; ++a) { + for (uint64_t b = a + 1; b <= std::min(prompt, a + 4); ++b) { + for (uint64_t c = b; c < prompt; ++c) { + for (uint64_t d = c + 1; d <= std::min(prompt, c + 4); ++d) { + const std::vector spans{{a, a, b, b}, {c, c, d, d}}; + require(valid_image_spans(view(spans), prompt), "generated spans valid"); + for (uint64_t position = 0; position <= prompt; ++position) { + for (int capacity : {1, 2, 3, 4, 7, 16}) { + for (int preferred : {1, 2, 4, 8, 16}) { + const uint64_t remaining = prompt - position; + check_oracle(spans, position, preferred, remaining, capacity); + } + } + } + } + } + } + } +} + +void memory_budget() { + constexpr uint64_t MiB = 1024 * 1024; + constexpr uint64_t GiB = 1024 * MiB; + constexpr uint64_t vision = dflash::vision::SCRATCH_RESERVATION; + constexpr uint64_t workspace = 76 * MiB; + require(vision == 2 * GiB, "retain qualified two-GiB vision reservation"); + const uint64_t initial = remaining_expert_budget(24 * GiB, 4 * GiB, GiB, 256 * MiB, 512 * MiB, vision); + require(initial == 16640 * MiB, "combined fixed charges leave expected expert budget"); + // A free-memory snapshot turns every completed allocation into core usage. + // Preloading the projector must be charged exactly once in either order. + constexpr uint64_t projector = 700 * MiB; + const uint64_t before_projector = remaining_expert_budget( + 24 * GiB, 4 * GiB, GiB, 256 * MiB, 512 * MiB, vision + projector); + const uint64_t after_projector = remaining_expert_budget( + 24 * GiB, 4 * GiB + projector, GiB, 256 * MiB, 512 * MiB, vision); + require(before_projector == after_projector, "preloaded projector charged once regardless of load order"); + const uint64_t after_workspace = remaining_expert_budget( + 24 * GiB, 4 * GiB + workspace, GiB, 256 * MiB, 512 * MiB, vision, workspace); + require(initial == after_workspace, "resident workspace offsets only its included reservation"); + require(remaining_expert_budget(24 * GiB, 4 * GiB + workspace, GiB, 256 * MiB, + 512 * MiB, vision) == initial - workspace, + "unproven workspace residency cannot receive a credit"); + require(remaining_expert_budget(MAX, 0, 0, 0, 0, vision, vision + 1) == 0, + "workspace credit cannot exceed reservation"); + require(remaining_expert_budget(100, 20, 20, 20, 20, 20) == 0, "exact exhaustion returns zero"); + require(remaining_expert_budget(99, 20, 20, 20, 20, 20) == 0, "one-byte shortage fails closed"); + require(remaining_expert_budget(MAX, MAX, 1, 0, 0, 0) == 0, "overflowing sum cannot wrap into budget"); + require(remaining_expert_budget(MAX, 1, MAX, 0, 0, 0) == 0, "late exhaustion cannot wrap"); + require(remaining_expert_budget(MAX, 0, 0, 0, 0, 0) == MAX, "uncharged maximum preserved"); + require(remaining_expert_budget(MAX, MAX - 7, 2, 1, 1, 2) == 1, "near-maximum arithmetic remains exact"); + // Independent conservation law for uniform expert rounds: tighter primary + // headroom must transfer the same quantized storage amount to the cold owner. + constexpr uint64_t round = 64 * MiB; + constexpr uint64_t total_experts = 32 * GiB; + uint64_t last_hot = initial / round * round; + uint64_t last_cold = total_experts - last_hot; + for (uint64_t extra = 0; extra <= 4 * GiB; extra += 17 * MiB) { + const uint64_t budget = remaining_expert_budget(24 * GiB, 4 * GiB, GiB, + 256 * MiB, 512 * MiB, vision + extra); + const uint64_t hot = budget / round * round; + const uint64_t cold = total_experts - hot; + require(hot <= last_hot && cold >= last_cold, "reduced hot budget cannot reduce cold demand"); + require(hot + cold == total_experts && budget - hot < round, "placement conserves expert bytes"); + require(last_hot - hot == cold - last_cold, "lost hot bytes equal additional cold bytes"); + last_hot = hot; + last_cold = cold; + } +} +} // namespace + +int main() { + try { + validation_and_lookup(); + production_boundary_matrix(); + exhaustive_endpoints(); + memory_budget(); + std::cout << "PASS: DS4 image integration invariants checks=" << checks << '\n'; + return EXIT_SUCCESS; + } catch (const std::exception & error) { + std::cerr << "FAIL: " << error.what() << '\n'; + return EXIT_FAILURE; + } +} diff --git a/server/tools/ds4v_preprocess_probe/CMakeLists.txt b/server/tools/ds4v_preprocess_probe/CMakeLists.txt index 97082afcf..0f326e3e4 100644 --- a/server/tools/ds4v_preprocess_probe/CMakeLists.txt +++ b/server/tools/ds4v_preprocess_probe/CMakeLists.txt @@ -13,40 +13,7 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") endif() if(DS4V_PREPROCESS_WITH_CODECS) - include(ExternalProject) - include(FetchContent) - - set(DS4V_JPEG_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/libjpeg-turbo-prefix) - file(MAKE_DIRECTORY ${DS4V_JPEG_PREFIX}/include) - ExternalProject_Add(libjpeg_turbo_external - URL https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.1.4.1/libjpeg-turbo-3.1.4.1.tar.gz - URL_HASH SHA256=ecae8008e2cc9ade2f2c1bb9d5e6d4fb73e7c433866a056bd82980741571a022 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE - CMAKE_ARGS - -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_PREFIX=${DS4V_JPEG_PREFIX} - -DCMAKE_INSTALL_LIBDIR=lib - -DENABLE_SHARED=OFF - -DENABLE_STATIC=ON - -DWITH_TOOLS=OFF - -DWITH_TESTS=OFF - -DWITH_SIMD=OFF - -DWITH_TURBOJPEG=OFF - BUILD_COMMAND ${CMAKE_COMMAND} --build --parallel 2 - BUILD_BYPRODUCTS ${DS4V_JPEG_PREFIX}/lib/libjpeg.a) - add_library(ds4v_libjpeg STATIC IMPORTED GLOBAL) - set_target_properties(ds4v_libjpeg PROPERTIES - IMPORTED_LOCATION ${DS4V_JPEG_PREFIX}/lib/libjpeg.a - INTERFACE_INCLUDE_DIRECTORIES ${DS4V_JPEG_PREFIX}/include) - add_dependencies(ds4v_libjpeg libjpeg_turbo_external) - - FetchContent_Declare(lodepng - URL https://github.com/lvandeve/lodepng/archive/ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a.tar.gz - URL_HASH SHA256=c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE) - FetchContent_MakeAvailable(lodepng) - add_library(ds4v_lodepng STATIC ${lodepng_SOURCE_DIR}/lodepng.cpp) - target_include_directories(ds4v_lodepng PUBLIC ${lodepng_SOURCE_DIR}) + include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/Ds4vImageCodecs.cmake") target_sources(ds4v_preprocess_probe PRIVATE ../../src/deepseek4/deepseek4_vision_decode.cpp) From 4f388d1a78d4e5fc7db242a55a62346fdcb857e9 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 07:40:27 -0400 Subject: [PATCH 067/123] Fix DS4V startup accounting for reclaimable HIP memory pools --- server/src/deepseek4/deepseek4_backend.cpp | 6 + .../deepseek4/deepseek4_image_admission.cpp | 102 ++++++++++++++++- .../src/deepseek4/deepseek4_image_admission.h | 20 ++++ server/tests/test_deepseek4_unit.cpp | 105 ++++++++++++++++++ 4 files changed, 229 insertions(+), 4 deletions(-) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index 0d97cb634..a9bf4ecf5 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -1835,6 +1835,12 @@ bool DeepSeek4Backend::init_hybrid_model() { gib(report.host_required_bytes), gib(report.host_available_bytes), gib(report.cold_activation_estimate_bytes), gib(report.cold_runtime_reservation_bytes), admitted ? "admitted" : err.c_str()); + std::fprintf(stderr, + "[deepseek4] image startup host capacity: raw=%.3f GiB gpu_reclaim_credit=%.3f GiB " + "effective=%.3f GiB policy=%s (capacity estimate, not a zero-swap guarantee)\n", + gib(report.host_available_bytes - report.host_gpu_reclaim_credit_bytes), + gib(report.host_gpu_reclaim_credit_bytes), gib(report.host_available_bytes), + report.host_capacity_policy.c_str()); if (!admitted) return fail_hybrid_init(); image_reserves_ = reserves; #else diff --git a/server/src/deepseek4/deepseek4_image_admission.cpp b/server/src/deepseek4/deepseek4_image_admission.cpp index d1daf4ebb..9d31b3f3a 100644 --- a/server/src/deepseek4/deepseek4_image_admission.cpp +++ b/server/src/deepseek4/deepseek4_image_admission.cpp @@ -4,6 +4,9 @@ #include "common/moe_hybrid_placement.h" #include "common/moe_hybrid_types.h" #include "ggml-backend.h" +#if defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP) +#include "ggml-cuda.h" +#endif #include #include @@ -12,6 +15,9 @@ #include #include #include +#if defined(__linux__) +#include +#endif namespace dflash::vision { namespace { @@ -140,6 +146,74 @@ bool device_free(ggml_backend_t backend, uint64_t & available, std::string & err } } // namespace +bool prepare_deepseek4_image_startup_snapshot( + const std::string & meminfo, const std::string & kernel_release, + const ImageAdmissionReserves & reserves, ImageMemorySnapshot & snapshot, + std::string & error) { + error.clear(); + const bool qualified = kernel_release == "7.1.3-070103-generic"; + // Read the entire stream: GPU counters can occur past the backend helper's + // historical 2 KiB buffer. Reject signs, duplicate keys, units and overflow. + const std::array names{ + "MemAvailable:", "MemTotal:", "GPUActive:", "GPUReclaim:", "HugePages_Total:"}; + std::array values{}; + std::array found{}; + std::istringstream input(meminfo); + std::string line; + while (std::getline(input, line)) { + std::istringstream fields(line); + std::string key; + fields >> key; + for (size_t i = 0; i < names.size(); ++i) { + if (key != names[i] || (i && !qualified)) continue; + std::string digits, unit, extra; + if (found[i] || !(fields >> digits) || digits.empty()) { + return fail(error, "duplicate or invalid startup memory field: " + key); + } + uint64_t value = 0; + for (char digit : digits) { + if (digit < '0' || digit > '9' || !mul(value, 10, value) || + !add(value, uint64_t(digit - '0'))) { + return fail(error, "invalid or overflowing startup memory field: " + key); + } + } + if (i != 4 && (!(fields >> unit) || unit != "kB" || !mul(value, 1024, value))) { + return fail(error, "invalid startup memory units or overflow: " + key); + } + if (fields >> extra) return fail(error, "extra startup memory field data: " + key); + values[i] = value; + found[i] = true; + } + } + if (!found[0]) return fail(error, "startup MemAvailable is missing"); + ImageMemorySnapshot next = snapshot; + next.host_available_bytes = values[0]; + next.host_gpu_reclaim_credit_bytes = 0; + next.host_capacity_policy = "raw-unqualified-kernel"; + if (qualified) { + uint64_t accounted = values[0]; + if ((found[2] && !add(accounted, values[2])) || + (found[3] && !add(accounted, values[3])) || + (found[1] && (!values[1] || accounted > values[1]))) { + return fail(error, "startup host/GPU memory counters exceed physical capacity"); + } + next.host_capacity_policy = "raw-missing-gpu-pool-fields"; + if (std::all_of(found.begin(), found.end(), [](bool value) { return value; })) { + next.host_capacity_policy = "raw-reserved-huge-pages"; + if (!values[4]) { + const uint64_t capacity = values[0] + values[3]; // checked in accounted above + next.host_available_bytes = capacity; + next.host_gpu_reclaim_credit_bytes = values[3]; + next.host_capacity_policy = "startup-linux-7.1.3-gpu-reclaim"; + if (reserves.primary_domain == ImageMemoryDomain::HostShared) next.primary_free_bytes = capacity; + if (reserves.cold_domain == ImageMemoryDomain::HostShared) next.cold_free_bytes = capacity; + } + } + } + snapshot = next; + return true; +} + bool estimate_deepseek4_image_storage( const common::DeepSeek4Weights & w, const common::MoeHybridPlacement & placement, const common::MoeHybridConfig & config, ggml_backend_t primary, ggml_backend_t cold, @@ -272,12 +346,30 @@ bool check_deepseek4_image_admission( reserves.cold_domain == ImageMemoryDomain::Unknown) { return fail(error, "actual owner host-memory sharing must be classified before admission"); } - if (!device_free(primary, out.primary_free_bytes, error) || - !device_free(cold, out.cold_free_bytes, error) || - !host_available(out.host_available_bytes, error)) return false; + ImageMemorySnapshot snapshot; + if (!device_free(primary, snapshot.primary_free_bytes, error) || + !device_free(cold, snapshot.cold_free_bytes, error)) return false; +#if defined(__linux__) + std::ifstream input("/proc/meminfo"); + if (!input) return fail(error, "cannot read startup host memory"); + std::ostringstream contents; + contents << input.rdbuf(); + if (!contents || input.bad()) return fail(error, "cannot finish reading startup host memory"); + std::string kernel_release; +#if defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP) + struct utsname kernel{}; + // Only real HIP owners use the qualified kernel pool accounting. Other + // backends retain their own free-memory limits, including exhausted ones. + if (ggml_backend_is_cuda(primary) && ggml_backend_is_cuda(cold) && + uname(&kernel) == 0) kernel_release = kernel.release; +#endif + if (!prepare_deepseek4_image_startup_snapshot(contents.str(), kernel_release, + reserves, snapshot, error)) return false; +#else + if (!host_available(snapshot.host_available_bytes, error)) return false; +#endif const ImageStorageEstimate storage = out.storage; const uint64_t activation_bytes = out.cold_activation_estimate_bytes; - const ImageMemorySnapshot snapshot{out.primary_free_bytes, out.cold_free_bytes, out.host_available_bytes}; return assess_deepseek4_image_admission(storage, activation_bytes, reserves, snapshot, out, error); } @@ -329,6 +421,8 @@ bool assess_deepseek4_image_admission( out.primary_free_bytes = snapshot.primary_free_bytes; out.cold_free_bytes = snapshot.cold_free_bytes; out.host_available_bytes = snapshot.host_available_bytes; + out.host_gpu_reclaim_credit_bytes = snapshot.host_gpu_reclaim_credit_bytes; + out.host_capacity_policy = snapshot.host_capacity_policy; const auto known_domain = [](ImageMemoryDomain domain) { return domain == ImageMemoryDomain::Dedicated || domain == ImageMemoryDomain::HostShared; }; diff --git a/server/src/deepseek4/deepseek4_image_admission.h b/server/src/deepseek4/deepseek4_image_admission.h index f6eeb5aca..93c4c8474 100644 --- a/server/src/deepseek4/deepseek4_image_admission.h +++ b/server/src/deepseek4/deepseek4_image_admission.h @@ -62,6 +62,8 @@ struct ImageAdmissionReport { uint64_t primary_free_bytes = 0; uint64_t cold_free_bytes = 0; uint64_t host_available_bytes = 0; + uint64_t host_gpu_reclaim_credit_bytes = 0; + std::string host_capacity_policy = "raw"; uint64_t primary_required_bytes = 0; uint64_t cold_required_bytes = 0; uint64_t host_required_bytes = 0; @@ -75,8 +77,26 @@ struct ImageMemorySnapshot { uint64_t primary_free_bytes = 0; uint64_t cold_free_bytes = 0; uint64_t host_available_bytes = 0; + uint64_t host_gpu_reclaim_credit_bytes = 0; + std::string host_capacity_policy = "raw"; }; +// Pure startup-only snapshot transform. The live caller supplies one complete +// /proc/meminfo read and a Linux/HIP kernel release. Credit is qualified only for +// 7.1.3-070103-generic: upstream v7.1.3 mm/show_mem.c si_mem_available excludes +// NR_GPU_RECLAIM, while Documentation/filesystems/proc.rst defines reclaimable +// GPU pools separately from GPUActive. Kernel build provenance must be retained +// during qualification; neither newer versions nor field presence imply support. +// Unknown kernels/missing optional fields retain raw accounting. Malformed +// provided fields on a qualified kernel fail closed. No reserved huge pages are +// supported for credit. This is capacity accounting, not a zero-swap guarantee. +// Shared owners use the same capacity as the combined host gate, never separate +// additive credits. Runtime and preparation deliberately do not call this helper. +bool prepare_deepseek4_image_startup_snapshot( + const std::string & meminfo, const std::string & kernel_release, + const ImageAdmissionReserves & reserves, ImageMemorySnapshot & snapshot, + std::string & error); + // Pure assessment shared by live admission and deterministic CPU tests. All // figures describe allocations still to come relative to this one snapshot. bool assess_deepseek4_image_admission( diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index d53942f2f..6603b2202 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -1248,6 +1248,7 @@ struct ImageAdmissionFakeOwner { ggml_backend_buffer_type buft{}; ggml_backend_device device{}; ggml_backend backend{}; + ggml_guid guid{}; size_t alignment = 128; size_t padding = 64; size_t maximum = SIZE_MAX; @@ -1288,6 +1289,7 @@ struct ImageAdmissionFakeOwner { *free = owner.free_bytes; *total = owner.total_bytes; }; + backend.guid = &guid; backend.device = &device; backend.context = this; backend.iface.graph_compute = [](ggml_backend_t b, ggml_cgraph *) { @@ -1435,6 +1437,7 @@ static void test_image_storage_admission_metadata() { &hot.backend, &cold.backend, reserves, report, error)); TEST_ASSERT(report.storage_estimated && !report.known_charges_fit); TEST_ASSERT(report.cold_free_bytes == 0); + TEST_ASSERT(report.host_gpu_reclaim_credit_bytes == 0); TEST_ASSERT(!check_deepseek4_image_runtime_admission(config, &hot.backend, &cold.backend, reserves, report, error)); TEST_ASSERT(report.storage.hot_allocation_bytes == 0 && report.storage.cold_allocation_bytes == 0); @@ -1558,6 +1561,107 @@ static void test_image_admission_resource_snapshots() { std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); } +static void test_image_startup_reclaim_accounting() { + std::fprintf(stderr, "test_image_startup_reclaim_accounting...\n"); + using namespace dflash::vision; + constexpr uint64_t kib = 1024; + constexpr uint64_t gib = 1024 * 1024 * kib; + const std::string kernel = "7.1.3-070103-generic"; + const auto meminfo = [](uint64_t available, uint64_t reclaim) { + return "MemAvailable: " + std::to_string(available) + " kB\nMemTotal: 130023424 kB\n" + "GPUActive: 1024 kB\nGPUReclaim: " + std::to_string(reclaim) + + " kB\nHugePages_Total: 0\n"; + }; + ImageAdmissionReserves reserves; + reserves.primary_domain = ImageMemoryDomain::Dedicated; + reserves.cold_domain = ImageMemoryDomain::HostShared; + reserves.host_request_bytes = 4 * gib; + reserves.host_loader_overhead_bytes = gib; + reserves.cold_runtime_reservation_bytes = 2 * gib; + ImageStorageEstimate storage; + storage.cold_allocation_bytes = 89163 * gib / 1000; + ImageMemorySnapshot raw{8 * gib, 73470 * gib / 1000, 73470 * gib / 1000}; + ImageMemorySnapshot startup = raw; + ImageAdmissionReport report; + std::string error; + const auto fields = meminfo(raw.host_available_bytes / kib, (49200 * gib / 1000) / kib); + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields, kernel, reserves, startup, error)); + TEST_ASSERT(startup.primary_free_bytes == raw.primary_free_bytes); + TEST_ASSERT(startup.cold_free_bytes == startup.host_available_bytes); + TEST_ASSERT(startup.host_available_bytes == raw.host_available_bytes / kib * kib + + (49200 * gib / 1000) / kib * kib); + TEST_ASSERT(assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); + TEST_ASSERT(report.host_required_bytes == storage.cold_allocation_bytes + 6 * gib); + TEST_ASSERT(report.host_gpu_reclaim_credit_bytes == startup.host_gpu_reclaim_credit_bytes); + TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, raw, report, error)); + for (const std::string release : {std::string("7.1.4"), std::string(""), std::string("7.1.3-other")}) { + ImageMemorySnapshot other = raw; + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields, release, reserves, other, error)); + TEST_ASSERT(other.host_gpu_reclaim_credit_bytes == 0 && other.cold_free_bytes == raw.cold_free_bytes); + TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, other, report, error)); + } + // Even with qualified credit, dedicated physical limits remain binding. + storage.hot_allocation_bytes = raw.primary_free_bytes + 1; + TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); + storage.hot_allocation_bytes = 0; + // Two UMA owners spend a single physical pool. Exact combined fit succeeds; + // adding one byte fails although both individual owner limits still fit. + reserves.primary_domain = ImageMemoryDomain::HostShared; + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields, kernel, reserves, startup, error)); + storage.hot_allocation_bytes = startup.host_available_bytes - storage.cold_allocation_bytes - 6 * gib; + TEST_ASSERT(assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); + ++storage.hot_allocation_bytes; + TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); + TEST_ASSERT(report.primary_required_bytes < startup.primary_free_bytes && + report.cold_required_bytes < startup.cold_free_bytes); + // An ordinary runtime snapshot has no credit; fresh graphs cannot inherit + // a startup pool balance that may have been consumed by resident weights. + ImageMemorySnapshot runtime{gib, gib, gib}; + TEST_ASSERT(!assess_deepseek4_image_admission({}, gib, reserves, runtime, report, error)); + TEST_ASSERT(report.host_gpu_reclaim_credit_bytes == 0 && report.host_capacity_policy == "raw"); + for (const std::string optional : {std::string(""), std::string("GPUReclaim: 8 kB\n")}) { + ImageMemorySnapshot missing = raw; + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot("MemAvailable: 32 kB\n" + optional, + kernel, reserves, missing, error)); + TEST_ASSERT(missing.host_available_bytes == 32 * kib && missing.host_gpu_reclaim_credit_bytes == 0); + TEST_ASSERT(missing.primary_free_bytes == raw.primary_free_bytes); + } + ImageMemorySnapshot huge = raw; + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields + "Unrelated: 1\n", kernel, reserves, huge, error)); + std::string huge_fields = fields; + huge_fields.replace(huge_fields.find("HugePages_Total: 0"), 18, "HugePages_Total: 1"); + huge = raw; + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(huge_fields, kernel, reserves, huge, error)); + TEST_ASSERT(huge.host_gpu_reclaim_credit_bytes == 0 && huge.cold_free_bytes == raw.cold_free_bytes); + const std::string prefix = "MemAvailable: 32 kB\nMemTotal: 128 kB\nHugePages_Total: 0\n"; + for (const std::string counters : { + "GPUActive: 1 kB\nGPUReclaim: -1 kB\n", "GPUActive: 1 kB\nGPUReclaim: +1 kB\n", + "GPUActive: 1 kB\nGPUReclaim: 1 MB\n", "GPUActive: 1 kB\nGPUReclaim: 1 kB extra\n", + "GPUActive: 1 kB\nGPUReclaim: 1 kB\nGPUReclaim: 1 kB\n", + "GPUActive: 97 kB\nGPUReclaim: 0 kB\n", "GPUActive: 0 kB\nGPUReclaim: 97 kB\n", + "GPUActive: 0 kB\nGPUReclaim: 18446744073709551615 kB\n", + "GPUActive: 0 kB\nGPUReclaim: 18446744073709551616 kB\n"}) { + ImageMemorySnapshot invalid = raw; + TEST_ASSERT(!prepare_deepseek4_image_startup_snapshot(prefix + counters, kernel, reserves, invalid, error)); + TEST_ASSERT(invalid.host_available_bytes == raw.host_available_bytes && invalid.host_gpu_reclaim_credit_bytes == 0); + } + for (const std::string bad_raw : {"MemAvailable: -1 kB\n", "MemAvailable: 1 kB\nMemAvailable: 2 kB\n", + "MemAvailable: 1 MB\n", "GPUReclaim: 1 kB\n"}) { + ImageMemorySnapshot invalid = raw; + TEST_ASSERT(!prepare_deepseek4_image_startup_snapshot(bad_raw, "unknown", reserves, invalid, error)); + } + ImageMemorySnapshot active = raw; + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(prefix + "GPUActive: 80 kB\nGPUReclaim: 16 kB\n", + kernel, reserves, active, error)); + TEST_ASSERT(active.host_available_bytes == 48 * kib); // active pages never count as capacity + // GPU fields beyond 2 KiB must still be parsed, not silently truncated. + ImageMemorySnapshot long_input = raw; + TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(std::string(4096, ' ') + "\n" + fields, + kernel, reserves, long_input, error)); + TEST_ASSERT(long_input.host_gpu_reclaim_credit_bytes == startup.host_gpu_reclaim_credit_bytes); + std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); +} + static DeepSeek4LayerSplitAdapter make_test_adapter() { DeepSeek4LayerSplitAdapterConfig cfg; cfg.device.gpu = 0; @@ -4697,6 +4801,7 @@ int main() { test_image_batch_admission_before_execution(backend); test_image_storage_admission_metadata(); test_image_admission_resource_snapshots(); + test_image_startup_reclaim_accounting(); test_dspark_loader_contract_and_bounds(backend); test_dspark_confidence_uses_separate_hidden(backend); test_safe_compressor_batch_tokens(); From 14754e40c101f3803cc3728eca8dd0a98e0e0206 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 07:45:51 -0400 Subject: [PATCH 068/123] Revert "Fix DS4V startup accounting for reclaimable HIP memory pools" This reverts commit 4f388d1a78d4e5fc7db242a55a62346fdcb857e9. --- server/src/deepseek4/deepseek4_backend.cpp | 6 - .../deepseek4/deepseek4_image_admission.cpp | 102 +---------------- .../src/deepseek4/deepseek4_image_admission.h | 20 ---- server/tests/test_deepseek4_unit.cpp | 105 ------------------ 4 files changed, 4 insertions(+), 229 deletions(-) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index a9bf4ecf5..0d97cb634 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -1835,12 +1835,6 @@ bool DeepSeek4Backend::init_hybrid_model() { gib(report.host_required_bytes), gib(report.host_available_bytes), gib(report.cold_activation_estimate_bytes), gib(report.cold_runtime_reservation_bytes), admitted ? "admitted" : err.c_str()); - std::fprintf(stderr, - "[deepseek4] image startup host capacity: raw=%.3f GiB gpu_reclaim_credit=%.3f GiB " - "effective=%.3f GiB policy=%s (capacity estimate, not a zero-swap guarantee)\n", - gib(report.host_available_bytes - report.host_gpu_reclaim_credit_bytes), - gib(report.host_gpu_reclaim_credit_bytes), gib(report.host_available_bytes), - report.host_capacity_policy.c_str()); if (!admitted) return fail_hybrid_init(); image_reserves_ = reserves; #else diff --git a/server/src/deepseek4/deepseek4_image_admission.cpp b/server/src/deepseek4/deepseek4_image_admission.cpp index 9d31b3f3a..d1daf4ebb 100644 --- a/server/src/deepseek4/deepseek4_image_admission.cpp +++ b/server/src/deepseek4/deepseek4_image_admission.cpp @@ -4,9 +4,6 @@ #include "common/moe_hybrid_placement.h" #include "common/moe_hybrid_types.h" #include "ggml-backend.h" -#if defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP) -#include "ggml-cuda.h" -#endif #include #include @@ -15,9 +12,6 @@ #include #include #include -#if defined(__linux__) -#include -#endif namespace dflash::vision { namespace { @@ -146,74 +140,6 @@ bool device_free(ggml_backend_t backend, uint64_t & available, std::string & err } } // namespace -bool prepare_deepseek4_image_startup_snapshot( - const std::string & meminfo, const std::string & kernel_release, - const ImageAdmissionReserves & reserves, ImageMemorySnapshot & snapshot, - std::string & error) { - error.clear(); - const bool qualified = kernel_release == "7.1.3-070103-generic"; - // Read the entire stream: GPU counters can occur past the backend helper's - // historical 2 KiB buffer. Reject signs, duplicate keys, units and overflow. - const std::array names{ - "MemAvailable:", "MemTotal:", "GPUActive:", "GPUReclaim:", "HugePages_Total:"}; - std::array values{}; - std::array found{}; - std::istringstream input(meminfo); - std::string line; - while (std::getline(input, line)) { - std::istringstream fields(line); - std::string key; - fields >> key; - for (size_t i = 0; i < names.size(); ++i) { - if (key != names[i] || (i && !qualified)) continue; - std::string digits, unit, extra; - if (found[i] || !(fields >> digits) || digits.empty()) { - return fail(error, "duplicate or invalid startup memory field: " + key); - } - uint64_t value = 0; - for (char digit : digits) { - if (digit < '0' || digit > '9' || !mul(value, 10, value) || - !add(value, uint64_t(digit - '0'))) { - return fail(error, "invalid or overflowing startup memory field: " + key); - } - } - if (i != 4 && (!(fields >> unit) || unit != "kB" || !mul(value, 1024, value))) { - return fail(error, "invalid startup memory units or overflow: " + key); - } - if (fields >> extra) return fail(error, "extra startup memory field data: " + key); - values[i] = value; - found[i] = true; - } - } - if (!found[0]) return fail(error, "startup MemAvailable is missing"); - ImageMemorySnapshot next = snapshot; - next.host_available_bytes = values[0]; - next.host_gpu_reclaim_credit_bytes = 0; - next.host_capacity_policy = "raw-unqualified-kernel"; - if (qualified) { - uint64_t accounted = values[0]; - if ((found[2] && !add(accounted, values[2])) || - (found[3] && !add(accounted, values[3])) || - (found[1] && (!values[1] || accounted > values[1]))) { - return fail(error, "startup host/GPU memory counters exceed physical capacity"); - } - next.host_capacity_policy = "raw-missing-gpu-pool-fields"; - if (std::all_of(found.begin(), found.end(), [](bool value) { return value; })) { - next.host_capacity_policy = "raw-reserved-huge-pages"; - if (!values[4]) { - const uint64_t capacity = values[0] + values[3]; // checked in accounted above - next.host_available_bytes = capacity; - next.host_gpu_reclaim_credit_bytes = values[3]; - next.host_capacity_policy = "startup-linux-7.1.3-gpu-reclaim"; - if (reserves.primary_domain == ImageMemoryDomain::HostShared) next.primary_free_bytes = capacity; - if (reserves.cold_domain == ImageMemoryDomain::HostShared) next.cold_free_bytes = capacity; - } - } - } - snapshot = next; - return true; -} - bool estimate_deepseek4_image_storage( const common::DeepSeek4Weights & w, const common::MoeHybridPlacement & placement, const common::MoeHybridConfig & config, ggml_backend_t primary, ggml_backend_t cold, @@ -346,30 +272,12 @@ bool check_deepseek4_image_admission( reserves.cold_domain == ImageMemoryDomain::Unknown) { return fail(error, "actual owner host-memory sharing must be classified before admission"); } - ImageMemorySnapshot snapshot; - if (!device_free(primary, snapshot.primary_free_bytes, error) || - !device_free(cold, snapshot.cold_free_bytes, error)) return false; -#if defined(__linux__) - std::ifstream input("/proc/meminfo"); - if (!input) return fail(error, "cannot read startup host memory"); - std::ostringstream contents; - contents << input.rdbuf(); - if (!contents || input.bad()) return fail(error, "cannot finish reading startup host memory"); - std::string kernel_release; -#if defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP) - struct utsname kernel{}; - // Only real HIP owners use the qualified kernel pool accounting. Other - // backends retain their own free-memory limits, including exhausted ones. - if (ggml_backend_is_cuda(primary) && ggml_backend_is_cuda(cold) && - uname(&kernel) == 0) kernel_release = kernel.release; -#endif - if (!prepare_deepseek4_image_startup_snapshot(contents.str(), kernel_release, - reserves, snapshot, error)) return false; -#else - if (!host_available(snapshot.host_available_bytes, error)) return false; -#endif + if (!device_free(primary, out.primary_free_bytes, error) || + !device_free(cold, out.cold_free_bytes, error) || + !host_available(out.host_available_bytes, error)) return false; const ImageStorageEstimate storage = out.storage; const uint64_t activation_bytes = out.cold_activation_estimate_bytes; + const ImageMemorySnapshot snapshot{out.primary_free_bytes, out.cold_free_bytes, out.host_available_bytes}; return assess_deepseek4_image_admission(storage, activation_bytes, reserves, snapshot, out, error); } @@ -421,8 +329,6 @@ bool assess_deepseek4_image_admission( out.primary_free_bytes = snapshot.primary_free_bytes; out.cold_free_bytes = snapshot.cold_free_bytes; out.host_available_bytes = snapshot.host_available_bytes; - out.host_gpu_reclaim_credit_bytes = snapshot.host_gpu_reclaim_credit_bytes; - out.host_capacity_policy = snapshot.host_capacity_policy; const auto known_domain = [](ImageMemoryDomain domain) { return domain == ImageMemoryDomain::Dedicated || domain == ImageMemoryDomain::HostShared; }; diff --git a/server/src/deepseek4/deepseek4_image_admission.h b/server/src/deepseek4/deepseek4_image_admission.h index 93c4c8474..f6eeb5aca 100644 --- a/server/src/deepseek4/deepseek4_image_admission.h +++ b/server/src/deepseek4/deepseek4_image_admission.h @@ -62,8 +62,6 @@ struct ImageAdmissionReport { uint64_t primary_free_bytes = 0; uint64_t cold_free_bytes = 0; uint64_t host_available_bytes = 0; - uint64_t host_gpu_reclaim_credit_bytes = 0; - std::string host_capacity_policy = "raw"; uint64_t primary_required_bytes = 0; uint64_t cold_required_bytes = 0; uint64_t host_required_bytes = 0; @@ -77,26 +75,8 @@ struct ImageMemorySnapshot { uint64_t primary_free_bytes = 0; uint64_t cold_free_bytes = 0; uint64_t host_available_bytes = 0; - uint64_t host_gpu_reclaim_credit_bytes = 0; - std::string host_capacity_policy = "raw"; }; -// Pure startup-only snapshot transform. The live caller supplies one complete -// /proc/meminfo read and a Linux/HIP kernel release. Credit is qualified only for -// 7.1.3-070103-generic: upstream v7.1.3 mm/show_mem.c si_mem_available excludes -// NR_GPU_RECLAIM, while Documentation/filesystems/proc.rst defines reclaimable -// GPU pools separately from GPUActive. Kernel build provenance must be retained -// during qualification; neither newer versions nor field presence imply support. -// Unknown kernels/missing optional fields retain raw accounting. Malformed -// provided fields on a qualified kernel fail closed. No reserved huge pages are -// supported for credit. This is capacity accounting, not a zero-swap guarantee. -// Shared owners use the same capacity as the combined host gate, never separate -// additive credits. Runtime and preparation deliberately do not call this helper. -bool prepare_deepseek4_image_startup_snapshot( - const std::string & meminfo, const std::string & kernel_release, - const ImageAdmissionReserves & reserves, ImageMemorySnapshot & snapshot, - std::string & error); - // Pure assessment shared by live admission and deterministic CPU tests. All // figures describe allocations still to come relative to this one snapshot. bool assess_deepseek4_image_admission( diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index 6603b2202..d53942f2f 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -1248,7 +1248,6 @@ struct ImageAdmissionFakeOwner { ggml_backend_buffer_type buft{}; ggml_backend_device device{}; ggml_backend backend{}; - ggml_guid guid{}; size_t alignment = 128; size_t padding = 64; size_t maximum = SIZE_MAX; @@ -1289,7 +1288,6 @@ struct ImageAdmissionFakeOwner { *free = owner.free_bytes; *total = owner.total_bytes; }; - backend.guid = &guid; backend.device = &device; backend.context = this; backend.iface.graph_compute = [](ggml_backend_t b, ggml_cgraph *) { @@ -1437,7 +1435,6 @@ static void test_image_storage_admission_metadata() { &hot.backend, &cold.backend, reserves, report, error)); TEST_ASSERT(report.storage_estimated && !report.known_charges_fit); TEST_ASSERT(report.cold_free_bytes == 0); - TEST_ASSERT(report.host_gpu_reclaim_credit_bytes == 0); TEST_ASSERT(!check_deepseek4_image_runtime_admission(config, &hot.backend, &cold.backend, reserves, report, error)); TEST_ASSERT(report.storage.hot_allocation_bytes == 0 && report.storage.cold_allocation_bytes == 0); @@ -1561,107 +1558,6 @@ static void test_image_admission_resource_snapshots() { std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); } -static void test_image_startup_reclaim_accounting() { - std::fprintf(stderr, "test_image_startup_reclaim_accounting...\n"); - using namespace dflash::vision; - constexpr uint64_t kib = 1024; - constexpr uint64_t gib = 1024 * 1024 * kib; - const std::string kernel = "7.1.3-070103-generic"; - const auto meminfo = [](uint64_t available, uint64_t reclaim) { - return "MemAvailable: " + std::to_string(available) + " kB\nMemTotal: 130023424 kB\n" - "GPUActive: 1024 kB\nGPUReclaim: " + std::to_string(reclaim) + - " kB\nHugePages_Total: 0\n"; - }; - ImageAdmissionReserves reserves; - reserves.primary_domain = ImageMemoryDomain::Dedicated; - reserves.cold_domain = ImageMemoryDomain::HostShared; - reserves.host_request_bytes = 4 * gib; - reserves.host_loader_overhead_bytes = gib; - reserves.cold_runtime_reservation_bytes = 2 * gib; - ImageStorageEstimate storage; - storage.cold_allocation_bytes = 89163 * gib / 1000; - ImageMemorySnapshot raw{8 * gib, 73470 * gib / 1000, 73470 * gib / 1000}; - ImageMemorySnapshot startup = raw; - ImageAdmissionReport report; - std::string error; - const auto fields = meminfo(raw.host_available_bytes / kib, (49200 * gib / 1000) / kib); - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields, kernel, reserves, startup, error)); - TEST_ASSERT(startup.primary_free_bytes == raw.primary_free_bytes); - TEST_ASSERT(startup.cold_free_bytes == startup.host_available_bytes); - TEST_ASSERT(startup.host_available_bytes == raw.host_available_bytes / kib * kib + - (49200 * gib / 1000) / kib * kib); - TEST_ASSERT(assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); - TEST_ASSERT(report.host_required_bytes == storage.cold_allocation_bytes + 6 * gib); - TEST_ASSERT(report.host_gpu_reclaim_credit_bytes == startup.host_gpu_reclaim_credit_bytes); - TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, raw, report, error)); - for (const std::string release : {std::string("7.1.4"), std::string(""), std::string("7.1.3-other")}) { - ImageMemorySnapshot other = raw; - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields, release, reserves, other, error)); - TEST_ASSERT(other.host_gpu_reclaim_credit_bytes == 0 && other.cold_free_bytes == raw.cold_free_bytes); - TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, other, report, error)); - } - // Even with qualified credit, dedicated physical limits remain binding. - storage.hot_allocation_bytes = raw.primary_free_bytes + 1; - TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); - storage.hot_allocation_bytes = 0; - // Two UMA owners spend a single physical pool. Exact combined fit succeeds; - // adding one byte fails although both individual owner limits still fit. - reserves.primary_domain = ImageMemoryDomain::HostShared; - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields, kernel, reserves, startup, error)); - storage.hot_allocation_bytes = startup.host_available_bytes - storage.cold_allocation_bytes - 6 * gib; - TEST_ASSERT(assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); - ++storage.hot_allocation_bytes; - TEST_ASSERT(!assess_deepseek4_image_admission(storage, gib, reserves, startup, report, error)); - TEST_ASSERT(report.primary_required_bytes < startup.primary_free_bytes && - report.cold_required_bytes < startup.cold_free_bytes); - // An ordinary runtime snapshot has no credit; fresh graphs cannot inherit - // a startup pool balance that may have been consumed by resident weights. - ImageMemorySnapshot runtime{gib, gib, gib}; - TEST_ASSERT(!assess_deepseek4_image_admission({}, gib, reserves, runtime, report, error)); - TEST_ASSERT(report.host_gpu_reclaim_credit_bytes == 0 && report.host_capacity_policy == "raw"); - for (const std::string optional : {std::string(""), std::string("GPUReclaim: 8 kB\n")}) { - ImageMemorySnapshot missing = raw; - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot("MemAvailable: 32 kB\n" + optional, - kernel, reserves, missing, error)); - TEST_ASSERT(missing.host_available_bytes == 32 * kib && missing.host_gpu_reclaim_credit_bytes == 0); - TEST_ASSERT(missing.primary_free_bytes == raw.primary_free_bytes); - } - ImageMemorySnapshot huge = raw; - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(fields + "Unrelated: 1\n", kernel, reserves, huge, error)); - std::string huge_fields = fields; - huge_fields.replace(huge_fields.find("HugePages_Total: 0"), 18, "HugePages_Total: 1"); - huge = raw; - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(huge_fields, kernel, reserves, huge, error)); - TEST_ASSERT(huge.host_gpu_reclaim_credit_bytes == 0 && huge.cold_free_bytes == raw.cold_free_bytes); - const std::string prefix = "MemAvailable: 32 kB\nMemTotal: 128 kB\nHugePages_Total: 0\n"; - for (const std::string counters : { - "GPUActive: 1 kB\nGPUReclaim: -1 kB\n", "GPUActive: 1 kB\nGPUReclaim: +1 kB\n", - "GPUActive: 1 kB\nGPUReclaim: 1 MB\n", "GPUActive: 1 kB\nGPUReclaim: 1 kB extra\n", - "GPUActive: 1 kB\nGPUReclaim: 1 kB\nGPUReclaim: 1 kB\n", - "GPUActive: 97 kB\nGPUReclaim: 0 kB\n", "GPUActive: 0 kB\nGPUReclaim: 97 kB\n", - "GPUActive: 0 kB\nGPUReclaim: 18446744073709551615 kB\n", - "GPUActive: 0 kB\nGPUReclaim: 18446744073709551616 kB\n"}) { - ImageMemorySnapshot invalid = raw; - TEST_ASSERT(!prepare_deepseek4_image_startup_snapshot(prefix + counters, kernel, reserves, invalid, error)); - TEST_ASSERT(invalid.host_available_bytes == raw.host_available_bytes && invalid.host_gpu_reclaim_credit_bytes == 0); - } - for (const std::string bad_raw : {"MemAvailable: -1 kB\n", "MemAvailable: 1 kB\nMemAvailable: 2 kB\n", - "MemAvailable: 1 MB\n", "GPUReclaim: 1 kB\n"}) { - ImageMemorySnapshot invalid = raw; - TEST_ASSERT(!prepare_deepseek4_image_startup_snapshot(bad_raw, "unknown", reserves, invalid, error)); - } - ImageMemorySnapshot active = raw; - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(prefix + "GPUActive: 80 kB\nGPUReclaim: 16 kB\n", - kernel, reserves, active, error)); - TEST_ASSERT(active.host_available_bytes == 48 * kib); // active pages never count as capacity - // GPU fields beyond 2 KiB must still be parsed, not silently truncated. - ImageMemorySnapshot long_input = raw; - TEST_ASSERT(prepare_deepseek4_image_startup_snapshot(std::string(4096, ' ') + "\n" + fields, - kernel, reserves, long_input, error)); - TEST_ASSERT(long_input.host_gpu_reclaim_credit_bytes == startup.host_gpu_reclaim_credit_bytes); - std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); -} - static DeepSeek4LayerSplitAdapter make_test_adapter() { DeepSeek4LayerSplitAdapterConfig cfg; cfg.device.gpu = 0; @@ -4801,7 +4697,6 @@ int main() { test_image_batch_admission_before_execution(backend); test_image_storage_admission_metadata(); test_image_admission_resource_snapshots(); - test_image_startup_reclaim_accounting(); test_dspark_loader_contract_and_bounds(backend); test_dspark_confidence_uses_separate_hidden(backend); test_safe_compressor_batch_tokens(); From 91cc90a7a572b5a7e43940a18434177a05cadda1 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 18:38:51 -0400 Subject: [PATCH 069/123] Reclaim copied mmap source pages during hybrid GPU loading --- server/CMakeLists.txt | 4 + server/src/common/moe_hybrid_storage.cpp | 52 ++++++++++++- server/src/common/moe_hybrid_storage.h | 7 +- server/src/common/moe_source_page_range.h | 40 ++++++++++ server/test/test_moe_source_page_range.cpp | 88 ++++++++++++++++++++++ 5 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 server/src/common/moe_source_page_range.h create mode 100644 server/test/test_moe_source_page_range.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 679a3ce00..def640077 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1630,6 +1630,10 @@ if(DFLASH27B_TESTS) endif() # ─── Unit tests (no GPU, no model files) ──────────────────────────── + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_moe_source_page_range.cpp") + add_executable(test_moe_source_page_range test/test_moe_source_page_range.cpp) + list(APPEND _raw_unit_test_targets test_moe_source_page_range) + endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_moe_input_ready.cpp") add_executable(test_moe_input_ready test/test_moe_input_ready.cpp) target_include_directories(test_moe_input_ready PRIVATE diff --git a/server/src/common/moe_hybrid_storage.cpp b/server/src/common/moe_hybrid_storage.cpp index baa1adca5..19c13a2d4 100644 --- a/server/src/common/moe_hybrid_storage.cpp +++ b/server/src/common/moe_hybrid_storage.cpp @@ -1,5 +1,6 @@ #include "moe_hybrid_storage.h" #include "moe_hybrid_types.h" +#include "moe_source_page_range.h" #include "ggml-cpu.h" #include "ggml-backend.h" @@ -9,6 +10,11 @@ #include #include #include +#include +#include +#if defined(__linux__) +#include +#endif #if defined(DFLASH27B_BACKEND_CUDA) #include @@ -30,6 +36,31 @@ namespace dflash::common { namespace { +void advise_copied_source(const void * mapping, size_t mapping_size, + const ExpertTensorFileData & tensor, int layer) { +#if defined(__linux__) && defined(MADV_PAGEOUT) + if (!tensor.data || tensor.size == 0) return; + const long page_size = ::sysconf(_SC_PAGESIZE); + MoeSourcePageRange range; + if (page_size <= 0 || !moe_source_page_range( + reinterpret_cast(mapping), mapping_size, + reinterpret_cast(tensor.data), tensor.size, + static_cast(page_size), range)) { + std::fprintf(stderr, "[hybrid-storage] layer %d source pageout rejected: errno=%d requested=%zu bytes\n", + layer, EINVAL, tensor.size); + return; + } + if (range.size == 0) return; + errno = 0; + const int rc = ::madvise(reinterpret_cast(range.address), range.size, MADV_PAGEOUT); + const int error = rc == 0 ? 0 : errno; + std::fprintf(stderr, "[hybrid-storage] layer %d source pageout advisory: requested=%zu bytes rc=%d errno=%d\n", + layer, range.size, rc, error); +#else + (void) mapping; (void) mapping_size; (void) tensor; (void) layer; +#endif +} + void unregister_mix_tensor(ggml_tensor * tensor) { if (!tensor || !tensor->data) return; @@ -481,7 +512,9 @@ bool build_moe_hybrid_storage_from_file( std::string * err, int cache_slots, bool allocate_cold, - ggml_backend_t cold_gpu_backend) { + ggml_backend_t cold_gpu_backend, + const void * readonly_file_mmap, + size_t readonly_file_mmap_size) { if (!placement.matches(cfg)) { if (err) *err = "placement does not match config"; @@ -688,6 +721,21 @@ bool build_moe_hybrid_storage_from_file( ggml_backend_tensor_set(dst.down_cold, slice_buf.data(), 0, slice_buf.size()); } } + // Only the mmap-retaining wrapper supplies proven file-mapping bounds. + // All synchronous hot/cold uploads have returned, and both temporary + // slice buffers are gone. Retain the mapping for future streaming reads. + if (readonly_file_mmap && moe_source_pageout_eligible( + out.cold_backend_kind == MoeHybridColdBackend::Gpu, + cfg.materialize_hot_experts, cfg.materialize_cold_experts, + allocate_cold && cold_count > 0 && dst.cold_buf != nullptr)) { + if (dst.fused_gate_up) { + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.gate_up_exps, il); + } else { + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.gate_exps, il); + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.up_exps, il); + } + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.down_exps, il); + } } return true; @@ -790,7 +838,7 @@ bool build_moe_hybrid_storage_from_file_with_mmap( // First build storage normally (hot GPU + cold CPU buffers). if (!build_moe_hybrid_storage_from_file( cfg, gpu_backend, placement, layer_descs, file_data, - out, err, cache_slots, true, cold_gpu_backend)) { + out, err, cache_slots, true, cold_gpu_backend, mmap_base, mmap_total_size)) { return false; } diff --git a/server/src/common/moe_hybrid_storage.h b/server/src/common/moe_hybrid_storage.h index ab065175b..be36ae5c7 100644 --- a/server/src/common/moe_hybrid_storage.h +++ b/server/src/common/moe_hybrid_storage.h @@ -263,6 +263,9 @@ int moe_hybrid_cache_swap_in(MoeHybridLayerStorage & st, int global_expert, ggml_backend_t gpu_backend); // Build hybrid storage by loading expert data directly from file (mmap). +// Optional readonly_file_mmap metadata is supplied only by the mmap-retaining +// wrapper for a read-only file-backed mapping. It permits advisory reclamation +// of completed materialized GPU layers without invalidating source pointers. bool build_moe_hybrid_storage_from_file( const MoeHybridConfig & cfg, ggml_backend_t gpu_backend, @@ -273,7 +276,9 @@ bool build_moe_hybrid_storage_from_file( std::string * err = nullptr, int cache_slots = 0, bool allocate_cold = true, - ggml_backend_t cold_gpu_backend = nullptr); + ggml_backend_t cold_gpu_backend = nullptr, + const void * readonly_file_mmap = nullptr, + size_t readonly_file_mmap_size = 0); // Spark: split a VRAM budget into a pinned-hot tier + an auto-sized expert // cache ring. target_bytes==0 keeps the current budget (use the card); diff --git a/server/src/common/moe_source_page_range.h b/server/src/common/moe_source_page_range.h new file mode 100644 index 000000000..31e094b6d --- /dev/null +++ b/server/src/common/moe_source_page_range.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include +#include + +namespace dflash::common { + +struct MoeSourcePageRange { + uintptr_t address = 0; + size_t size = 0; +}; + +// Select only complete pages inside BOTH a tensor and its verified read-only +// file mapping. An empty interior is valid; invalid/overflowing spans fail. +inline bool moe_source_page_range(uintptr_t mapping, size_t mapping_size, + uintptr_t tensor, size_t tensor_size, + size_t page_size, MoeSourcePageRange & out) { + out = {}; + const uintptr_t max = std::numeric_limits::max(); + if (!mapping || !page_size || (page_size & (page_size - 1)) != 0 || + mapping_size > max - mapping || tensor < mapping) return false; + const uintptr_t mapping_end = mapping + mapping_size; + if (tensor > mapping_end || tensor_size > mapping_end - tensor) return false; + const size_t padding = (page_size - tensor % page_size) % page_size; + if (padding > tensor_size) return true; + const size_t length = ((tensor_size - padding) / page_size) * page_size; + if (length) { + out.address = tensor + padding; + out.size = length; + } + return true; +} + +inline bool moe_source_pageout_eligible(bool cold_gpu, bool hot_materialized, + bool cold_materialized, bool cold_allocated) { + return cold_gpu && hot_materialized && cold_materialized && cold_allocated; +} + +} // namespace dflash::common diff --git a/server/test/test_moe_source_page_range.cpp b/server/test/test_moe_source_page_range.cpp new file mode 100644 index 000000000..9935a8c80 --- /dev/null +++ b/server/test/test_moe_source_page_range.cpp @@ -0,0 +1,88 @@ +#include "../src/common/moe_source_page_range.h" + +#include +#include +#include +#include +#include +#if defined(__linux__) +#include +#include +#include +#endif + +using namespace dflash::common; + +static void check(bool ok, const char * message) { + if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } +} + +int main() { + constexpr size_t page = 4096; + MoeSourcePageRange range; + check(moe_source_page_range(page, 5 * page, page + 17, 3 * page + 100, page, range), "unaligned tensor valid"); + check(range.address == 2 * page && range.size == 2 * page, "only tensor interior pages selected"); + check(moe_source_page_range(page, 2 * page, page, 2 * page, page, range) && range.size == 2 * page, + "exact mapping boundaries"); + check(moe_source_page_range(page, page, page + 1, page - 2, page, range) && range.size == 0, + "partial page never reclaimed"); + check(moe_source_page_range(page, page, 2 * page, 0, page, range) && range.size == 0, "empty end span"); + check(!moe_source_page_range(page, page, page - 1, page, page, range), "span before mapping rejected"); + check(!moe_source_page_range(page, page, page, page + 1, page, range), "span past mapping rejected"); + check(!moe_source_page_range(page, page, page, page, 0, range), "zero page size rejected"); + check(!moe_source_page_range(page, page, page, page, 3, range), "invalid page size rejected"); + const uintptr_t max = std::numeric_limits::max(); + check(!moe_source_page_range(max - page, page + 1, max - page, 1, page, range), "mapping overflow rejected"); + check(!moe_source_page_range(page, page, page, std::numeric_limits::max(), page, range), + "tensor overflow rejected"); + check(moe_source_page_range(max - 3 * page, 2 * page, max - 3 * page + 1, page, page, range), + "valid near-address-limit span"); + for (unsigned mask = 0; mask < 16; ++mask) { + check(moe_source_pageout_eligible(mask & 1, mask & 2, mask & 4, mask & 8) == (mask == 15), + "CPU/unmaterialized/unallocated modes excluded"); + } +#if defined(__linux__) && defined(MADV_PAGEOUT) + const long raw_page = ::sysconf(_SC_PAGESIZE); + check(raw_page > 0, "native page size"); + const size_t p = static_cast(raw_page), size = 5 * p; + char name[] = "/tmp/ds4v-source-pageout-XXXXXX"; + const int fd = ::mkstemp(name); + check(fd >= 0, "temporary backing file"); + ::unlink(name); + check(::ftruncate(fd, static_cast(size)) == 0, "size backing file"); + void * writable = ::mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + check(writable != MAP_FAILED, "initialize file mapping"); + std::vector expected(size); + for (size_t i = 0; i < size; ++i) expected[i] = static_cast((i * 37 + i / p) % 251); + for (size_t i = 0; i < size; ++i) static_cast(writable)[i] = expected[i]; + check(::msync(writable, size, MS_SYNC) == 0, "clean backing pages"); + check(::munmap(writable, size) == 0, "close initialization mapping"); + void * mapped = ::mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); + check(mapped != MAP_FAILED, "read-only source mapping"); + ::close(fd); // Match the production lifetime: mapping remains, FD is closed. + const volatile uint8_t * bytes = static_cast(mapped); + for (size_t i = 0; i < size; ++i) check(bytes[i] == expected[i], "source before advice"); + check(moe_source_page_range(reinterpret_cast(mapped), size, + reinterpret_cast(mapped) + 17, 3 * p + 100, p, range), "native source range"); + std::vector before(5), after(5); + const int before_rc = ::mincore(mapped, size, before.data()); + errno = 0; + const int rc = ::madvise(reinterpret_cast(range.address), range.size, MADV_PAGEOUT); + const int advice_errno = rc == 0 ? 0 : errno; + const int after_rc = ::mincore(mapped, size, after.data()); + unsigned before_count = 0, after_count = 0; + for (size_t i = 0; i < 5; ++i) { before_count += before[i] & 1; after_count += after[i] & 1; } + std::printf("MADV_PAGEOUT requested=%zu rc=%d errno=%d mincore_before_rc=%d pages=%u after_rc=%d pages=%u\n", + range.size, rc, advice_errno, before_rc, before_count, after_rc, after_count); + check(rc == 0 || advice_errno == EINVAL || advice_errno == ENOSYS || advice_errno == EOPNOTSUPP, + "unexpected pageout failure"); + if (rc != 0) std::puts("SKIP: kernel does not support this advisory; bounds checks still passed"); + // Refault correctness is mandatory; eviction count is only diagnostic. + for (size_t i = 0; i < size; ++i) check(bytes[i] == expected[i], "source/refault and edge bytes preserved"); + check(::munmap(mapped, size) == 0, "close source mapping"); +#else + std::puts("SKIP: Linux MADV_PAGEOUT unavailable; bounds and mode checks passed"); +#endif + std::puts("PASS: source-page bounds, mode exclusions and supported file-refault checks"); + return 0; +} From 439c8c3384997d8ea6dd09abbef4bb6b28878751 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 19:16:49 -0400 Subject: [PATCH 070/123] Release copied model file cache during hybrid loading --- server/src/common/copied_source_reclaim.h | 64 ++++++++++++++++++++++ server/src/common/moe_hybrid_storage.cpp | 24 +++++--- server/src/common/moe_hybrid_storage.h | 8 ++- server/src/deepseek4/deepseek4_loader.cpp | 21 ++++++- server/test/test_moe_source_page_range.cpp | 31 +++++++---- 5 files changed, 126 insertions(+), 22 deletions(-) create mode 100644 server/src/common/copied_source_reclaim.h diff --git a/server/src/common/copied_source_reclaim.h b/server/src/common/copied_source_reclaim.h new file mode 100644 index 000000000..f9dced63e --- /dev/null +++ b/server/src/common/copied_source_reclaim.h @@ -0,0 +1,64 @@ +#pragma once + +#include "moe_source_page_range.h" +#include +#include +#if defined(__linux__) +#include +#include +#include +#endif + +namespace dflash::common { + +struct CopiedSourceAdviceResult { + size_t requested = 0; + int range_error = 0; + int madvise_error = 0; + int fadvise_error = 0; +}; + +// Contract: a read-only file mapping starting at file offset zero, its original +// borrowed fd, and a fully copied source tensor. Never advise anonymous/GPU +// allocations. The caller retains mapping ownership; future reads refault the +// original file bytes. Return values report advice acceptance, not bytes freed. +inline CopiedSourceAdviceResult reclaim_copied_file_source( + const void * mapping, size_t mapping_size, const void * source, + size_t source_size, int fd, const char * label, int layer = -1) { + CopiedSourceAdviceResult result; +#if defined(__linux__) + const long page_size = ::sysconf(_SC_PAGESIZE); + MoeSourcePageRange range; + const uintptr_t base = reinterpret_cast(mapping); + if (fd < 0 || page_size <= 0 || !moe_source_page_range( + base, mapping_size, reinterpret_cast(source), source_size, + static_cast(page_size), range)) { + result.range_error = EINVAL; + } else if (range.size != 0) { + const uintptr_t offset = range.address - base; + const auto max_offset = static_cast(std::numeric_limits::max()); + if (offset > max_offset || range.size > max_offset - offset) { + result.range_error = EOVERFLOW; + } else { + result.requested = range.size; + // Drop this mapping's PTE references before invalidating file cache. + if (::madvise(reinterpret_cast(range.address), range.size, MADV_DONTNEED) != 0) + result.madvise_error = errno; + // posix_fadvise returns the error number itself, not -1/errno. + result.fadvise_error = ::posix_fadvise(fd, static_cast(offset), + static_cast(range.size), POSIX_FADV_DONTNEED); + } + } + if (result.requested || result.range_error) { + std::fprintf(stderr, "[source-reclaim] %s layer=%d requested=%zu range_error=%d madvise_error=%d fadvise_error=%d\n", + label, layer, result.requested, result.range_error, + result.madvise_error, result.fadvise_error); + } +#else + (void) mapping; (void) mapping_size; (void) source; (void) source_size; + (void) fd; (void) label; (void) layer; +#endif + return result; +} + +} // namespace dflash::common diff --git a/server/src/common/moe_hybrid_storage.cpp b/server/src/common/moe_hybrid_storage.cpp index 19c13a2d4..821a040c8 100644 --- a/server/src/common/moe_hybrid_storage.cpp +++ b/server/src/common/moe_hybrid_storage.cpp @@ -1,6 +1,7 @@ #include "moe_hybrid_storage.h" #include "moe_hybrid_types.h" #include "moe_source_page_range.h" +#include "copied_source_reclaim.h" #include "ggml-cpu.h" #include "ggml-backend.h" @@ -37,7 +38,12 @@ namespace dflash::common { namespace { void advise_copied_source(const void * mapping, size_t mapping_size, - const ExpertTensorFileData & tensor, int layer) { + const ExpertTensorFileData & tensor, int layer, int source_fd) { + if (source_fd >= 0) { + reclaim_copied_file_source(mapping, mapping_size, tensor.data, tensor.size, + source_fd, "expert", layer); + return; + } #if defined(__linux__) && defined(MADV_PAGEOUT) if (!tensor.data || tensor.size == 0) return; const long page_size = ::sysconf(_SC_PAGESIZE); @@ -514,7 +520,8 @@ bool build_moe_hybrid_storage_from_file( bool allocate_cold, ggml_backend_t cold_gpu_backend, const void * readonly_file_mmap, - size_t readonly_file_mmap_size) { + size_t readonly_file_mmap_size, + int readonly_file_fd) { if (!placement.matches(cfg)) { if (err) *err = "placement does not match config"; @@ -729,12 +736,12 @@ bool build_moe_hybrid_storage_from_file( cfg.materialize_hot_experts, cfg.materialize_cold_experts, allocate_cold && cold_count > 0 && dst.cold_buf != nullptr)) { if (dst.fused_gate_up) { - advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.gate_up_exps, il); + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.gate_up_exps, il, readonly_file_fd); } else { - advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.gate_exps, il); - advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.up_exps, il); + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.gate_exps, il, readonly_file_fd); + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.up_exps, il, readonly_file_fd); } - advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.down_exps, il); + advise_copied_source(readonly_file_mmap, readonly_file_mmap_size, fd.down_exps, il, readonly_file_fd); } } @@ -833,12 +840,13 @@ bool build_moe_hybrid_storage_from_file_with_mmap( MoeHybridStorage & out, std::string * err, int cache_slots, - ggml_backend_t cold_gpu_backend) { + ggml_backend_t cold_gpu_backend, + int readonly_file_fd) { // First build storage normally (hot GPU + cold CPU buffers). if (!build_moe_hybrid_storage_from_file( cfg, gpu_backend, placement, layer_descs, file_data, - out, err, cache_slots, true, cold_gpu_backend, mmap_base, mmap_total_size)) { + out, err, cache_slots, true, cold_gpu_backend, mmap_base, mmap_total_size, readonly_file_fd)) { return false; } diff --git a/server/src/common/moe_hybrid_storage.h b/server/src/common/moe_hybrid_storage.h index be36ae5c7..04e534ea3 100644 --- a/server/src/common/moe_hybrid_storage.h +++ b/server/src/common/moe_hybrid_storage.h @@ -278,7 +278,8 @@ bool build_moe_hybrid_storage_from_file( bool allocate_cold = true, ggml_backend_t cold_gpu_backend = nullptr, const void * readonly_file_mmap = nullptr, - size_t readonly_file_mmap_size = 0); + size_t readonly_file_mmap_size = 0, + int readonly_file_fd = -1); // Spark: split a VRAM budget into a pinned-hot tier + an auto-sized expert // cache ring. target_bytes==0 keeps the current budget (use the card); @@ -293,6 +294,8 @@ MoeSparkBudget spark_budget_split(uint64_t expert_budget, uint64_t total_expert_ // mmap_base: pointer to start of mmap'd file. // mmap_total_size: total file size. // This variant populates out.layer_regions for use by MoeHybridStreamEngine. +// Optional fd is borrowed only during construction and must identify this +// offset-zero read-only mapping. The caller closes it after this call returns. bool build_moe_hybrid_storage_from_file_with_mmap( const MoeHybridConfig & cfg, ggml_backend_t gpu_backend, @@ -304,6 +307,7 @@ bool build_moe_hybrid_storage_from_file_with_mmap( MoeHybridStorage & out, std::string * err = nullptr, int cache_slots = 0, - ggml_backend_t cold_gpu_backend = nullptr); + ggml_backend_t cold_gpu_backend = nullptr, + int readonly_file_fd = -1); } // namespace dflash::common diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index 2cbd6bf60..0dc1fece9 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -18,6 +18,7 @@ #include "dflash27b.h" #include "common/gguf_bounds.h" #include "../common/moe_hybrid_storage.h" +#include "../common/copied_source_reclaim.h" #include "../common/moe_hybrid_types.h" #include "ggml-cuda.h" @@ -1819,6 +1820,11 @@ bool load_deepseek4_gguf_partial(const std::string & path, if (!a.upload_to_backend || !a.dense_split) continue; const void * src_data = (const char *)mmap.addr + a.file_offset; ggml_backend_tensor_set(a.tensor, src_data, 0, a.file_size); +#if defined(__linux__) + // set_tensor has completed its source copy, including split buffers. + reclaim_copied_file_source(mmap.addr, mmap.len, src_data, a.file_size, + mmap.fd, ggml_get_name(a.tensor)); +#endif } if (!read_ok) { set_last_error("parallel weight read failed"); @@ -1834,6 +1840,11 @@ bool load_deepseek4_gguf_partial(const std::string & path, if (!a.upload_to_backend) continue; const void * src_data = (const char *)mmap.addr + a.file_offset; ggml_backend_tensor_set(a.tensor, src_data, 0, a.file_size); +#if defined(__linux__) + // set_tensor has completed its source copy, including split buffers. + reclaim_copied_file_source(mmap.addr, mmap.len, src_data, a.file_size, + mmap.fd, ggml_get_name(a.tensor)); +#endif } } mmap.close_map(); @@ -2138,7 +2149,9 @@ bool build_deepseek4_moe_hybrid_storage_from_file_with_mmap( if (err) *err = mmap_err; return false; } +#if !defined(__linux__) mmap.close_fd(); +#endif const size_t data_start = gguf_get_data_offset(gctx); const auto * file_bytes = static_cast(mmap.addr); @@ -2185,7 +2198,13 @@ bool build_deepseek4_moe_hybrid_storage_from_file_with_mmap( const MoeHybridConfig cfg = cfg_override ? *cfg_override : make_ds4_moe_hybrid_config(w); const bool ok = build_moe_hybrid_storage_from_file_with_mmap( cfg, backend, placement, layer_descs, layer_file_data, - mmap.addr, mmap.len, out, err, 0, cold_gpu_backend); + mmap.addr, mmap.len, out, err, 0, cold_gpu_backend +#if defined(__linux__) + , mmap.fd +#endif + ); + // Advice borrows the original fd only while construction is in progress. + mmap.close_fd(); if (!ok) { mmap.close_map(); diff --git a/server/test/test_moe_source_page_range.cpp b/server/test/test_moe_source_page_range.cpp index 9935a8c80..42f579d63 100644 --- a/server/test/test_moe_source_page_range.cpp +++ b/server/test/test_moe_source_page_range.cpp @@ -1,4 +1,5 @@ #include "../src/common/moe_source_page_range.h" +#include "../src/common/copied_source_reclaim.h" #include #include @@ -41,7 +42,7 @@ int main() { check(moe_source_pageout_eligible(mask & 1, mask & 2, mask & 4, mask & 8) == (mask == 15), "CPU/unmaterialized/unallocated modes excluded"); } -#if defined(__linux__) && defined(MADV_PAGEOUT) +#if defined(__linux__) const long raw_page = ::sysconf(_SC_PAGESIZE); check(raw_page > 0, "native page size"); const size_t p = static_cast(raw_page), size = 5 * p; @@ -59,29 +60,37 @@ int main() { check(::munmap(writable, size) == 0, "close initialization mapping"); void * mapped = ::mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); check(mapped != MAP_FAILED, "read-only source mapping"); - ::close(fd); // Match the production lifetime: mapping remains, FD is closed. + // Keep the original descriptor until copied-source advice returns. const volatile uint8_t * bytes = static_cast(mapped); for (size_t i = 0; i < size; ++i) check(bytes[i] == expected[i], "source before advice"); check(moe_source_page_range(reinterpret_cast(mapped), size, reinterpret_cast(mapped) + 17, 3 * p + 100, p, range), "native source range"); std::vector before(5), after(5); const int before_rc = ::mincore(mapped, size, before.data()); - errno = 0; - const int rc = ::madvise(reinterpret_cast(range.address), range.size, MADV_PAGEOUT); - const int advice_errno = rc == 0 ? 0 : errno; + errno = EBUSY; // Successful advice must not report a stale errno. + const auto advice = reclaim_copied_file_source(mapped, size, + static_cast(mapped) + 17, 3 * p + 100, fd, "test"); const int after_rc = ::mincore(mapped, size, after.data()); unsigned before_count = 0, after_count = 0; for (size_t i = 0; i < 5; ++i) { before_count += before[i] & 1; after_count += after[i] & 1; } - std::printf("MADV_PAGEOUT requested=%zu rc=%d errno=%d mincore_before_rc=%d pages=%u after_rc=%d pages=%u\n", - range.size, rc, advice_errno, before_rc, before_count, after_rc, after_count); - check(rc == 0 || advice_errno == EINVAL || advice_errno == ENOSYS || advice_errno == EOPNOTSUPP, - "unexpected pageout failure"); - if (rc != 0) std::puts("SKIP: kernel does not support this advisory; bounds checks still passed"); + std::printf("copied source requested=%zu madvise_error=%d fadvise_error=%d mincore_before_rc=%d pages=%u after_rc=%d pages=%u\n", + advice.requested, advice.madvise_error, advice.fadvise_error, + before_rc, before_count, after_rc, after_count); + check(advice.range_error == 0 && advice.requested == 2 * p, "advice uses exact interior range"); + check(advice.madvise_error == 0, "read-only file MADV_DONTNEED accepted"); + check(advice.fadvise_error == 0 || advice.fadvise_error == ENOSYS || advice.fadvise_error == EOPNOTSUPP, + "unexpected file cache advice failure"); + const auto partial = reclaim_copied_file_source(mapped, size, + static_cast(mapped) + 1, p - 2, fd, "partial-test"); + check(partial.requested == 0 && partial.range_error == 0, "empty interior causes no whole-file fadvise"); + const auto invalid = reclaim_copied_file_source(mapped, size, mapped, size, -1, "invalid-fd-test"); + check(invalid.requested == 0 && invalid.range_error == EINVAL, "invalid fd rejected before advice"); + check(::close(fd) == 0, "close borrowed fd after advice"); // Refault correctness is mandatory; eviction count is only diagnostic. for (size_t i = 0; i < size; ++i) check(bytes[i] == expected[i], "source/refault and edge bytes preserved"); check(::munmap(mapped, size) == 0, "close source mapping"); #else - std::puts("SKIP: Linux MADV_PAGEOUT unavailable; bounds and mode checks passed"); + std::puts("SKIP: Linux copied-source advice unavailable; bounds and mode checks passed"); #endif std::puts("PASS: source-page bounds, mode exclusions and supported file-refault checks"); return 0; From 6914f9d7e7557e3c942b6476fa41059227a7d94f Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 19:42:56 -0400 Subject: [PATCH 071/123] Stage HIP dense uploads through bounded host scratch --- server/src/common/copied_source_upload.h | 38 ++++++++++++++++ server/src/deepseek4/deepseek4_loader.cpp | 32 ++++++++++++- server/test/test_moe_source_page_range.cpp | 52 +++++++++++++++++++++- 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 server/src/common/copied_source_upload.h diff --git a/server/src/common/copied_source_upload.h b/server/src/common/copied_source_upload.h new file mode 100644 index 000000000..4c2bb0d31 --- /dev/null +++ b/server/src/common/copied_source_upload.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace dflash::common { + +inline constexpr size_t COPIED_SOURCE_UPLOAD_CHUNK = 16 * 1024 * 1024; + +// Upload a verified file span through one reusable heap buffer. The synchronous +// callback must finish reading each chunk before returning. Its destination +// offset is relative to the tensor, never the file. No original file pointer +// reaches the callback. The caller retains and reuses scratch across tensors. +template +inline bool upload_copied_file_chunks( + const void * mapping, size_t mapping_size, size_t source_offset, + size_t source_size, std::vector & scratch, Upload && upload) { + const uintptr_t base = reinterpret_cast(mapping); + if (!mapping || mapping_size > std::numeric_limits::max() - base || + source_offset > mapping_size || source_size > mapping_size - source_offset) + return false; + if (source_size == 0) return true; + scratch.resize(std::min(source_size, COPIED_SOURCE_UPLOAD_CHUNK)); + const auto * source = static_cast(mapping) + source_offset; + for (size_t offset = 0; offset < source_size;) { + const size_t count = std::min(scratch.size(), source_size - offset); + std::memcpy(scratch.data(), source + offset, count); + upload(scratch.data(), offset, count); + offset += count; + } + return true; +} + +} // namespace dflash::common diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index 0dc1fece9..168e21dd5 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -19,6 +19,7 @@ #include "common/gguf_bounds.h" #include "../common/moe_hybrid_storage.h" #include "../common/copied_source_reclaim.h" +#include "../common/copied_source_upload.h" #include "../common/moe_hybrid_types.h" #include "ggml-cuda.h" @@ -1836,10 +1837,34 @@ bool load_deepseek4_gguf_partial(const std::string & path, return false; } } else { +#if defined(__linux__) && (defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP)) + std::vector upload_scratch; +#endif for (auto & a : allocs) { if (!a.upload_to_backend) continue; const void * src_data = (const char *)mmap.addr + a.file_offset; - ggml_backend_tensor_set(a.tensor, src_data, 0, a.file_size); +#if defined(__linux__) && (defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP)) + if (!a.dense_split && ggml_backend_is_cuda(backend) && !ggml_backend_cuda_buffer_is_managed(buf)) { + // HIP may pin pageable upload sources. Keep file-backed pages + // out of that path so completed-source cache advice can act. + if (!upload_copied_file_chunks(mmap.addr, mmap.len, a.file_offset, + a.file_size, upload_scratch, + [&](const uint8_t * bytes, size_t offset, size_t count) { + ggml_backend_tensor_set(a.tensor, bytes, offset, count); + })) { + set_last_error("invalid dense staged-upload source range"); + mmap.close_map(); + if (split_buf) ggml_backend_buffer_free(split_buf); + if (buf) ggml_backend_buffer_free(buf); + gguf_free(gctx); + ggml_free(meta_ctx); + return false; + } + } else +#endif + { + ggml_backend_tensor_set(a.tensor, src_data, 0, a.file_size); + } #if defined(__linux__) // set_tensor has completed its source copy, including split buffers. reclaim_copied_file_source(mmap.addr, mmap.len, src_data, a.file_size, @@ -1862,6 +1887,11 @@ bool load_deepseek4_gguf_partial(const std::string & path, if (emb_mmap.open_ro(path, emb_err)) { std::memcpy(out.embedder.tok_embd_owned.data(), (const char *)emb_mmap.addr + a.file_offset, a.file_size); +#if defined(__linux__) + reclaim_copied_file_source(emb_mmap.addr, emb_mmap.len, + (const char *)emb_mmap.addr + a.file_offset, a.file_size, + emb_mmap.fd, "token_embd.weight"); +#endif emb_mmap.close_map(); } else { set_last_error("embedder mmap: " + emb_err); diff --git a/server/test/test_moe_source_page_range.cpp b/server/test/test_moe_source_page_range.cpp index 42f579d63..51aee1afb 100644 --- a/server/test/test_moe_source_page_range.cpp +++ b/server/test/test_moe_source_page_range.cpp @@ -1,5 +1,6 @@ #include "../src/common/moe_source_page_range.h" #include "../src/common/copied_source_reclaim.h" +#include "../src/common/copied_source_upload.h" #include #include @@ -18,7 +19,56 @@ static void check(bool ok, const char * message) { if (!ok) { std::fprintf(stderr, "FAIL: %s\n", message); std::exit(1); } } +static void check_staged_upload() { + const size_t payload = 2 * COPIED_SOURCE_UPLOAD_CHUNK + 37; + const size_t prefix = 17; + std::vector source(prefix + payload + 19), scratch, destination(payload); + for (size_t i = 0; i < source.size(); ++i) + source[i] = static_cast((i * 37 + i / 4096) % 251); + size_t next = 0, calls = 0; + const uint8_t * scratch_base = nullptr; + auto upload = [&](const uint8_t * bytes, size_t offset, size_t count) { + check(offset == next, "staged destination offsets are contiguous and tensor-relative"); + check(count > 0 && count <= COPIED_SOURCE_UPLOAD_CHUNK && count <= payload - offset, + "staged chunk bounds"); + check(bytes == scratch.data(), "backend receives heap scratch"); + const uintptr_t address = reinterpret_cast(bytes); + const uintptr_t source_address = reinterpret_cast(source.data()); + check(address < source_address || address >= source_address + source.size(), + "backend never receives original source bytes"); + if (!scratch_base) scratch_base = bytes; + check(bytes == scratch_base, "one buffer reused for every chunk"); + std::memcpy(destination.data() + offset, bytes, count); + next += count; + ++calls; + }; + check(upload_copied_file_chunks(source.data(), source.size(), prefix, payload, scratch, upload), + "valid multichunk upload"); + check(next == payload && calls == 3, "two full chunks and short final chunk"); + check(std::memcmp(destination.data(), source.data() + prefix, payload) == 0, + "all uploaded bytes preserved"); + auto unexpected = [&](const uint8_t *, size_t, size_t) { check(false, "invalid/empty upload callback"); }; + check(upload_copied_file_chunks(source.data(), source.size(), source.size(), 0, scratch, unexpected), + "empty end span does not upload"); + check(!upload_copied_file_chunks(source.data(), source.size(), source.size() + 1, 0, scratch, unexpected), + "source offset past mapping rejected"); + check(!upload_copied_file_chunks(source.data(), source.size(), prefix, + std::numeric_limits::max(), scratch, unexpected), "overflowing source size rejected"); + check(!upload_copied_file_chunks(nullptr, 0, 0, 0, scratch, unexpected), "null mapping rejected"); + check(!upload_copied_file_chunks(source.data(), std::numeric_limits::max(), + 0, 1, scratch, unexpected), "mapping address overflow rejected"); + size_t small_calls = 0; + check(upload_copied_file_chunks(source.data(), source.size(), prefix, 29, scratch, + [&](const uint8_t * bytes, size_t offset, size_t count) { + check(offset == 0 && count == 29 && bytes == scratch_base, "scratch reused across tensors"); + check(std::memcmp(bytes, source.data() + prefix, count) == 0, "short tensor bytes preserved"); + ++small_calls; + }), "short next tensor upload"); + check(small_calls == 1, "short tensor uploaded once"); +} + int main() { + check_staged_upload(); constexpr size_t page = 4096; MoeSourcePageRange range; check(moe_source_page_range(page, 5 * page, page + 17, 3 * page + 100, page, range), "unaligned tensor valid"); @@ -92,6 +142,6 @@ int main() { #else std::puts("SKIP: Linux copied-source advice unavailable; bounds and mode checks passed"); #endif - std::puts("PASS: source-page bounds, mode exclusions and supported file-refault checks"); + std::puts("PASS: staged-copy bytes/bounds, source-page bounds, mode exclusions and supported file-refault checks"); return 0; } From 5dc0fdde1a1c0541d1f7f6348f8f238ab57608ba Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 20:57:59 -0400 Subject: [PATCH 072/123] Release cached GPU temporaries before image admission --- server/src/deepseek4/deepseek4_backend.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index 0d97cb634..c4f42cf5c 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -878,6 +878,22 @@ bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, if (spec_backend_) ggml_backend_synchronize(spec_backend_); deepseek4_release_image_scratch(cache_, moe_hybrid_.get()); reset_deepseek4_dspark_runtime_cache(); + // Gallocr teardown leaves operator temporaries in legacy CUDA/HIP pools. + // Retire their captured executables and activation memos through the + // backend API before measuring headroom for the next image request. + const auto trim_pool = [](ggml_backend_t owner, const char * name) { + const size_t released = ggml_backend_cuda_trim_pool(owner); + std::fprintf(stderr, + "[deepseek4] image transition pool trim: owner=%s released=%zu bytes\n", + name, released); + }; + trim_pool(backend_, "primary"); + if (expert_backend_ && expert_backend_ != backend_) { + trim_pool(expert_backend_, "expert"); + } + if (spec_backend_ && spec_backend_ != backend_ && spec_backend_ != expert_backend_) { + trim_pool(spec_backend_, "spec"); + } auto reserves = image_reserves_; const uint64_t resident_workspace = (vision::detail::hip_bias_launches(backend_) || vision::detail::hip_av_launches(backend_)) From dbaeb1e2912a5e361916f7b9469a1298233c0473 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 21:10:49 -0400 Subject: [PATCH 073/123] Trim cached GPU pools before bulk heterogeneous prefill --- server/src/deepseek4/deepseek4_graph.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index db42dd83a..629dfd946 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -7273,6 +7273,21 @@ bool deepseek4_step_layer_range( moe_hybrid->prefill_cold_alloc = nullptr; } } + // Gallocr teardown does not return cached operator temporaries to + // the driver. Retire backend captures/memos and trim free pool blocks + // before allocating the new bulk-prefill scratch on either owner. + const size_t primary_released = ggml_backend_cuda_trim_pool(backend); + std::fprintf(stderr, + "[deepseek4] bulk prefill pool trim: owner=primary released=%zu bytes\n", + primary_released); + if (moe_hybrid && moe_hybrid->cold_backend && + moe_hybrid->cold_backend != backend) { + const size_t cold_released = + ggml_backend_cuda_trim_pool(moe_hybrid->cold_backend); + std::fprintf(stderr, + "[deepseek4] bulk prefill pool trim: owner=cold released=%zu bytes\n", + cold_released); + } std::fprintf(stderr, "[deepseek4] released prior decode/tail arenas before " "new heterogeneous prefill\n"); From 5296836f66ffd64107136518e9d5e76983e1783a Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 21:38:45 -0400 Subject: [PATCH 074/123] Release bulk prefill arenas before ordinary hybrid decode --- server/src/deepseek4/deepseek4_backend.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index c4f42cf5c..f15fd560a 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -2538,7 +2538,11 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, if (timing) { log_step_tel("prefill", n_total, steps, elapsed_s(phase_t0), tel_acc); } - if (capture_spec) { + // AR decode also needs the completed bulk-prefill arenas retired before + // constructing its per-layer decode graphs. Restrict the new cleanup to + // bulk sparse hybrid prompts; preserve existing small-prompt reuse and + // speculative feature-capture cleanup. KV, HC mirrors and logits survive. + if (capture_spec || (bound_hybrid_scratch && n_total >= 512)) { deepseek4_release_prefill_scratch(cache_, moe_hybrid_.get()); } return pos; From 5fe8713071c172e075d8d52376c4acf4769eed0f Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 21:57:47 -0400 Subject: [PATCH 075/123] Bound paired decode attention cache to the current shape --- server/src/deepseek4/deepseek4_graph.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index 629dfd946..907e03be4 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -7683,7 +7683,13 @@ bool deepseek4_step_layer_range( candidate.index_flush == index_flush; }); if (it == per_layer.end()) { - if (per_layer.size() >= 20) { + // Compressed-row counts grow during AR decode. Retaining + // historical shapes multiplies context-sized attention + // arenas on a tightly packed paired-owner primary GPU. + const size_t cache_limit = moe_hybrid && + moe_hybrid->cold_backend && moe_hybrid->cold_backend != backend + ? 1 : 20; + while (per_layer.size() >= cache_limit) { per_layer.front().free(); per_layer.erase(per_layer.begin()); } From be48d1b0f8677462f7429e5cfe1bc434c67db38d Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 22:40:38 -0400 Subject: [PATCH 076/123] fix(deepseek4): trim returned pool blocks between long prefill chunks --- server/src/deepseek4/deepseek4_backend.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index f15fd560a..d74ab2461 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -2508,6 +2508,27 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, (size_t) final_new_rows); } } + // Completed chunks retain model/KV/HC state in backend buffers, not + // in the CUDA operator pool. Growing context-dependent temporaries + // can strand smaller legacy-pool blocks across successive chunks. + // Retire captured executables/memos and only returned pool blocks at + // this synchronized boundary; leave reusable gallocr arenas intact. + if (bound_hybrid_scratch && n_tok >= 512 && + kv_offset + n_total > 4096 && + moe_hybrid_->cold_backend && + moe_hybrid_->cold_backend != backend_) { + ggml_backend_synchronize(backend_); + ggml_backend_synchronize(moe_hybrid_->cold_backend); + const size_t primary_released = + ggml_backend_cuda_trim_pool(backend_); + const size_t cold_released = + ggml_backend_cuda_trim_pool(moe_hybrid_->cold_backend); + std::fprintf(stderr, + "[deepseek4] prefill chunk pool trim pos=%d " + "primary=%.2f MiB cold=%.2f MiB\n", + pos, primary_released / (1024.0 * 1024.0), + cold_released / (1024.0 * 1024.0)); + } } keep_spec_feature_tail(spec_feat_window_, (size_t) std::max(0, w_.n_swa)); From c76edfab1527340783b692a4ade2074d9473b969 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sat, 5 Sep 2026 23:20:05 -0400 Subject: [PATCH 077/123] fix(deepseek4): acknowledge every HC worker job generation --- server/src/deepseek4/deepseek4_graph.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index 907e03be4..e7cbaccf4 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -3598,7 +3598,14 @@ struct Ds4HcMatvecPool { if (stop.load(std::memory_order_relaxed)) return; last = s; const Job j = job; - if (i >= j.active_workers) continue; + if (i >= j.active_workers) { + // Every worker must acknowledge this generation before + // the caller can overwrite job or row_fn. Otherwise an + // inactive worker can pair an old seq with the next job + // and execute/decrement that next generation twice. + remaining.fetch_sub(1, std::memory_order_acq_rel); + continue; + } const int chunk = (j.rows + j.active_workers - 1) / j.active_workers; const int r0 = i * chunk; const int r1 = j.rows < r0 + chunk ? j.rows : r0 + chunk; @@ -3644,7 +3651,7 @@ struct Ds4HcMatvecPool { row_fn = nullptr; const int active_workers = std::min(nth, rows); job = {mat, x, out, rows, cols, active_workers}; - remaining.store(active_workers, std::memory_order_release); + remaining.store(nth, std::memory_order_release); { // Publish the new generation while holding wait_mu so a worker // cannot miss the transition between its predicate check and @@ -3669,7 +3676,7 @@ struct Ds4HcMatvecPool { row_fn = std::move(fn); const int active_workers = std::min(nth, rows); job = {nullptr, nullptr, nullptr, rows, 0, active_workers}; - remaining.store(active_workers, std::memory_order_release); + remaining.store(nth, std::memory_order_release); { std::lock_guard wake_lk(wait_mu); seq.fetch_add(1, std::memory_order_release); From 7e851cb81937fa92081d1b2e988af82b07d72575 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Sun, 6 Sep 2026 01:04:35 -0400 Subject: [PATCH 078/123] feat(ds4v): add bounded IQ85 quantization from original weights --- server/test/test_ds4_iq_converter.cpp | 212 +++++++++++++++++ server/tools/ds4_mix_converter/CMakeLists.txt | 12 +- server/tools/ds4_mix_converter/README.md | 68 ++++++ .../ds4_mix_converter/ds4_mix_converter.cpp | 29 +++ .../tools/ds4_mix_converter/expert_batches.h | 64 ++++++ .../ds4_mix_converter/iq85_converter.inc | 214 ++++++++++++++++++ server/tools/ds4_mix_converter/prove_iq85.py | 82 +++++++ 7 files changed, 679 insertions(+), 2 deletions(-) create mode 100644 server/test/test_ds4_iq_converter.cpp create mode 100644 server/tools/ds4_mix_converter/README.md create mode 100644 server/tools/ds4_mix_converter/expert_batches.h create mode 100644 server/tools/ds4_mix_converter/iq85_converter.inc create mode 100644 server/tools/ds4_mix_converter/prove_iq85.py diff --git a/server/test/test_ds4_iq_converter.cpp b/server/test/test_ds4_iq_converter.cpp new file mode 100644 index 000000000..639932c46 --- /dev/null +++ b/server/test/test_ds4_iq_converter.cpp @@ -0,0 +1,212 @@ +#include +#define main ds4_mix_converter_main +#include "../tools/ds4_mix_converter/ds4_mix_converter.cpp" +#undef main + +template void must_fail(F fn) { + bool rejected = false; + try { fn(); } catch (const std::exception &) { rejected = true; } + if (!rejected) fail("expected rejection"); +} + +void test_source_artifact() { + char pattern[]="/tmp/ds4-iq-source-XXXXXX"; + if(!::mkdtemp(pattern)) fail("mkdtemp failed"); + const fs::path root=pattern; + struct Cleanup {fs::path path; ~Cleanup(){std::error_code ec;fs::remove_all(path,ec);}} cleanup{root}; + json config; + for(const char * key:{"num_hidden_layers","num_attention_heads","num_key_value_heads","head_dim", + "qk_rope_head_dim","q_lora_rank","o_lora_rank","o_groups","num_experts_per_tok","n_shared_experts", + "moe_intermediate_size","num_hash_layers","sliding_window","index_n_heads","index_head_dim", + "index_topk","hc_mult","hc_sinkhorn_iters"}) config[key]=1; + config["n_routed_experts"]=2;config["hidden_size"]=256;config["vocab_size"]=2; + std::ofstream(root/"config.json")< payload; + auto add=[&](const std::string & name,const std::string & dtype,const std::vector& shape,std::vector bytes){ + const size_t start=payload.size();payload.insert(payload.end(),bytes.begin(),bytes.end()); + header[name]={{"dtype",dtype},{"shape",shape},{"data_offsets",{start,payload.size()}}}; + index["weight_map"][name]="model.safetensors"; + }; + std::vector bf16(1024); + for(size_t i=0;i<512;++i){uint16_t v=float_to_bf16(std::sin(float(i)));std::memcpy(bf16.data()+i*2,&v,2);} + add("embed.weight","BF16",{2,256},bf16);add("head.weight","BF16",{2,256},bf16); + for(const char * name:{"norm.weight","hc_head_base","hc_head_fn","hc_head_scale"}) + add(name,"F32",{4},std::vector(16)); + add("layers.0.attn.wq_a.weight","F8_E4M3",{2,256},std::vector(512,0x38)); + add("layers.0.attn.wq_a.scale","F8_E8M0",{1,2},{127,128}); + add("layers.0.ffn.gate.tid2eid","I64",{2},std::vector(16)); + add("vision.test.weight","BF16",{2,3},std::vector(12,0x3f)); + for(uint32_t e=0;e<2;++e) for(const auto & recipe:kExpertRecipes) { + auto packed=std::vector(256); + for(size_t i=0;i(16,127)); + } + std::ofstream(root/"model.safetensors.index.json")<()) fail("plan size differs from serialized artifact"); + const auto serial=read_file(options.output); + must_fail([&]{run_iq85(options,source,1,2);}); + if(read_file(options.output)!=serial) fail("existing artifact was overwritten"); + options.output=root/"parallel.gguf";options.encode_threads=8;run_iq85(options,source,1,2); + if(read_file(options.output)!=serial) fail("full original-source serial/parallel GGUF differs"); + // Already-open source metadata does not mask a later short read; no final artifact may appear. + fs::resize_file(root/"model.safetensors",8+len+payload.size()-1); + options.output=root/"short-read.gguf"; + must_fail([&]{run_iq85(options,source,1,2);}); + if(fs::exists(options.output)) fail("failed conversion published final artifact"); +} + +void test_fp8_dense_analytic() { + // Independent analytical decoding oracle: these E4M3 bytes represent exactly + // +1, -1, +2 and +0.5. E8M0 127/128 mean scale 1/2. Both tile axes cross 128. + constexpr size_t rows=129,cols=256; + const std::array codes={0x38,0xb8,0x40,0x30}; + const std::array decoded={1.0f,-1.0f,2.0f,0.5f}; + const std::array scales={127,128,128,127}; + std::vector payload(rows*cols); + std::vector expected_values(rows*cols); + for(size_t row=0;row=128) != (col>=128)) ? 2.0f : 1.0f; + expected_values[row*cols+col]=decoded[choice]*scale; + } + char pattern[]="/tmp/ds4-iq-fp8-XXXXXX"; + const int fd=::mkstemp(pattern); + if(fd<0) fail("FP8 fixture mkstemp failed"); + struct Cleanup {const char * path;~Cleanup(){::unlink(path);}} cleanup{pattern}; + if(::write(fd,payload.data(),payload.size())!=ssize_t(payload.size()) || + ::write(fd,scales.data(),scales.size())!=ssize_t(scales.size())) { + ::close(fd);fail("FP8 fixture write failed"); + } + ::close(fd); + StEntry weight;weight.name="layers.0.attn.wq_a.weight";weight.dtype="F8_E4M3"; + weight.path=pattern;weight.shape={rows,cols};weight.size=payload.size(); + StEntry scale;scale.name="layers.0.attn.wq_a.scale";scale.dtype="F8_E8M0"; + scale.path=pattern;scale.shape={2,2};scale.offset=payload.size();scale.size=scales.size(); + TensorSpec spec;spec.name="blk.0.attn_q_a.weight";spec.source=&weight;spec.scale=&scale; + spec.ne=reverse_shape(weight);spec.type=GGML_TYPE_Q8_0;spec.producer=Producer::DenseFp8; + std::unique_ptr out(std::tmpfile(),std::fclose); + if(!out) fail("FP8 output tmpfile failed"); + iq85_write_dense(out.get(),spec); + std::vector expected(ggml_row_size(GGML_TYPE_Q8_0,cols)*rows),actual(expected.size()); + if(ggml_quantize_chunk(GGML_TYPE_Q8_0,expected_values.data(),expected.data(),0,rows,cols,nullptr)!=expected.size()) + fail("analytical Q8 expected byte size mismatch"); + std::rewind(out.get()); + if(std::fread(actual.data(),1,actual.size(),out.get())!=actual.size() || actual!=expected || std::fgetc(out.get())!=EOF) + fail("FP8 Q8 differs from independent analytical source/scales oracle"); +} + +int main() { + try { + StEntry source; + source.name = "layers.0.attn.wq_a.weight"; + source.shape = {2,256}; + TensorSpec spec; + spec.source = &source; spec.type = GGML_TYPE_BF16; spec.ne = reverse_shape(source); + for (const char * name : {"blk.0.attn_q_a.weight", "output.weight", "token_embd.weight"}) { + spec.name = name; + if (!iq85_dense(spec)) fail("eligible dense tensor excluded"); + } + for (const char * name : {"blk.0.hc_attn_fn.weight", "blk.0.ffn_gate_inp.weight", + "blk.0.indexer.proj.weight", "blk.0.attn_compressor_kv.weight", "vision.blocks.0.attn.wqkv.weight"}) { + spec.name = name; + if (iq85_dense(spec)) fail("protected tensor quantized"); + } + spec.name = "output.weight"; source.shape={256}; spec.ne=reverse_shape(source); + if (iq85_dense(spec)) fail("vector quantized"); + + // Actual dense BF16 source -> Q8 bytes against canonical row encoding. + char pattern[] = "/tmp/ds4-iq-test-XXXXXX"; + const int fd = ::mkstemp(pattern); + if (fd < 0) fail("mkstemp failed"); + source.path = pattern; source.dtype = "BF16"; source.shape = {2,256}; source.size = 1024; + std::vector bf16(512); + std::vector values(512); + for (size_t i=0;i<512;++i) { bf16[i] = float_to_bf16(std::sin(float(i))*2); values[i] = bf16_to_float(bf16[i]); } + if (::write(fd,bf16.data(),1024)!=1024) fail("fixture write failed"); + ::close(fd); + spec.ne = {256,2}; spec.type = GGML_TYPE_Q8_0; spec.producer = Producer::Raw; + std::unique_ptr output(std::tmpfile(),std::fclose); + if (!output) fail("tmpfile failed"); + iq85_write_dense(output.get(),spec); + std::vector expected(ggml_row_size(GGML_TYPE_Q8_0,256)*2), actual(expected.size()); + ggml_quantize_chunk(GGML_TYPE_Q8_0,values.data(),expected.data(),0,2,256,nullptr); + std::rewind(output.get()); + if (std::fread(actual.data(),1,actual.size(),output.get())!=actual.size() || actual!=expected) + fail("source-to-Q8 bytes differ from canonical encoder"); + ::unlink(pattern); + + spec.producer=Producer::Expert; spec.name="blk.0.ffn_gate_exps.weight"; spec.ne={256,2,1}; + std::optional imatrix=Imatrix{{spec.name,{1,std::vector(256,1)}}}; + iq85_validate_importance({spec},imatrix,"uniform-unvalidated"); + must_fail([&]{iq85_validate_importance({spec},imatrix,"activation-derived");}); + std::fill(imatrix->at(spec.name).values.begin(),imatrix->at(spec.name).values.end(),0); + must_fail([&]{iq85_validate_importance({spec},imatrix,"uniform-unvalidated");}); + imatrix->at(spec.name).values.resize(256*3); + for(size_t i=0;i<256*3;++i)imatrix->at(spec.name).values[i]=float(i+1); + if(iq85_importance(imatrix,spec,2,3)[0]!=513) fail("wrong per-expert importance slice"); + must_fail([&]{iq85_importance(imatrix,spec,0,2);}); + must_fail([&]{iq85_importance(imatrix,spec,3,3);}); + + // Canonical IQ rows encoded concurrently must equal serial bytes, including + // nonuniform importance and a batch tail. Shared lookup tables are initialized first. + std::vector importance(256); + for(size_t i=0;i<256;++i) importance[i]=0.25f+float(i%13); + for(auto type:{GGML_TYPE_IQ2_XXS,GGML_TYPE_IQ2_XS}) { + ggml_quantize_init(type); + auto encode=[&](uint32_t e) { + auto v=values; for(auto & x:v) x+=float(e)*0.015625f; + ds4_mix_detail::EncodedExpert bytes(ggml_row_size(type,256)*2); + if(ggml_quantize_chunk(type,v.data(),bytes.data(),0,2,256,importance.data())!=bytes.size()) + fail("IQ size mismatch"); + return bytes; + }; + std::vector serial,parallel,parallel16; + auto writer=[](std::vector& out){return [&out](uint32_t,const auto & b){out.insert(out.end(),b.begin(),b.end());};}; + ds4_mix_detail::ordered_expert_batches(17,1,ggml_row_size(type,256)*2,encode,writer(serial)); + ds4_mix_detail::ordered_expert_batches(17,8,ggml_row_size(type,256)*2,encode,writer(parallel)); + ds4_mix_detail::ordered_expert_batches(17,16,ggml_row_size(type,256)*2,encode,writer(parallel16)); + if(serial!=parallel || serial!=parallel16) fail("IQ parallel output differs"); + } + must_fail([&]{ds4_mix_detail::checked_encoded_size(UINT64_MAX,2,16);}); + must_fail([&]{ds4_mix_detail::checked_encoded_size(1,1,17);}); + std::vector args={"converter","--input","/unused","--output","/unused/new.gguf", + "--recipe","iq85","--imatrix","/unused/importance.dat","--imatrix-provenance","uniform-unvalidated"}; + auto parse=[&] {std::vector argv;for(auto & arg:args)argv.push_back(arg.data());return parse_options(argv.size(),argv.data());}; + if(parse().encode_threads!=1) fail("default encode thread count changed"); + args.push_back("--encode-threads");args.push_back("16"); + if(parse().encode_threads!=16) fail("CLI rejects sixteen encoder workers"); + args.back()="17";must_fail([&]{parse();}); + unsigned writes=0; + must_fail([&]{ds4_mix_detail::ordered_expert_batches(17,8,1, + [](uint32_t e){if(e==3) fail("injected worker failure");return ds4_mix_detail::EncodedExpert(1);}, + [&](uint32_t,const auto&){++writes;});}); + if(writes!=3) fail("publication continued after worker failure"); + test_fp8_dense_analytic(); + test_source_artifact(); + std::cout<<"PASS: dense preservation/canonical encoding, imatrix provenance, IQ parallel determinism, failure bounds\n"; + return 0; + } catch(const std::exception& e) {std::cerr<<"FAIL: "< plan.json +``` + +Encoding uses canonical ggml IQ/Q8 encoders. `--encode-threads 1..16` (default 1) +parallelizes independent experts in ordered batches; lookup tables initialize +before workers launch. Each worker owns one encoded expert plus row scratch and +two source descriptors. With the actual 4096x2048 expert dimensions, encoded +payloads are 2.0625 MiB gate/up and 2.3125 MiB down, at most 37 MiB for sixteen +results. This excludes canonical IQ lookup/encoder scratch, source headers, +imatrix and thread stacks. Dense encoding uses one row plus its small FP8 scale +grid. No entire expert is expanded to F32. Worker failures drain launched jobs +before propagation. A fresh `.partial` path is exclusively created; complete +bytes/header and raw preserved tensors are verified before atomic no-overwrite +publication. Failed partial files remain for inspection and are never reused. + +Build/test on the authorized remote Linux CPU host only: + +```sh +cmake -S server/tools/ds4_mix_converter -B /absolute/fresh-build -DCMAKE_BUILD_TYPE=Release +cmake --build /absolute/fresh-build -j2 +ctest --test-dir /absolute/fresh-build --output-on-failure +``` + +`test_ds4_iq_converter` covers dense preservation, canonical source-to-Q8 bytes, +an independent analytical FP8/scaling fixture crossing 128-row/column boundaries, +importance rejection/provenance, canonical IQ serial/8/16-worker identity with +nonuniform weights and a batch tail, and allocation/worker failure bounds. +`prove_iq85.py` runs exactly one layer and 1–17 experts (default 8), using 1, 8, +then 8 workers by default. `--reference-threads 1|8` and +`--parallel-threads 8|16` allow a bounded 8/16/repeat16 comparison after the +serial/8 qualification. It uses fresh output paths, timeouts, exact plan-size checks, hashes +and whole-file identity. It records binary/imatrix/source-index hashes and does +not build, run GPU code or convert a full model. Synthetic and bounded source +tests do not establish text/image quality or long-context performance; those +require separate runtime qualification against the existing model. diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index d3245ae03..e28d7691c 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -1,6 +1,7 @@ #include "ggml.h" #include "gguf.h" #include "rocmfpx.h" +#include "expert_batches.h" #include @@ -551,6 +552,10 @@ struct LayerCalibration { }; struct Options { + std::string recipe = "mix"; + std::string imatrix_provenance; + bool plan_only = false; + unsigned encode_threads = 1; fs::path input; fs::path output; std::optional imatrix; @@ -566,6 +571,9 @@ struct Options { void usage(const char * argv0) { std::cerr << "Usage: " << argv0 << " --input DIR --output FILE (--imatrix FILE | --absmax-only)\n" << " [--layer-start N] [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force]\n"; + std::cerr << "IQ85: --recipe iq85 --imatrix FILE --imatrix-provenance " + << "uniform-unvalidated|activation-derived|transferred-text-calibration " + << "[--plan-only] [--encode-threads 1..16]; fresh output only\n"; } int parse_nonnegative(const char * value, const std::string & option, bool allow_zero = true) { @@ -587,6 +595,10 @@ Options parse_options(int argc, char ** argv) { return argv[i]; }; if (arg == "--input") out.input = value(); + else if (arg == "--recipe") out.recipe = value(); + else if (arg == "--imatrix-provenance") out.imatrix_provenance = value(); + else if (arg == "--plan-only") out.plan_only = true; + else if (arg == "--encode-threads") out.encode_threads = parse_nonnegative(value(), arg, false); else if (arg == "--output") out.output = value(); else if (arg == "--imatrix") out.imatrix = fs::path(value()); else if (arg == "--absmax-only") out.absmax_only = true; @@ -600,6 +612,17 @@ Options parse_options(int argc, char ** argv) { else fail("unknown option " + arg); } if (out.input.empty() || out.output.empty()) fail("--input and --output are required"); + if (out.recipe != "mix" && out.recipe != "iq85") fail("--recipe must be mix or iq85"); + if (out.encode_threads > 16) fail("--encode-threads must be in 1..16"); + if (out.recipe == "iq85") { + if (out.force) fail("iq85 never overwrites artifacts; use a fresh output path"); + if (out.absmax_only || !out.imatrix) fail("iq85 requires --imatrix; --absmax-only is unsupported"); + if (out.imatrix_provenance != "uniform-unvalidated" && out.imatrix_provenance != "activation-derived" && + out.imatrix_provenance != "transferred-text-calibration") + fail("iq85 requires --imatrix-provenance uniform-unvalidated|activation-derived|transferred-text-calibration"); + } else if (out.plan_only || out.encode_threads != 1 || !out.imatrix_provenance.empty()) { + fail("--plan-only, --encode-threads and --imatrix-provenance require --recipe iq85"); + } if (out.absmax_only == out.imatrix.has_value()) { fail("choose exactly one of --imatrix FILE or --absmax-only"); } @@ -1354,6 +1377,8 @@ void write_gguf(const Options & options, const SafeTensorSet & source, verify_artifact(options.output, gumix_path, plan, p4, gumix); } +#include "iq85_converter.inc" + } // namespace int main(int argc, char ** argv) { @@ -1373,6 +1398,10 @@ int main(int argc, char ** argv) { if (!options.experts_only && (layers != source_layers || experts != source_experts)) { fail("layer/expert limits are permitted only with --experts-only smoke artifacts"); } + if (options.recipe == "iq85") { + run_iq85(options, source, layers, experts); + return 0; + } if (options.validate_input_only) { const auto layout = validate_input_layout(source, layers, experts); const auto plan = make_plan(source, layout, experts, options.experts_only); diff --git a/server/tools/ds4_mix_converter/expert_batches.h b/server/tools/ds4_mix_converter/expert_batches.h new file mode 100644 index 000000000..f17542595 --- /dev/null +++ b/server/tools/ds4_mix_converter/expert_batches.h @@ -0,0 +1,64 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +// Internal scheduling only: no source, calibration, codec or FILE state lives here. +namespace ds4_mix_detail { +using EncodedExpert = std::vector; +inline size_t checked_encoded_size(uint64_t row_bytes, uint64_t rows, unsigned workers) { + if (workers < 1 || workers > 16) throw std::runtime_error("encode threads must be in 1..16"); + if (!row_bytes || !rows || row_bytes > std::numeric_limits::max()/rows) + throw std::runtime_error("invalid or overflowing encoded expert size"); + const uint64_t bytes = row_bytes*rows; + if (bytes > std::numeric_limits::max()/workers || + bytes*workers > std::numeric_limits::max()) + throw std::runtime_error("overflowing encoded batch size"); + return static_cast(bytes); +} +struct AsyncExpert { + template auto operator()(Task task) const { + return std::async(std::launch::async, std::move(task)); + } +}; +// Launch is injectable only to test failure after some tasks have started. +// All launched futures are explicitly consumed before any captured owner can die. +template +void ordered_expert_batches(uint32_t count, unsigned workers, size_t expert_bytes, + Encode encode, Write write, Launch launch = {}) { + checked_encoded_size(expert_bytes, 1, workers); + auto publish = [&](uint32_t expert, const EncodedExpert & bytes) { + if (bytes.size() != expert_bytes) throw std::runtime_error("encoded expert byte count mismatch"); + write(expert, bytes); + }; + if (workers == 1) { + for (uint32_t expert = 0; expert < count; ++expert) publish(expert, encode(expert)); + return; + } + for (uint32_t first = 0; first < count;) { + const unsigned batch = std::min(workers, count - first); + std::vector> futures; + futures.reserve(batch); // Allocation precedes all launches. + std::exception_ptr failure; + try { + for (unsigned i = 0; i < batch; ++i) { + const uint32_t expert = first + i; + futures.push_back(launch([&encode, expert] { return encode(expert); })); + } + } catch (...) { failure = std::current_exception(); } + for (unsigned i = 0; i < futures.size(); ++i) { + try { + EncodedExpert bytes = futures[i].get(); + if (!failure) publish(first + i, bytes); + } catch (...) { if (!failure) failure = std::current_exception(); } + } + if (failure) std::rethrow_exception(failure); + first += batch; // No next batch until every future was drained. + } +} +} // namespace ds4_mix_detail diff --git a/server/tools/ds4_mix_converter/iq85_converter.inc b/server/tools/ds4_mix_converter/iq85_converter.inc new file mode 100644 index 000000000..c9e8a1b2e --- /dev/null +++ b/server/tools/ds4_mix_converter/iq85_converter.inc @@ -0,0 +1,214 @@ +// Included inside the converter's anonymous namespace. Reuses only source decoding +// and metadata helpers; the existing MIX calibration/encoding path is unchanged. +bool iq85_dense(const TensorSpec & spec) { + if (!spec.source || spec.type != GGML_TYPE_BF16 || spec.source->shape.size() != 2 || + spec.ne.size() < 2 || spec.ne[0] % 32) return false; + if (spec.name == "token_embd.weight" || spec.name == "output.weight") return true; + const auto parsed = parse_layer_name(spec.source->name); + if (!parsed) return false; + static const std::set leaves = { + "attn_kv.weight", "attn_output_a.weight", "attn_output_b.weight", + "attn_q_a.weight", "attn_q_b.weight", "ffn_down_shexp.weight", + "ffn_gate_shexp.weight", "ffn_up_shexp.weight"}; + const size_t dot = spec.name.find('.', 4); + return dot != std::string::npos && leaves.count(spec.name.substr(dot + 1)); +} + +std::vector iq85_plan(const SafeTensorSet & source, + const std::vector & layout, uint32_t experts, bool smoke) { + auto plan = make_plan(source, layout, experts, smoke); + for (auto & spec : plan) { + if (spec.producer == Producer::Expert) { + spec.type = spec.recipe->surface == Surface::Down ? GGML_TYPE_IQ2_XS : GGML_TYPE_IQ2_XXS; + } else if (iq85_dense(spec)) { + spec.type = GGML_TYPE_Q8_0; + } + } + return plan; +} + +const float * iq85_importance(const std::optional & imatrix, + const TensorSpec & spec, uint32_t expert, uint32_t source_experts) { + if (!imatrix) fail("iq85 requires importance weights"); + const auto it = imatrix->find(spec.name); + if (it == imatrix->end()) fail("imatrix missing " + spec.name); + const size_t in = spec.ne[0]; + const size_t full = checked_mul(in, source_experts, "per-expert importance dimensions"); + const auto & values = it->second.values; + if (values.size() != in && values.size() != full) + fail("iq85 importance dimensions must be input width or input width * SOURCE expert count: " + spec.name); + if (expert >= source_experts) fail("iq85 importance expert out of range"); + return values.data() + (values.size() == in ? 0 : size_t(expert)*in); +} + +void iq85_validate_importance(const std::vector & plan, + const std::optional & imatrix, const std::string & provenance, uint32_t source_experts = 1) { + bool any_nonuniform = false; + for (const auto & spec : plan) { + if (spec.producer != Producer::Expert) continue; + for (uint32_t expert = 0; expert < uint32_t(spec.ne[2]); ++expert) { + const float * values = iq85_importance(imatrix, spec, expert, source_experts); + if (std::none_of(values, values + spec.ne[0], [](float x) { return x > 0; })) + fail("iq85 requires nonzero importance for every selected expert: " + spec.name); + any_nonuniform |= std::any_of(values, values + spec.ne[0], [&](float x) { return x != values[0]; }); + } + } + if (provenance != "uniform-unvalidated" && !any_nonuniform) + fail("all importance rows are uniform; label these uniform-unvalidated, not activation-derived"); + std::cerr << "[iq85] imatrix provenance=" << provenance + << "; this label is operator supplied, not a quality qualification\n"; +} + +void iq85_write_experts(FILE * out, const SafeTensorSet & source, const TensorSpec & spec, + uint32_t experts, const std::optional & imatrix, unsigned workers) { + const size_t row_bytes = ggml_row_size(spec.type, spec.ne[0]); + const size_t expert_bytes = ds4_mix_detail::checked_encoded_size(row_bytes, spec.ne[1], workers); + const uint32_t source_experts = config_u32(source.config(), "n_routed_experts"); + // Initialize shared, immutable IQ lookup tables before any worker launches. + ggml_quantize_init(spec.type); + ds4_mix_detail::ordered_expert_batches(experts, workers, expert_bytes, + [&](uint32_t expert) { + const float * importance = iq85_importance(imatrix, spec, expert, source_experts); + const auto shape = validate_expert_source(source, spec.layer, expert, *spec.recipe); + if (shape.in != spec.ne[0] || shape.out != spec.ne[1]) fail("iq85 expert shape drift"); + const auto & w = source.at(source_expert_name(spec.layer, expert, *spec.recipe, "weight")); + const auto & s = source.at(source_expert_name(spec.layer, expert, *spec.recipe, "scale")); + OpenTensorPair input(w, s); + std::vector packed, scales; + std::vector values; + ds4_mix_detail::EncodedExpert bytes(expert_bytes); + for (uint32_t row = 0; row < shape.out; ++row) { + decode_expert_row(input, row, shape.in, packed, scales, values); + const size_t n = ggml_quantize_chunk(spec.type, values.data(), bytes.data() + row*row_bytes, + 0, 1, shape.in, importance); + if (n != row_bytes) fail("iq85 encoder returned wrong row size"); + } + return bytes; + }, [&](uint32_t expert, const ds4_mix_detail::EncodedExpert & bytes) { + fwrite_exact(out, bytes.data(), bytes.size(), spec.name); + std::cerr << "[iq85 encode] " << spec.name << " expert " << expert + 1 << '/' << experts << '\n'; + }); +} + +void iq85_write_dense(FILE * out, const TensorSpec & spec) { + const auto & w = *spec.source; + const size_t cols = spec.ne[0], rows = spec.ne[1]; + FileDescriptor wf(w.path); + std::vector values(cols); + std::vector input(cols * (spec.producer == Producer::DenseFp8 ? 1 : 2)); + std::vector scales; + if (spec.producer == Producer::DenseFp8) { + FileDescriptor sf(spec.scale->path); + scales.resize(spec.scale->size); + pread_exact(sf.fd, scales.data(), scales.size(), spec.scale->offset, spec.scale->name); + } else if (w.dtype != "BF16") fail("iq85 unsupported dense source " + w.name); + const size_t row_bytes = ggml_row_size(GGML_TYPE_Q8_0, cols); + std::vector encoded(row_bytes); + for (size_t row = 0; row < rows; ++row) { + pread_exact(wf.fd, input.data(), input.size(), w.offset + row*input.size(), w.name); + for (size_t col = 0; col < cols; ++col) { + if (spec.producer == Producer::DenseFp8) { + values[col] = fp8_e4m3fn(input[col]) * fp8_e8m0(scales[(row/128)*spec.scale->shape[1] + col/128]); + } else { + uint16_t b; + std::memcpy(&b, input.data() + col*2, 2); + values[col] = bf16_to_float(b); + } + if (!std::isfinite(values[col])) fail("non-finite iq85 dense source " + w.name); + } + if (ggml_quantize_chunk(GGML_TYPE_Q8_0, values.data(), encoded.data(), 0, 1, cols, nullptr) != row_bytes) + fail("Q8 encoder returned wrong row size"); + fwrite_exact(out, encoded.data(), encoded.size(), spec.name); + } +} + +void run_iq85(const Options & options, const SafeTensorSet & source, uint32_t layers, uint32_t experts) { + const auto layout = validate_input_layout(source, layers, experts); + const auto plan = iq85_plan(source, layout, experts, options.experts_only); + const std::optional imatrix = load_imatrix(*options.imatrix); + iq85_validate_importance(plan, imatrix, options.imatrix_provenance, config_u32(source.config(), "n_routed_experts")); + std::unique_ptr ctx(gguf_init_empty(), gguf_free); + if (!ctx) fail("gguf_init_empty failed"); + set_model_metadata(ctx.get(), source, layers, experts, false, options.experts_only, {}); + for (const char * key : {"deepseek4.p4mix.sidecar", "deepseek4.mix.calibration", + "deepseek4.mix.lower_quality_absmax_only", "deepseek4.mix.experts_only_smoke_artifact"}) + gguf_remove_key(ctx.get(), key); + gguf_set_val_str(ctx.get(), "general.name", "DeepSeek-V4-Flash-Vision iq85 experimental"); + gguf_set_val_u32(ctx.get(), "general.file_type", GGML_FTYPE_MOSTLY_IQ2_XXS); + gguf_set_val_str(ctx.get(), "deepseek4.quant.recipe", "iq85-v1: gate/up IQ2_XXS; down IQ2_XS; selected dense Q8_0"); + gguf_set_val_str(ctx.get(), "deepseek4.quant.imatrix_provenance", options.imatrix_provenance.c_str()); + gguf_set_val_str(ctx.get(), "deepseek4.quant.imatrix_source", options.imatrix->filename().c_str()); + gguf_set_val_bool(ctx.get(), "deepseek4.quant.quality_validated", false); + gguf_set_val_bool(ctx.get(), "deepseek4.quant.experts_only_smoke_artifact", options.experts_only); + std::vector> descriptors; + uint64_t data_bytes = 0; + json rows = json::array(); + for (const auto & spec : plan) { + uint64_t n = 1; + for (auto dim : spec.ne) { + if (dim <= 0) fail("iq85 invalid tensor dimension"); + n = checked_mul(n, dim, "iq85 descriptor element count"); + } + if (n > size_t(-1)/ggml_type_size(spec.type)) fail("iq85 tensor exceeds host size bounds"); + descriptors.push_back(make_tensor_descriptor(spec)); + gguf_add_tensor(ctx.get(), descriptors.back().get()); + const int64_t id = gguf_find_tensor(ctx.get(), spec.name.c_str()); + const uint64_t offset = gguf_get_tensor_offset(ctx.get(), id), bytes = gguf_get_tensor_size(ctx.get(), id); + if (bytes > UINT64_MAX - offset) fail("iq85 plan size overflow"); + data_bytes = offset + bytes; + rows.push_back({{"name",spec.name},{"type",int(spec.type)},{"bytes",bytes}}); + } + const uint64_t meta_bytes = gguf_get_meta_size(ctx.get()); + if (data_bytes > UINT64_MAX - meta_bytes) fail("iq85 file size overflow"); + const uint64_t total = meta_bytes + data_bytes; + std::cout << json({{"recipe","iq85-v1"},{"file_bytes",total},{"decimal_GB",double(total)/1e9}, + {"metadata_bytes",meta_bytes},{"tensor_bytes_with_padding",data_bytes}, + {"imatrix_provenance",options.imatrix_provenance},{"quality_validated",false}, + {"experts_only",options.experts_only},{"tensors",rows}}).dump(2) << '\n'; + if (options.plan_only || options.validate_input_only) return; + if (fs::exists(options.output) || fs::exists(options.output.string()+".gumix.bin")) fail("iq85 output exists"); + if (!options.output.parent_path().empty()) fs::create_directories(options.output.parent_path()); + const fs::path temporary = options.output.string() + ".partial"; + const int fd = ::open(temporary.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0600); + if (fd < 0) fail("cannot exclusively create " + temporary.string()); + FILE * raw = ::fdopen(fd, "wb"); + if (!raw) { ::close(fd); fail("fdopen failed"); } + std::unique_ptr out(raw, std::fclose); + std::vector metadata(meta_bytes); + gguf_get_meta_data(ctx.get(), metadata.data()); + fwrite_exact(out.get(), metadata.data(), metadata.size(), "iq85 metadata"); + std::array zero{}; + for (const auto & spec : plan) { + const int64_t id = gguf_find_tensor(ctx.get(), spec.name.c_str()); + const uint64_t expected = meta_bytes + gguf_get_tensor_offset(ctx.get(), id); + const off_t position = ::ftello(out.get()); + if (position < 0 || uint64_t(position) > expected || expected - position >= kAlignment) + fail("iq85 stream offset mismatch"); + fwrite_exact(out.get(), zero.data(), expected-position, "iq85 padding"); + if (spec.producer == Producer::Expert) iq85_write_experts(out.get(), source, spec, experts, imatrix, options.encode_threads); + else if (spec.type == GGML_TYPE_Q8_0) iq85_write_dense(out.get(), spec); + else if (spec.producer == Producer::Raw) copy_raw(out.get(), *spec.source); + else if (spec.producer == Producer::DenseFp8) write_dense_fp8(out.get(), *spec.source, *spec.scale); + else if (spec.producer == Producer::Int64ToInt32) write_int64_to_int32(out.get(), *spec.source); + else fail("iq85 unknown producer"); + const off_t after = ::ftello(out.get()); + if (after < 0 || uint64_t(after) != expected + gguf_get_tensor_size(ctx.get(), id)) fail("iq85 producer size mismatch"); + } + if (std::fflush(out.get()) || ::fsync(::fileno(out.get()))) fail("iq85 flush/fsync failed"); + if (std::fclose(out.release())) fail("iq85 close failed"); + // Validate the partial file BEFORE publication; link is atomic and refuses overwrite. + gguf_init_params params = {true, nullptr}; + std::unique_ptr parsed(gguf_init_from_file(temporary.c_str(), params), gguf_free); + if (!parsed || fs::file_size(temporary) != total || gguf_get_n_tensors(parsed.get()) != int64_t(plan.size())) + fail("iq85 completed file size/header verification failed"); + FileDescriptor input(temporary); + for (const auto & spec : plan) { + const int64_t id = gguf_find_tensor(parsed.get(), spec.name.c_str()); + if (id < 0 || gguf_get_tensor_type(parsed.get(), id) != spec.type) fail("iq85 verification type mismatch"); + if (spec.producer == Producer::Raw && spec.type != GGML_TYPE_Q8_0) + compare_raw_passthrough(input.fd, gguf_get_data_offset(parsed.get()) + gguf_get_tensor_offset(parsed.get(), id), *spec.source); + } + if (::link(temporary.c_str(), options.output.c_str())) fail("iq85 atomic no-overwrite publication failed"); + if (::unlink(temporary.c_str())) fail("iq85 published but partial link cleanup failed"); + std::cerr << "[iq85 done] verified/published " << total << " bytes (" << double(total)/1e9 << " decimal GB)\n"; +} diff --git a/server/tools/ds4_mix_converter/prove_iq85.py b/server/tools/ds4_mix_converter/prove_iq85.py new file mode 100644 index 000000000..659618cf5 --- /dev/null +++ b/server/tools/ds4_mix_converter/prove_iq85.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Bounded CPU-only original-source IQ85 pilot; never converts a full model.""" +import argparse +import hashlib +import json +import pathlib +import subprocess +import sys +import time + + +def sha256(path): + h = hashlib.sha256() + with path.open("rb") as f: + for chunk in iter(lambda: f.read(8 * 1024 * 1024), b""): + h.update(chunk) + return h.hexdigest() + + +def main(): + p = argparse.ArgumentParser(description=__doc__) + p.add_argument("--binary", type=pathlib.Path, required=True) + p.add_argument("--input", type=pathlib.Path, required=True) + p.add_argument("--imatrix", type=pathlib.Path, required=True) + p.add_argument("--imatrix-provenance", choices=["uniform-unvalidated", "activation-derived", "transferred-text-calibration"], required=True) + p.add_argument("--output-dir", type=pathlib.Path, required=True) + p.add_argument("--experts", type=int, default=8, choices=range(1, 18)) + p.add_argument("--reference-threads", type=int, default=1, choices=[1, 8]) + p.add_argument("--parallel-threads", type=int, default=8, choices=[8, 16]) + p.add_argument("--timeout", type=int, default=14400) + a = p.parse_args() + if sys.platform != "linux": + p.error("run only on the authorized Linux CPU host") + if not 1 <= a.timeout <= 28800: + p.error("timeout must be 1..28800 seconds per lane") + for name in ("binary", "input", "imatrix", "output_dir"): + value = getattr(a, name) + if not value.is_absolute(): + p.error(f"--{name.replace('_','-')} must be absolute") + a.output_dir.mkdir(parents=False, exist_ok=False) + manifest = {"recipe": "iq85-v1", "quality_validated": False, + "imatrix_provenance": a.imatrix_provenance, + "binary_sha256": sha256(a.binary), "imatrix_sha256": sha256(a.imatrix), + "source_index_sha256": sha256(a.input / "model.safetensors.index.json"), "lanes": []} + common = [str(a.binary), "--input", str(a.input), "--recipe", "iq85", + "--imatrix", str(a.imatrix), "--imatrix-provenance", a.imatrix_provenance, + "--layer-count", "1", "--expert-limit", str(a.experts), "--experts-only"] + def save(): + (a.output_dir / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") + save() + reference_label = "serial" if a.reference_threads == 1 else "reference8" + for label, threads in ((reference_label, a.reference_threads), + (f"parallel{a.parallel_threads}", a.parallel_threads), + (f"repeat{a.parallel_threads}", a.parallel_threads)): + artifact = a.output_dir / (label + ".gguf") + command = common + ["--output", str(artifact), "--encode-threads", str(threads)] + start = time.monotonic() + lane = {"label": label, "command": command} + manifest["lanes"].append(lane) + save() + try: + with (a.output_dir / (label + ".plan.json")).open("w") as out, (a.output_dir / (label + ".log")).open("w") as err: + result = subprocess.run(command, stdout=out, stderr=err, timeout=a.timeout, check=False) + lane.update(exit_code=result.returncode, seconds=time.monotonic()-start) + if result.returncode: + save() + raise RuntimeError(f"{label} failed: exit {result.returncode}") + lane.update(bytes=artifact.stat().st_size, sha256=sha256(artifact)) + plan = json.loads((a.output_dir / (label + ".plan.json")).read_text()) + if lane["bytes"] != plan["file_bytes"]: + raise RuntimeError("plan byte count differs from output") + finally: + save() + manifest["byte_identical"] = len({lane["sha256"] for lane in manifest["lanes"]}) == 1 + save() + if not manifest["byte_identical"]: + raise RuntimeError("reference/parallel/repeat whole-file hashes differ") + print("PASS: bounded pilot exact plan sizes and reference/parallel/repeat whole-file identity; quality remains unvalidated") + + +if __name__ == "__main__": + main() From 39c079180c0d2fd3ef33526734698f1d158843a7 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Thu, 10 Sep 2026 04:33:05 -0400 Subject: [PATCH 079/123] docs(ds4v): record vision continuation status, decision log and IQ85 evidence Adds the operator handoff for the DeepSeek V4 Flash Vision work: the continuation status authority, the DS4V vision plan it supersedes, the four decision logs for the DS4V-1/2/3 and continuation passes, and the IQ85 candidate evidence summary describing conversion output, trials and the unmet quality/speed gates. No code changes. --- artifacts/ds4v-quant85/README.md | 67 ++++++++ decisions-ds4v-1.tsv | 10 ++ decisions-ds4v-2.tsv | 2 + decisions-ds4v-3.tsv | 5 + decisions-ds4v-continuation.tsv | 24 +++ docs/ds4v-continuation-status.md | 82 ++++++++++ docs/ds4v-uncensored-vision-plan.md | 242 ++++++++++++++++++++++++++++ 7 files changed, 432 insertions(+) create mode 100644 artifacts/ds4v-quant85/README.md create mode 100644 decisions-ds4v-1.tsv create mode 100644 decisions-ds4v-2.tsv create mode 100644 decisions-ds4v-3.tsv create mode 100644 decisions-ds4v-continuation.tsv create mode 100644 docs/ds4v-continuation-status.md create mode 100644 docs/ds4v-uncensored-vision-plan.md diff --git a/artifacts/ds4v-quant85/README.md b/artifacts/ds4v-quant85/README.md new file mode 100644 index 000000000..36162ff2a --- /dev/null +++ b/artifacts/ds4v-quant85/README.md @@ -0,0 +1,67 @@ +# DS4V 80–85 GB candidate + +The requested target is an 80–85 GB model and at least 35 tokens/s for short chat, while retaining image input and the previously qualified 131072-token context capacity. **The 83.62 GB conversion is complete, but the candidate has failed the quality gate and reached only 20.1 tokens/s median. It is not qualified for daily service.** The original c76 service has been restored and verified at `http://127.0.0.1:8016/v1` on soulf; neither reduced model nor experimental runtime was installed. + +Conversion on `soulf` started 2026-09-06 at 05:07:59 UTC and completed successfully in 1 hour 41 minutes, including wrapper verification. The completed file is **83,619,648,416 bytes**, SHA256 `954433dcb2e64ce6082f4ea8c1478428198fd81b6e7ec0729e9eefa3d56f8497`. Full conversion receipts, completed-header checks and raw trial evidence are stored beside this document. + +## Candidate and conversion evidence + +- Verified complete file: **83,619,648,416 bytes / 83.619648416 decimal GB**. Original file: 113,745,874,400 bytes. This saves 30.126225984 GB, approximately 26.5% of the original file size. +- Expert gate/up: IQ2_XXS; expert down: IQ2_XS. Selected dense matrices: Q8_0. Vision, aligner, routing, normalization and related control tensors retain their source representations. +- Conversion reads the original FP4/FP8/BF16 safetensors directly. It does not requantize the older MIX GGUF. +- Importance calibration is explicitly **transferred text calibration**, not DS4V-specific calibration. The smaller candidate needs actual regression and image tests; byte correctness alone does not establish model quality. +- Source commit: `7e851cb81937fa92081d1b2e988af82b07d72575`. Converter SHA256: `afa56b0a60e7f883091ed669f8bad01439f9789fa7835184f736563075f84533`. +- Small conversion pilots were byte-identical across serial/parallel execution. The 17-expert pilot also verified the partial final worker batch with 8 and 16 workers. +- Full job limits: 16 CPU workers, 6 GiB cgroup memory maximum, no cgroup swap, six-hour runtime maximum. No GPU benchmarks run during conversion. +- Completed-file header verification passed: all 11 tokenizer keys and 33 architecture keys are identical; all 1641 tensor names/dimensions match. The 129 expert tensors and 346 selected dense tensors follow the new quantization policy; 1166 tensors preserve their type and encoded byte count. The parser's 23 CPU tests passed on soulf. Header validation and the separate full checksum do not establish model quality. + +Remote final target: `/home/marcelorm/ds4v-work/DeepSeek-V4-Flash-Vision-IQ85-v1.gguf`. +Remote conversion evidence: `/home/marcelorm/ds4v-work/image-integration/quant85-full-v1/`. + +## Frozen comparison and runtime sequence + +The existing model scored **15/16 for answer content and 7/16 for strict JSON formatting** on the fixed small task set. Raw responses are in `quant85-quality-baseline-v1/`; the content-scoring rubric was frozen before candidate inference. The candidate gate requires retaining every baseline-correct answer and at least the baseline strict-format score. This is a narrow regression check, not a broad quality benchmark. + +Three completed trials used the same candidate and unchanged c76 runtime: + +| Decoding | Expert cap | Actual hot experts/layer | Text tokens per sample | Median text decode | Median image decode | Content / strict score | +|---|---:|---:|---|---:|---:|---| +| AR | 1024 MiB | 3 | 243 / 245 / 261 | 14.7 t/s | 14.9 t/s | 12/16 / 5/16 | +| Fused AR | 1024 MiB | 3 | 239 / 251 / 263 | 20.1 t/s | 20.3 t/s | 12/16 / 5/16 | +| Fused AR | 4096 MiB | 14 | 267 / 247 / 253 | 20.1 t/s | 20.4 t/s | 12/16 / 5/16 | + +All 18 original functional cases passed in each trial, including image ordering and follow-ups. The appended quality gate then failed: `python-copy` and `nested-json` answered incorrectly, and `python-slice` gave the correct array inside an unrequested Markdown fence. The existing failing `python-loop` case remained wrong. No rubric was relaxed. Cache-eviction, SSE and long-context stages were **not run**, because the functional quality gate stopped each trial first. + +Each owned model exited cleanly with no safety breach, OOM or global swap-out growth recorded by the supervisor. Each post-stop idle-memory recovery timed out; the separately authorized bounded TTM cleanup and fresh admission were required between trials. These failed overall reports remain intact. No candidate was installed. + +The second candidate restores only the original BF16 embedding and output matrices while retaining the other 1639 IQ85 payloads byte-for-byte. Assembly and independent verification completed: **84,612,519,168 bytes**, SHA256 `99a2260c862e270fa654a1f1e75fad88ec824c58963a2c30135ba91edaf9bb2b`. Its first fused-AR trial passed arithmetic and color-image ordering/follow-ups, then failed the corn/carrot response-format assertion: correct labels appeared inside prose and a Markdown fence. This preserved failure stopped the trial before sustained throughput and the 16-task quality comparison. No sustained speed or quality improvement is established for this variant. The failed trial exited without a safety breach; bounded idle TTM recovery was again needed and completed separately. + +A separately versioned diagnostic probe captured benchmark and quality evidence without converting that failed functional result into a pass. The IOBF16 AR diagnostic completed with **19.9 tokens/s median text decode** (252/240/256 output tokens) and **20.1 tokens/s median image decode** (182/143/139 output tokens). Its content/strict scores remained **12/16 and 5/16**, with the same three baseline regressions. Restoring the two BF16 matrices therefore showed no quality or speed benefit on these checks. Raw evidence is in `quant85-candidate-iobf16-diagnostic-ar-v1/`. This new diagnostic uses identical fixed requests across its forthcoming AR/reference/batched comparisons; it is not paired with the older randomized benchmarks. + +The existing 10.65 GB draft completed its controlled reference diagnostic capture: text median **8.3 tokens/s**, image median **20.1 tokens/s**, and the same **12/16 content, 5/16 strict** scores. All 22 visible answers and their reported completion counts matched the paired AR run on identical requests (`quant85-iobf16-ar-reference-visible-comparison.json`). This is visible-text agreement, not token-ID parity: the HTTP API does not expose token IDs. No speculative runtime qualification is established. The target plus draft would exceed 85 GB of combined model storage; the target model itself remains below 85 GB. Batched verification completed at **14.0 tokens/s text**, **20.0 tokens/s image**, and **13/16 content, 6/16 strict**. Its text answers differ from the paired AR/reference lane, so this is not a verified equivalent speedup. + +A timing-enabled control reproduced all 22 visible answers and completion counts at the same 14.0 tokens/s text median. Increasing graph cache slots from two to four reduced measured graph build time from approximately 23–29 ms to 10–11 ms per verification step, while compute remained approximately 104–107 ms. Text throughput rose to **16.8 tokens/s**, still below AR and the 35 tokens/s target. All 16 quality answers matched the two-slot lane, but all three benchmark text answers changed. See `quant85-iobf16-cache-visible-comparison.json`; numerical or token-ID parity is not established. + +An isolated scoped Q4 MMVQ diagnostic was built on soulf at source commit `7fa6ad3ee6892c9b60faabba8159a252c6aac704`. It reproduced the old release binary and original graph object before compiling the patch; CPU unit tests and nine preparer tests passed. The existing source, build and release were unchanged. Its guarded GPU diagnostic completed at **15.8 tokens/s text** and **20.3 tokens/s image**, with the same **13/16 content and 6/16 strict** scores. This did not improve performance. Policy activation was logged, but no direct kernel-dispatch trace was captured. The model exited cleanly with no safety breach or observed global swap-out growth; idle TTM recovery was performed separately. This build is not qualified or installed. + +A source review found a separate correctness defect in cached speculative attention: preserved ring-row views used construction-time offsets while runtime write indices advanced. The isolated fix replaces those views with a gather driven by refreshed indices. Existing CPU units and ten preparer tests passed. Real guarded cache2/cache4 tests now produced **identical visible text and completion counts for all 22 identical requests**, compared with 19/22 before the fix. This verifies removal of the observed cache-size-dependent divergence on this set, not broad numerical parity. The fixed cache2/cache4 text medians were **14.1/16.2 tokens/s**; both retained the failing **13/16 content, 6/16 strict** scores. Only 16/22 visible replies match the earlier AR-equivalent reference lane. See `fused-preserved-ring-offset.patch`, its evidence notes, and `quant85-iobf16-ring-cache2-cache4-visible-comparison.json`. The fix remains isolated and is not installed in daily service. + +**No tested 80–85 GB candidate met the 35 tokens/s and quality requirements.** Candidate cache-eviction, SSE and long-context qualification were not run after the failed quality gate. The original c76 service is restored and verified; the smaller files and all failure evidence are retained. + +A future candidate must retain quality and meet the short-chat speed target, then complete cache-eviction, SSE, 8K/32K/64K/124K text/image qualification and actual daily-service acceptance. Every model trial retains strict admission and the original service fallback. + +The fixed cache sequence is 2K, 4K, 8K, 16K, 2K in one server process; it is separate from short throughput medians and from 124K qualification. The validation wrapper and pinned configuration generator passed CPU tests on soulf; these tests do not establish GPU or model behavior. + +See [PREPARATION.md](PREPARATION.md) for exact unchanged guard requirements and [runtime-performance-review.md](runtime-performance-review.md) for source-backed trial settings. [draft-compatibility.md](draft-compatibility.md) assesses an optional speculative draft, which was exercised only in unqualified candidate diagnostics and adds approximately 10.65 GB of model storage. Current image requests do not use that speculative path. + +Reducing file size alone does not establish 35 tokens/s. Report sustained generated-token counts, actual decode timings, image and context results, and memory observations before adopting the candidate as the daily service. + +## Final original-service restoration + +Restoration passed on 2026-09-06 after the final diagnostic exited and bounded idle cleanup reached the existing startup threshold. Exact runtime, source/config pins, arguments, environment, both GPU devices, namespace isolation, loopback listener and process identity passed the existing acceptance checker before and after workloads. The service remains active as PID `201518`, invocation `896aae1d96fb4f958323470b9d1e4508`, with zero restarts. + +All **14 text/image functional cases** and **four edge cases** passed. A fresh oversized request was rejected with HTTP 400; real SSE cancellation correlated with `finish=client_disconnect` in the same service invocation, followed by a successful request in **0.83 seconds**. The restored service generated **269 text tokens at 13.8 tokens/s** and **251 image-response tokens at 13.7 tokens/s** in this acceptance run. These are single samples, not paired medians against the smaller-model benchmarks. + +The service advertises 131072 total context tokens and 4096 default output tokens. The prior 124K text/image qualification for this unchanged c76 runtime and original model remains the relevant long-context evidence; it was not repeated during restoration. Final snapshots show zero owned-process VmSwap, OOM kills and kernel taint. The system-wide swap-out counter grew by 26,681,344 bytes during restoration; that is not attributed to this process and is not reported as zero. + +Full restoration evidence and hash-bound summary: `quant85-original-restoration-v1/acceptance.json`. The original model remains 113.75 GB. The size target was achieved experimentally at 83.62 GB, but **35 tokens/s with retained quality was not achieved**. diff --git a/decisions-ds4v-1.tsv b/decisions-ds4v-1.tsv new file mode 100644 index 000000000..2f8553d7d --- /dev/null +++ b/decisions-ds4v-1.tsv @@ -0,0 +1,10 @@ +2026-09-04T06:12Z Created branch ds4v/baseline at origin/main 298031aa4222ec61c971ed834ec8f8829ce37a5c via git branch plus symbolic-ref Plain git checkout -b aborted because the sparse checkout keeps staged deletions; repointing HEAD avoids touching index or worktree +2026-09-04T06:21Z Authored docs/ds4v-baseline.md with PR 604 and blog numbers, no long dashes, no prose colons Task step 3 +2026-09-04T06:21Z Authored scripts/ds4v-baseline.sh with launch, doctor, chat-smoke, spec-flag, cleanup Task step 4 +2026-09-04T06:22Z Fixed spec-flag jq reading, replaced fallback operator with has() test because jq collapses JSON false to the fallback Found by stub server test returning spec_decode_ran false +2026-09-04T06:22Z Validated doctor, chat-smoke, spec-flag, cleanup against local stub servers on ports 8216 and 8217, bash -n, help path, missing-file path, unknown subcommand path Task step 5 +2026-09-04T06:23Z Committing only docs/ds4v-baseline.md and scripts/ds4v-baseline.sh via pathspec commit Sparse checkout index holds staged deletions that must not enter the commit +2026-09-04T06:26Z Committed f27aefc on ds4v/baseline with only the two assigned files, pathspec commit kept sparse checkout staged deletions out Task step 6 +2026-09-04T06:26Z git push to origin denied, 403 for marcelormendes on both SSH and HTTPS, saved format-patch to /tmp/ds4v-program/ds4v-1.patch Task step 6 fallback +2026-09-04T06:28Z Asked supervisor, chose between fork push PR and patch only handoff Supervisor approved fork route +2026-09-04T06:29Z Pushed ds4v/baseline to fork marcelormendes/lucebox and opened ready PR 695 against Luce-Org/lucebox main Supervisor decision, option A diff --git a/decisions-ds4v-2.tsv b/decisions-ds4v-2.tsv new file mode 100644 index 000000000..33b38885e --- /dev/null +++ b/decisions-ds4v-2.tsv @@ -0,0 +1,2 @@ +2026-09-04T06:27:03Z created decisions log for DS4V-2 owner pass, kept uncommitted by design +2026-09-04T06:29:42Z manifest-auth HF token absent and parent repo is gated, supervisor picked optional HF_TOKEN bearer plus public API shard listing and counts pending on operator machine diff --git a/decisions-ds4v-3.tsv b/decisions-ds4v-3.tsv new file mode 100644 index 000000000..b4b6a8cb2 --- /dev/null +++ b/decisions-ds4v-3.tsv @@ -0,0 +1,5 @@ +2026-09-04T07:05Z branch created ds4v/quant from origin/main 298031aa4222ec61c971ed834ec8f8829ce37a5c via git branch plus symbolic-ref, sparse checkout staged deletions left in index untouched +2026-09-04T07:20Z verified-tool-surface read master llama-quantize usage and src/llama-quant.cpp from github, confirmed --tensor-type takes regex tensor name plus ggml type, so the recipe uses per class overrides instead of multi pass requant +2026-09-04T07:45Z proof bash -n clean, help and plan exit 0 printing pin 9400c894, verify --help exit 0, jq empty clean, python budget recompute shows IQ2_XXS at 104.0 GiB with 48.0 margin against 152 and 92.7 on one 96 GiB card at 1M context, dash grep clean on all three files Task step 6 +2026-09-04T07:52Z stub-tests stub llama.cpp proved b10763 refusal exit 1, pinned 9400c894 run executes the exact quantize command, verify passes at 43 bias_vl and fails at 42 Task step 6 +2026-09-04T07:55Z commit committing only docs/ds4v-quant.md, scripts/ds4v-quant.sh, share/model_cards/ds4v-vision.json via pathspec, sparse checkout staged deletions and DS4V-1 plus DS4V-2 staged files must stay out Task step 7 diff --git a/decisions-ds4v-continuation.tsv b/decisions-ds4v-continuation.tsv new file mode 100644 index 000000000..257002bdb --- /dev/null +++ b/decisions-ds4v-continuation.tsv @@ -0,0 +1,24 @@ +ts phase decision why evidence result +2026-09-04T21:20:31Z fitter Repair duplicate centers on the BF16 grid and stamp affected codebooks Original fitter aborted on a degenerate source expert; preserve the existing Lloyd fit 07e3284; artifacts/ds4v-fitter-fix/red.log; artifacts/ds4v-fitter-fix/green.log; artifacts/ds4v-fitter-fix/replay-42-164.log Unit, smoke, and expert replay pass; full conversion running +2026-09-04T21:20:31Z isolation Use separate local and soulf worktrees; stop recorded baseline PID3263851 Original Mac index has pre-existing deletions; soulf baseline test occupied memory 07e3284; soulf:lucebox-ds4v-mix-fix/artifacts/fitter-fix/cleanup-baseline.txt Original checkout files preserved; test PID exited;8016not touched +2026-09-04T21:32:21Z vision-reference Run the parent image processor, tower, and aligner on both supplied photos Need numerical ground truth for the C++ vision implementation artifacts/ds4v-step2/reference-manifest.json; artifacts/ds4v-step2/reference-run.log CPU reference produced finite embeddings and position-dependent layouts for carrots and corn +2026-09-04T21:45:09Z architecture Keep the small request payload and extend the existing hybrid graph Avoid duplicating the decoder or migrating unrelated backend APIs artifacts/ds4v-step2/design-a.md; artifacts/ds4v-step2/design-b.md; artifacts/ds4v-step2/design.md Selected after independent same-family review; component and image HTTP gates remain open +2026-09-04T21:58:41Z conversion Full calibration passed the prior failing expert BF16 epsilon separation fixed the actual full-run collision without changing fitting weights artifacts/ds4v-fitter-fix/full-run-repair.log PASS calibration; encoding and load proof pending +2026-09-04T22:09:12Z projector Verified lossless standalone projector at ef64f62 Native GGUF parser and independent original-byte comparison qualify the runtime input artifacts/ds4v-step2/mmproj-proof/byte-proof.json PASS 12 tests, 267 tensors and 932786176 payload bytes +2026-09-04T22:55:05Z arena Select A as experimental runtime base; keep numerical ISSUES Independent review scored A20/25 B16/25; both share unchanged corn failure artifacts/ds4v-step2/native-tower-crossjudge.md; ds4v/vision-runtime5bf705e Complete token budget fixed with remote red/green; maximum grid finite and observer invariant; no parity or GPU claim +2026-09-04T23:13:41Z implementation Accept independent image policy and transport units Source fixture policy parity and remote transport red-green; independent transport review resolved depth and placeholder findings ds4v/vision-policy5da9272; ds4v/vision-transport25f6105; artifacts/ds4v-step2/transport-review.md Unintegrated CPU units PASS; no HTTP/GPU/model behavior claim +2026-09-04T23:26:54Z step2 HIP tower probe build passes Reuse server HIP compatibility definitions for standalone gfx1100 and gfx1151 qualification; GPU execution remains after text proof artifacts/ds4v-step2/runtime-proof/build-hip-compat.log PASS build only at 4bf7270; numerical qualification remains ISSUES +2026-09-04T23:35:57Z step2 Isolate pure prompt preparation from tower integration Uses accepted preprocessing core only; final token expansion, checked admission and owning spans can be verified without unqualified tower or codec artifacts/ds4v-step2/prompt-preparation-brief.md Writer ds4v/vision-prompt; tests-first remote CPU red/green required +2026-09-05T00:03:00Z step2 Accept CPU image preparation composition Verified units compose through actual renderer/tokenizer with exact source patches/layouts and interaction negatives; tower remains separate artifacts/ds4v-step2/cpu-composition-review.md PASS ds4v/vision-cpu0065158; no HTTP/backend/GPU integration +2026-09-05T00:03:00Z step1 Prototype byte-preserving parallel expert encoding Single-core full run is substantially slower than handoff estimate; independent review found bounded immutable expert tasks feasible artifacts/ds4v-fitter-fix/parallel-encoding-brief.md Prototype only; current full run preserved; complete17expert byte comparisons and speed evidence before another full run +2026-09-05T00:21:39Z implementation Launch distinct eight-worker full MIX candidate after acceptance Original serial run takes substantially longer than handoff estimate; exact codec and five-lane output identity with 3.9x sample speedup justify bounded concurrency ds4v/mix-parallel 1a38b984; artifacts/ds4v-fitter-fix/mix-parallel-review.md; soulf PID3333730; conversion.started 2026-09-05T00:20:05Z ACTIVE; original PID3289986 untouched; text load and vision runtime still unproven +2026-09-05T00:26:40Z verification Prepare isolated original-source ROCm control without GPU execution CPU native drift remains unresolved; eventual same-device source comparison can distinguish backend arithmetic from graph mismatch artifacts/ds4v-step2/rocm-source-reference-options.md; precommitted AMD torch2.10 rocm7.2.4 investigative build Preparation only; immutable CPU fixtures and system ROCm preserved; no version sweep or gate change +2026-09-05T00:41:37Z verification Accept isolated source ROCm environment and CPU portability control One fixed AMD Torch build plus private matching MIOpen now preserves original corn CPU outputs exactly, enabling later same-GPU diagnostic comparison artifacts/ds4v-step2/source-rocm-reference/report.md; source-rocm-reference-review.md; runner17ba9d66; CPU feature/embedding hashes identical Scoped PASS; source HIP and native acceptance NOT_QUALIFIED; fixed gate unchanged +2026-09-05T01:21:27Z checkpoint Allow standalone vision GPU qualification during CPU quantization User reaffirmed working on vision now; verified projector/source fixtures make the standalone tower independent of the unfinished text artifact. Existing text-first chat milestone stays; idle pair and operator protection are enforced User follow-up; KFD empty; operator inactive PID0; hip-qualification.sh component-only mode with double idle/resource gate Pending harness review before GPU execution; no full image chat before text proof +2026-09-05T01:31:57Z verification Execute independently guarded native HIP component qualification Standalone projector is verified and pair is idle; preserve full text-first chat milestone component-window-review.md; native-hip-first/summary.json; harness152af330 Execution PASS; fixed features and embeddings ISSUES/exit3; corn repeat byte-identical; targeted biased-linear rounding hypothesis under investigation +2026-09-05T01:37:12Z qualification-policy Adopt prospective same7900XT original-source reference with unchanged numeric thresholds Original source HIP itself fails CPU feature threshold; native also fails against source HIP; controlled target fidelity requires target reference target-hip-qualification-policy.md 62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f; two independent reviews of substantive policy7edde20e Policy accepted; reference repeat/freeze and corrected candidate qualification still pending; all CPU failures retained +2026-09-05T01:44:59Z reference-freeze Freeze first original-source HIP outputs after exact repeat stability Same-target baseline must precede corrected native full-tower output canonical677b5ef0; freeze8ab35a8a; source-rocm-reference/reference-stability-report.md PASS source stability both images; native candidate NOT_EVALUATED; CPU corn feature portability ISSUES retained +2026-09-05T02:02:01Z conversion Accept completed parallel MIX artifact for private load proof All129 tensors and internal structural/sidecar/raw-byte verification pass; complete2filemanifestpublishedbeforeexit0 parallel-run/conversion.sha256; GGUF58086fcd; GUMIX954110a5 Conversion PASS; private text load active; original serial preserved until text verdict +2026-09-05T02:02:01Z vision Keep scoped rounding fix but preserve full tower ISSUES Tiny source-grounded regression passes and CPU outputs staybyteidentical; both target embeddings nowpass but featuresstillfail be8b0f1; biased-linear-rounding.md; native-hip-scoped/summary.json Implementation scopedPASS; targettowerexit3; runtime integration remains blocked +2026-09-05T02:08:07Z text-load Preserve failed first chat proof and fix BF16 RMS affine compatibility Model loads acrossbothGPUs but firstchat asserts GPUbinarybroadcast src1 type; actualnormvectorsBF16 load-first/server.log; load-first/harness.exit52; PID3380377gone/KFDempty Load/topologyPASS; chatFAIL; isolated narrowgraphfix authorized; operator remainsdown untilsuccessfulproof +2026-09-05T02:42:20Z text-load Accept full MIX text proof with narrow BF16 norm fix All six requests succeed; exact math and triplicate longer145-token replies pass with speculative decoding true 7071946; binaryc32e5ae3; load-bf16-pass/verdict.json; harness.exit0; PID3396669gone/KFDempty Text PASS; short decode15.9/16.4/17.4tps; old operator as-is restoration next; vision still unqualified diff --git a/docs/ds4v-continuation-status.md b/docs/ds4v-continuation-status.md new file mode 100644 index 000000000..5f1dba410 --- /dev/null +++ b/docs/ds4v-continuation-status.md @@ -0,0 +1,82 @@ +# DS4V continuation status + +Current authority is the September 4 operator handoff. Earlier quant recipes, gating blockers, and throughput targets in ds4v-uncensored-vision-plan.md are historical. + +## Completed units + +The adaptive-codebook failure is fixed on fork branch `ds4v/mix-converter` at `07e32844cc32602bab8167072e9b301eb832bfe2`. Tests reproduced the failure before the fix. The repaired fitter passes degenerate-input tests, preserves distinct BF16 levels, and passed the actual layer 42 expert 164 replay. The one-layer, one-expert converter smoke passed. The full run crossed the former failure and stamped one `bf16-epsilon-v1` repair. Plain MSE fitting and the uniform imatrix remain in use. + +The standalone projector exporter is verified on branch `ds4v/vision` at `ef64f62fdc9aca1202ef58600d020c42d8e4c1b0`. Twelve tests pass on soulf without skips. The native gguf.cpp reader accepted the file, and an independent reader compared all 267 tensor names, shapes, BF16 types, and 932786176 payload bytes against the parent. The artifact is `~/ds4v-work/ds4v-mmproj.gguf` on soulf, SHA256 `58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c`. Independent source review passed. + +## Current work + +Current checkpoint: full MIX conversion and new-model text chat PASS. Existing operator8016 remains PID3401443, active/running with NRestarts0 and its owned listener; no test request was sent to it. Direct original-source patch/QKV GPU calculations match exactly. Native HIP-only operation6137f430 passes clean CPU/HIP builds and preserves all four previous CPU image outputs. The first reviewed concurrent tiny RED attempt failed before HIP initialization completed: a monitor allocation-field parse error triggered owned-child cleanup, and the kernel logged an AMD XDNA NPU driver NULL dereference during that interval. PID3414758/start50217527 remains in uninterruptible `amdxdna_drm_close` with KILL pending; cleanup is incomplete. GREEN/full-image lanes did not launch. All further GPU work is held. A separate monitor repair handles standard MiB counters, preserves parse/error diagnostics, and bounds post-KILL waiting; all115 CPU tests pass. Restart approval was requested because restarting soulf interrupts the protected server. Evidence: `artifacts/ds4v-step2/native-lt-concurrent-first/` and `radeon-numerical-guard-recovery/`. Vision serving remains unfinished. + +Full conversion and the corrected full-model text proof pass. Vision features remain unqualified. The original serial converter PID3289986 was terminated only after text PASS, using its exact command, executable and start ticks47893724 with a PID file descriptor. Its partial output is preserved. The successful parallel artifact is authoritative for subsequent testing. + +The first supervised as-is operator restoration failed safely. Original PID3398634 reached its91.1GiB managed-memory allocation, then sustained memory pressure without log/process-I/O progress: PSI approximately50–74%, swap1.2→13.1GiB, and available memory approximately1.1GiB after61seconds. No OOM or automatic restart was observed. The guard stopped the one service it started and confirmed inactive/MainPID0; evidence is `artifacts/ds4v-fitter-fix/operator-restoration-first/`. After cleanup KFD was empty and available memory recovered to42GiB. The original unit/profile/binary are unchanged. Read-only diagnosis precedes any further restoration attempt; no reset, cache drop or configuration workaround is authorized by this failure. + +Observed single-core encoding is substantially slower than the handoff's three-hour estimate. The bounded optional encode-threads1..8 prototype on separate `ds4v/mix-parallel` is frozen at `1a38b984cfdc51f6bc83acc366e3543ab4192498`. Calibration, repair, row quantizer arithmetic, recipes and metadata are unchanged. All three suites and actual42/164 fitter replay pass. Seventeen actual experts produce byte-identical complete GGUF+GUMIX files with old/default/1/8/repeat8. The eight-worker sample takes10.8seconds versus42.2seconds serially, about3.9x end-to-end and5.15x during encoding, with153888KiB peak RSS. This shares the machine with the original conversion and is not an isolated benchmark. Independent source and corrected launch-wrapper reviews pass. + +A separate full eight-worker candidate started at2026-09-05T00:20:05Z: wrapperPID3333717, converterPID3333730, output `~/ds4v-work/DeepSeek-V4-Flash-Vision-Uncensored-ROCmFPX-MIX-parallel.gguf`, evidence `~/lucebox-ds4v-mix-parallel/artifacts/fitter-fix`. The wrapper pins source and binary, verifies the qualification manifest's config/tokenizer/index and uniform-imatrix hashes, records both PIDs, and publishes conversion.exit only after the complete two-entry output checksum manifest. Calibration remained serial. This candidate has completed; both recorded processes have exited. + +The parallel candidate completed calibration in1999.04seconds, with exactly the expected layer42/expert164 down repair stamp, and has now finished all129 expert tensors. Its internal verifier passed qtypes/bounds, exact sidecars and raw pass-through bytes. Conversion wall time was1:34:34, peak RSS168996KiB. The wrapper subsequently finished the complete two-file checksum manifest and published conversion.exit0. Its first64MiB matches the original candidate byte for byte, SHA256 `f13168d5f45282b39451b6756184ab7bcd8dd20f91733d83969b2d90c27cf88c`. This is a bounded prefix comparison, not whole-artifact verification. Initial full expert tensors take about25–34seconds each. Evidence: `artifacts/ds4v-fitter-fix/parallel-run/prefix-comparison.json`. + +Both isolated native tower candidates completed CPU qualification. Independent review selected A as the experimental base,20/25 versus16/25, while keeping the numerical verdict ISSUES. Selected branch `ds4v/vision-runtime` at `4bf7270` fixes the complete N-layout token budget and retains a machine-visible failing numerical gate. Functional loader, arithmetic, original-image shape/finite checks, and maximum permitted3366-patch execution pass. Peak measured scratch is891424512bytes with diagnostic snapshots; snapshots do not change outputs. Corn feature cosine remains0.99822935 against the fixed0.9995 minimum. + +The first projection diagnostic shows adjacent-BF16 accumulation sensitivity without detecting a layout/formula bug, but does not explain every source-kernel outcome or prove harmless end-to-end error. The completed sensitive-block diagnostic found no semantic or BF16-boundary discrepancy in corn blocks12/31. A single original-source corn one-thread control reproduced the two-thread reference bitwise; native drift remains unresolved and the gate is unchanged. + +The native HIP qualification harness now supports an independently released `--component-only` window. Source review passed; it checks operator inactivity, free ports, no KFD compute processes and at least 8GiB available host/discrete memory before execution. Current harness SHA256 is `152af330bb37f77f8e6f9c795ae46c12f787e8587c42fc210aeb671b205296d5`. This changes component scheduling only; full image chat still follows text proof. + +The first native HIP qualification executed on the 7900 XT at unchanged runtime `4bf7270`. All three image probe processes completed, produced finite outputs and repeated corn byte-identically. Both unchanged numerical comparisons failed with exit3: corn feature cosine0.985939 and carrots0.993884, below0.9995; embeddings also failed their cosine gate. Evidence is `artifacts/ds4v-step2/native-hip-first/`. Standalone encode times were0.279s corn and0.716s carrots, including the probe's transfers; these are not full chat or warmed throughput measurements. + +The exact dyadic micro-regression confirmed a biased-linear rounding defect: GGML's nonbatched BF16 HIP GEMM returns BF16 even when F32 precision was requested, before the tower adds bias. The scoped fix at `be8b0f1b07f1a3a034ce1d7333fd0d3402754c60` promotes biased weight operands only on GPU backends. Tiny HIP biased errors fall from528 to0. The unchanged unbiased output exactly matches original Torch HIP, including256 negative tie cases that differ from an abstract nearest-even oracle. The test now checks the actual unmodified product-rounding contract and retains those diagnostic differences. Direct fused source biased output still differs in88 tiny tie cases; no bitwise fused-linear claim is made. + +The first broader F32-weight candidate8bec967 regressed CPU numerics; the scoped fix avoids that regression. All four CPU corn/carrots outputs now match pre-fix4bf7270 byte for byte. CPU tiny, geometry and16 loader cases pass; original CPU corn feature ISSUES remains. The scoped implementation and harness passed independent review. See `artifacts/ds4v-step2/biased-linear-rounding.md` and `vision-linear-rounding-review.md`. + +The full scoped target-HIP run completed at `soulf:~/lucebox-ds4v-linear-rounding/artifacts/hip-target-scoped`, with metadata/logs copied to `artifacts/ds4v-step2/native-hip-scoped/`. Both embeddings now pass against frozen same-GPU source (corn cosine0.999525047, carrots0.999721786). Features still fail: corn maxabs0.947265625 and cosine0.999063593; carrots maxabs0.26416015625 (cosine0.999538399 passes). All outputs are finite and corn repeat is byte-identical. Harness exit3 is preserved. The tower dependency remains blocking; no production HTTP/backend vision wiring or image-chat acceptance is claimed. + +The independent original-source ROCm environment at `~/ds4v-work/source-rocm210-reference/.venv` uses one preselected AMD Torch2.10/ROCm7.2.4 build and privately extracted matching MIOpen, without system installation. Its CPU corn control reproduces frozen features and embeddings byte for byte. The first source HIP attempt stopped before weights because of an exact marketing-name guard. An identity-only query confirmed the same7900XT/gfx1100/20464MiB and justified a two-line runner correction: print actual identity and match `Radeon RX 7900 XT`. Corrected runner SHA256 is `cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`; original runner/evidence remain intact. + +The corrected original-source corn HIP forward completed successfully, but features fail the unchanged CPU gate (cosine0.997729789, maxabs2.73828125); embeddings pass (cosine0.999115332). NativeHIP also fails against sourceHIP (features0.991196939, embeddings0.994697537), so CPU/GPU portability does not explain away the native discrepancy. All three comparisons are retained in `source-rocm-reference/hip-supervision-confirmed/three-way.json`. Source GPU peak allocation was1176237056bytes; the lane released its GPU allocations after exit. + +A prospective GPU-only policy passed two independent reviews and was adopted in `artifacts/ds4v-step2/target-hip-qualification-policy.md`: freeze stable original-source corn+carrots outputs on the same7900XT before corrected native full-tower execution, apply every existing numerical threshold unchanged, and retain CPU portability failures separately. The immutable CPU reference, system ROCm, source graph and precision remain unchanged. No alternative-version sweep or tolerance change occurred. + +Native preprocessing is accepted at `ds4v/vision-preprocess` `edb3b0e15d3e73b5408d8fbb13532fe40fc9fefb`. Source RGB/resize/BF16/layout fixtures pass. Decoder regressions first failed at a28abfd; 6d28845 fixes bounded PNG IDAT inflation, Pillow-compatible grayscale16 and explicit unsupported CMYK/YCCK, and edb3b0e corrects the pinned license notice. Original ten fixtures and three reviewer fixtures pass, with independent review PASS. UBSan covers the C++ wrapper and LodePNG, not the external libjpeg C build. The earlier RGB core combined implementation and tests; no tests-first RED evidence is claimed for that earlier commit. Decoder total memory includes inflate/raw/interlace buffers beyond its two RGB buffers. + +Pure prompt preparation at `ds4v/vision-prompt`859f2f7 passes recorded RED/GREEN, exact source comparisons for ten single-image cases and both image orders, and independent review. The accepted components are composed at `ds4v/vision-cpu`0065158, with recorded composition RED/GREEN and independent review PASS. The probe joins actual data-URL extraction, renderer/tokenizer, codecs, preprocessing and expansion, with real-image source comparisons, Jinja/cardinality controls, context/errors/redaction and same-layout pixel isolation. It uses an explicit probe text adapter, not HttpServer normalization. HTTP image input, decoder image visibility, and image expert routing are not yet integrated into the server. + +The selected design is in `artifacts/ds4v-step2/design.md`. It keeps monolithic asymmetric expert parallelism and extends the existing sparse layer-major graph. Sparse prefill remains approximate. No complete Torch decoder-logit parity is claimed. + +`artifacts/ds4v-step2/integration-touchpoints.md` maps the actual request, HTTP, lifecycle, cache, routing, raw-mask and chunking seams at4bf7270, including unsupported-path guards and ordered validation. This is read-only preparation; it does not clear the tower gate or implement runtime integration. + +Independent image policy is verified at `ds4v/vision-policy`5da9272:120 image expert IDs and433562 raw visibility pairs match source exactly; maximum weight error5.96e-8. Bounded image data-URL transport is verified at `ds4v/vision-transport`25f6105, with remote red/green and independent review. Neither unit is wired into the server. The optional HIP tower probe built successfully on soulf from `ds4v/vision-runtime`4bf7270 for gfx1100 and gfx1151, using the existing server HIP compatibility definitions. The first standalone GPU execution completed with numerical ISSUES, as recorded above. + +## Load and operator restoration + +The private text load harness is `artifacts/ds4v-fitter-fix/load-proof.sh`, copied to `/tmp/ds4v-load-proof.sh` with `/tmp/ds4v-load-validate.py` on soulf. Conversion is complete. The first private parallel text proof, `load-proof-20260905T015809Z-IYNiSF`, verified both full checksums and loaded the model across both GPUs. The first chat request aborted at `ggml-cuda/binbcast.cu:414`, which rejects a BF16 second operand. Harness exit52 and own-PID cleanup are recorded in `artifacts/ds4v-fitter-fix/load-first/`; PID3380377 is gone and KFD is empty. No chat verdict passed. Its optional `serial|parallel` selector couples the candidate model and evidence root, while retaining the same original launch profile; default is serial. Both the original harness and this narrow selector pass independent review. It requires and verifies exact target/sidecar checksum records, uses a fresh evidence directory indexed by `artifacts/fitter-fix/load-proof.latest`, pins private8217 and the intended child topology/workaround environment, checks actual HIP device order and expert ownership, and validates triplicate exact math and longer deterministic speculative replies. It records valid usage/timings, binary/draft hashes and final exit status, then cleans up its own PID. Six validator CPU tests pass. Timing results will be short load observations, not a warmed benchmark. + +Current harness SHA256 is `da2a424317885376017c866674df7f77642427ec6d843db2a8aa07432838f1f6`. Its added load-slot guard checks that the operator is inactive/failed with PID0 and both8016/8217 are free, before evidence creation and again immediately before launch. Listener-query failure rejects the run. Independent review and9/9 CPU mock cases pass; the first full execution failed on first chat and cleaned up private8217. A clean isolated text compatibility fix is being prepared from298031aa. + +Operator8016 is restored. Its unchanged user unit is `deepseek-dflash.service`, with ExecStart `~/lucebox-0731-main/run/serve-ds4-0731-mix-merged.sh`; it serves the older Strix-only model at128K. The first attempt stopped under memory pressure. A richer second observation demonstrated real allocation/upload progress and completed main-model initialization, but reached its300-second deadline before listener readiness. Cleanup naturally reclaimed retained memory, leaving111GiB available. The final attempt required at least110GiB available in all three pre-start snapshots and retained every runtime/cleanup guard. It passed: PID3401443, active/running, NRestarts0, correct gfx1151-only initialization and owned8016 listener. Evidence is `artifacts/ds4v-fitter-fix/operator-restored/`; both unsuccessful attempts remain preserved. No reset, dropped cache, model/profile change, or operator chat request occurred. + +The user was asked whether to restore 8016 after text proof or keep the pair available through vision qualification. Until a reply changes the instruction, the handoff requires restoration after text proof. Full-model image lanes need a separate GPU window once the operator service is restored. + +## Evidence + +- `artifacts/ds4v-fitter-fix/` contains fitter red/green logs, actual-expert replay, converter smoke, full-run repair stamp, memory diagnosis, and the private load harness. +- `artifacts/ds4v-step2/mmproj-proof/` contains the projector manifest, output hash, native inventory, and tests. +- `artifacts/ds4v-step2/reference-manifest.json` and `routing-mask-manifest.json` index parent CPU fixtures stored only on soulf. +- `decisions-ds4v-continuation.tsv` records completed decisions and their evidence. + +No GPU execution, model download, or native build has run on this Mac. The original worktree index remains untouched. All fork pushes use origin; no upstream push occurred. + +The original-source target references are now frozen at canonical manifest677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86 and freeze/provenance8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0. Both features and embeddings repeat byte-identically for both images. FIRST outputs are retained. Source corn CPU feature portability remains ISSUES; source carrots features/embeddings and corn embeddings pass their CPU gates. See `source-rocm-reference/reference-stability-report.md`. + +Completed parallel artifact:113745874400bytes (GGUF), SHA256 `58086fcd38a57338d0f6ac50466ce4bc9cae1a2e0faebe975c22fee3061b97be`; GUMIX376696bytes, SHA256 `954110a5169fedf06a6973604a20242e9473a8be7661c1bfe2e3195680f50a2a`. Final manifest and exit0 are copied in `artifacts/ds4v-fitter-fix/parallel-run/`. The live text load measured12.88GiB dense/core on7900XT, leaving room for6.07GiB hot experts; future vision-enabled placement must reserve projector weights plus scratch before choosing its hot budget. + +Text compatibility fix: actual model normalization vectors are BF16, while HC parameters, router biases and attention sinks are F32. The DS4 RMS graph passed the BF16 norm vector directly to a GPU binary multiplication that accepts only F32/F16. Isolated branch `ds4v/text-bf16-affine` at `707194695703c023a8bf026684102d7c597d15b6` casts only BF16 affine vectors to F32 in the graph, preserving stored model bytes and dense/expert matrices. Real HIP RED reproduced the assertion; GREEN executes both tested token shapes with maximum error below 2.2e-7. CPU execution and F16/F32 graph-preservation checks pass. Independent review passes. + +The clean full server build completed on soulf at `/tmp/ds4-text-bf16-server-build/dflash_server`, SHA256 `c32e5ae32da82cde1c8aab61e26c693dbc5b3679181b55b7483ae9312d64fca0`. The pinned retry harness `load-bf16-proof.sh`, SHA256 `336bc3816553fbf000c40a67997cea757c93c9f13c9f7386e760c5d200da9d2e`, passes independent review, shell syntax, and all 11 load-slot guard mocks. Private full text retry `load-proof-20260905T023749Z-Vc1jid` completed with exit0 and all acceptance checks PASS. Three math replies returned exactly4; three longer145-token replies were byte-identical with speculative decoding true and acceptance0.6610. Their short decode observations were15.9/16.4/17.4tokens/s, not a warmed benchmark. The private PID3396669 exited and KFD was empty after cleanup. Complete evidence is copied to `artifacts/ds4v-fitter-fix/load-bf16-pass/`. Main checkout/binary and operator configuration remain untouched; supervised as-is operator restoration is next. + +The direct hipBLASLt synthetic biased projection matches the original GPU source output exactly (1024/1024 values), using the source's fixed first-heuristic, 76 MiB workspace configuration. Two preselected real corn projections are prepared for separate guarded execution: patch projection and block-0 QKV. Their capture must reproduce the frozen original full-tower outputs before either fixture is accepted. No production operation or image-chat support is implied by this small test; see `artifacts/ds4v-step2/hipblaslt-real-projections/README.md`. diff --git a/docs/ds4v-uncensored-vision-plan.md b/docs/ds4v-uncensored-vision-plan.md new file mode 100644 index 000000000..acba93152 --- /dev/null +++ b/docs/ds4v-uncensored-vision-plan.md @@ -0,0 +1,242 @@ +# DS4V uncensored vision GGUF for Strix Halo plus 7900XT plan + +Build an uncensored DeepSeek V4 Flash Vision GGUF that runs on the operator pair. +Start from the OrcaRouter abliterated parent with vision intact. +Quant with the prometheusAIR imatrix recipe for a 128 GiB plus 24 GiB budget. +Serve with asymmetric expert parallelism per the Lucebox report. +PR ids in order. DS4V-1 then DS4V-2 then DS4V-3. +Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. +Style rules. i dont want any abstract metaphors. Write like hemingway. + +## How to read this + +One box is one unit of work. Every box names the evidence that checks it. A nested box is a substep of the box above it. Check a box only when its evidence exists. A file. A log line. A test run. Or a SHA. +The program runs `playbooks/autopilot-stack.md`. The root builds the chain. The operator lands it with her own clicks. +Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. +Style rules. i dont want any abstract metaphors. Write like hemingway. + +## Program checklist + +### Arm the program + +- [ ] State the protocol and this plan to the operator, then stop. Start execution only on her explicit go. +- [ ] On her go, adopt the run objective with this exact text. "`docs/ds4v-uncensored-vision-plan.md`, DS4V-1 then DS4V-2 then DS4V-3, Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked., the root builds the chain and the operator lands it, done when every box is checked with its evidence." +- [ ] Read these from trunk at program start. Re-read them at every tick. + - [ ] `git show origin/main:skills/poteto-mode/playbooks/autopilot-stack.md` + - [ ] `git show origin/main:skills/swarm/SKILL.md` + - [ ] `git show origin/main:.pi/skills/verify-lucebox/SKILL.md` + - [ ] `git show origin/main:skills/poteto-mode/playbooks/opening-a-pr.md` + - [ ] `git show origin/main:skills/how/SKILL.md` +- [ ] Arm the 30-minute audit tick as a bash polling loop with an explicit iteration cap. Never leave the cadence to memory. +- [ ] Use this tick prompt, verbatim. "Re-read the execution playbook from trunk and the run objective. Audit the operation against both and fix drift in this tick. Probe every active lane and judge progress by side effects only. Stand down a stuck lane and dispatch its replacement now. Then send the operator a status message, whether or not anything changed, with the queue table of PR, owner, state, and head SHA, the verdicts since the last tick, what merged, open operator gates, and blockers." +- [ ] On the operator hold or stand down order, send every owner a zero writes order at once. + +### Run owner passes + +- [ ] Run one owner pass per PR with the full lifecycle the execution playbook names. +- [ ] Follow this dependency graph. Start dependent work only after its parent merges. + - [ ] DS4V-1 and DS4V-2 are independent and first. Both branch from `main`. + - [ ] DS4V-3 after DS4V-1 and DS4V-2. +- [ ] Hold the file boundaries. DS4V-1 touches only `docs/ds4v-baseline.md` and `scripts/ds4v-baseline.sh`. DS4V-2 touches only `docs/ds4v-source.md` and `scripts/ds4v-fetch-source.sh`. DS4V-3 touches only `scripts/ds4v-quant.sh` and `docs/ds4v-quant.md` and `share/model_cards/ds4v-vision.json`. +- [ ] Hold the review gate. No PR changes an interaction. All three stop at merge ready without an operator media review. + +### PR mechanics, for every PR + +- [ ] Resolve the forge once. Default to `gh`. If `command -v origin` succeeds and Origin can resolve the repository, use `origin pr` for every PR operation. Record any fallback to `gh`. Never require `gt`. +- [ ] Open the PR ready, never draft, with `origin pr create --status open --base main` or `gh pr create --base main` according to the resolved forge. A stack child targets its parent branch. +- [ ] Run the repo lint and typecheck once before the PR facing push. Push with hooks on. +- [ ] Run `/unslop` over the diff before each commit and `/no-comments` before review. +- [ ] Triage every Bugbot and security reviewer comment per `../references/bugbot-triage.md`. +- [ ] Rebase onto current trunk before babysit and again before the merge ready report. + +### Verdict and merge, for every PR + +- [ ] At the merge ready head SHA, run the swarm per `skills/swarm/SKILL.md`. One gates lane. The ten live lanes from the PR Verify live block. The perf lane from its Verify perf block. One audit lane that reads the diff and the receipts and distrusts the PR body. +- [ ] Clean only when every lane is `PASS`. Findings go back to the owner. A new head gets a fresh swarm and a fresh verdict. +- [ ] The root appends each clean PR to the one linear base branch stack and the operator lands it bottom up. A rebase that changes a patch id sends that PR back through verification. + +### Boot recipe, for every live lane + +- [ ] Fetch the PR head with `git fetch origin` and check out the exact head SHA. +- [ ] Start the backend on the Strix Halo plus 7900XT pair and wait for `/props.build` to answer. +- [ ] Deliver input only through the bash driven harness. Name the read only diagnostics. +- [ ] Save every proof file under `/tmp/swarm-DS4V/worker-1` and return the paths with the report. + +## Reproduce the asymmetric baseline on the operator pair (DS4V-1) + +**Depends on.** None. + +**Files.** + +- [ ] Create `docs/ds4v-baseline.md` with the measured setup and commands. +- [ ] Create `scripts/ds4v-baseline.sh` with the launch and curl proof steps. + +**Build.** + +- [ ] Record the server SHA and both model SHAs in `docs/ds4v-baseline.md`. + +**You see.** + +- [ ] A `curl` call to `/props.build` answers with the expected image tag. + +**Verify, unit.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Run `ctest --output-on-failure -R deepseek4_unit` in `server/build` and keep the log. + +**Verify, live.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on `fast mechanical model (setup default)` at the PR head, per the boot recipe. + +- [ ] Lane 1. Regression lane against trunk. Run the same text prompt at trunk and head. Save `artifacts/ds4v-1/lane-1-compare.json`. Pass when both sides return HTTP 200 with non empty text. +- [ ] Lane 2. Chat smoke over the heterogeneous path. Send the LRU prompt from the verify skill. Save `artifacts/ds4v-1/lane-2-chat.json`. Pass when the response holds generated text. +- [ ] Lane 3. Build identity. Read `/props.build` from the running server. Save `artifacts/ds4v-1/lane-3-props.json`. Pass when the file names the expected image tag. +- [ ] Lane 4. Prefill probe. Send the 2k prompt used in the Lucebox report. Save `artifacts/ds4v-1/lane-4-prefill.json`. Pass when prompt processing exceeds 300 tok/s. +- [ ] Lane 5. Decode probe. Generate 128 tokens from the same prompt. Save `artifacts/ds4v-1/lane-5-decode.json`. Pass when decode exceeds 40 tok/s. +- [ ] Lane 6. DSpark acceptance. Read the served response header for the spec flag. Save `artifacts/ds4v-1/lane-6-spec.json`. Pass when the flag reports true. +- [ ] Lane 7. Placement proof. Read the server log for the owner lines. Save `artifacts/ds4v-1/lane-7-placement.log`. Pass when both devices appear as owners. +- [ ] Lane 8. Determinism. Send the same prompt twice. Save `artifacts/ds4v-1/lane-8-repeat.json`. Pass when both answers match byte for byte. +- [ ] Lane 9. Model list. Read `/v1/models` from the same server. Save `artifacts/ds4v-1/lane-9-models.txt`. Pass when the call returns 200 or 404 with a body. +- [ ] Lane 10. Cleanup. Run the verify skill cleanup. Save `artifacts/ds4v-1/lane-10-cleanup.log`. Pass when the instance is gone and the proof files remain. + +**Verify, perf.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Metric. Decode tok/s on the 2k prompt with 128 output tokens. +- [ ] Probe. Run `scripts/ds4v-baseline.sh` at trunk and at the head, interleaved. +- [ ] Baseline. Record the trunk value first. +- [ ] Rule. Head ties or beats trunk. Fail when head trails by more than 5 percent. + +**Review gate.** None. DS4V-1 is not review-gated. + +**Merge.** + +- [ ] Root records a clean verdict at the exact head SHA. +- [ ] The owner rebases onto current trunk after the verdict with patch id unchanged. + +## Lock the abliterated vision source with provenance (DS4V-2) + +**Depends on.** None. + +**Files.** + +- [ ] Create `docs/ds4v-source.md` with the parent repo and the tensor manifest. +- [ ] Create `scripts/ds4v-fetch-source.sh` with the exact download commands. + +**Build.** + +- [ ] Verify all 48 shards and 72633 tensors match the manifest by name and shape. + +**You see.** + +- [ ] The manifest lists the vision tower and the aligner as present. + +**Verify, unit.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Run `python3 scripts/ds4v-fetch-source.sh --check-only` and keep the checksum log. + +**Verify, live.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on `fast mechanical model (setup default)` at the PR head, per the boot recipe. + +- [ ] Lane 1. Regression lane against trunk. Run the same text prompt against the stock parent and the abliterated source. Save `artifacts/ds4v-2/lane-1-text.json`. Pass when both answers match in shape and the ablated one refuses less. +- [ ] Lane 2. Vision presence. List `vision.*` tensors in the manifest. Save `artifacts/ds4v-2/lane-2-vision.txt`. Pass when the count equals 259. +- [ ] Lane 3. Aligner presence. List `aligner.*` tensors in the manifest. Save `artifacts/ds4v-2/lane-3-aligner.txt`. Pass when the count equals 4. +- [ ] Lane 4. Router bias. List `bias_vl` tensors in the manifest. Save `artifacts/ds4v-2/lane-4-bias.txt`. Pass when the count equals 43. +- [ ] Lane 5. Image smoke. Describe the Earth image with the reference implementation. Save `artifacts/ds4v-2/lane-5-earth.json`. Pass when the answer names Earth. +- [ ] Lane 6. Text capability. Score the MMLU sample with both checkpoints. Save `artifacts/ds4v-2/lane-6-mmlu.json`. Pass when the delta stays within 1 point. +- [ ] Lane 7. Tokenizer. Encode the OpenAI style image message with the reference encoder. Save `artifacts/ds4v-2/lane-7-encode.json`. Pass when token ids match the reference. +- [ ] Lane 8. Draft head. List `mtp.*` blocks in the manifest. Save `artifacts/ds4v-2/lane-8-mtp.txt`. Pass when the count equals 3. +- [ ] Lane 9. License. Read the model `LICENSE` from the source repo. Save `artifacts/ds4v-2/lane-9-license.txt`. Pass when the text names MIT. +- [ ] Lane 10. Cleanup. Remove the scratch download cache. Save `artifacts/ds4v-2/lane-10-cleanup.log`. Pass when the manifest and proof files remain. + +**Verify, perf.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Metric. Minutes to verify all shard checksums on the build machine. +- [ ] Probe. Run `scripts/ds4v-fetch-source.sh --check-only` twice on the same host. +- [ ] Baseline. Record the first run value first. +- [ ] Rule. Second run ties or beats the first. Fail when it trails by more than 20 percent. + +**Review gate.** None. DS4V-2 is not review-gated. + +**Merge.** + +- [ ] Root records a clean verdict at the exact head SHA. +- [ ] The owner rebases onto current trunk after the verdict with patch id unchanged. + +## Ship a vision GGUF tuned for the operator pair (DS4V-3) + +**Depends on.** DS4V-1 and DS4V-2. + +**Files.** + +- [ ] Create `scripts/ds4v-quant.sh` with the imatrix quant recipe. +- [ ] Create `docs/ds4v-quant.md` with the rung table and the budget math. +- [ ] Create `share/model_cards/ds4v-vision.json` with the placement and budget. + +**Build.** + +- [ ] Run the quant recipe and record the GGUF SHAs in `docs/ds4v-quant.md`. + +**You see.** + +- [ ] A `curl` image prompt returns a correct scene description. + +**Verify, unit.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Assert the GGUF keeps `bias_vl` on all 43 layers and the mmproj loads. + +**Verify, live.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on `fast mechanical model (setup default)` at the PR head, per the boot recipe. + +- [ ] Lane 1. Regression lane against trunk. Run the same text prompt at the DS4V-1 baseline and at this head. Save `artifacts/ds4v-3/lane-1-compare.json`. Pass when both return HTTP 200 with non empty text. +- [ ] Lane 2. Image description. Describe the carrots image through `/v1/chat/completions`. Save `artifacts/ds4v-3/lane-2-carrots.json`. Pass when the answer names carrots. +- [ ] Lane 3. Second image. Describe the corn image through the same endpoint. Save `artifacts/ds4v-3/lane-3-corn.json`. Pass when the answer names corn. +- [ ] Lane 4. Missing projector. Send an image prompt without mmproj loaded. Save `artifacts/ds4v-3/lane-4-nommproj.json`. Pass when the server answers 400 cleanly. +- [ ] Lane 5. Text still works. Send the LRU prompt with mmproj loaded. Save `artifacts/ds4v-3/lane-5-text.json`. Pass when the response holds generated text. +- [ ] Lane 6. Placement proof. Read the server log for the owner lines. Save `artifacts/ds4v-3/lane-6-placement.log`. Pass when both devices appear as owners. +- [ ] Lane 7. Decode probe. Generate 128 tokens from the 2k prompt. Save `artifacts/ds4v-3/lane-7-decode.json`. Pass when decode exceeds 35 tok/s. +- [ ] Lane 8. Determinism. Send the same image prompt twice. Save `artifacts/ds4v-3/lane-8-repeat.json`. Pass when both answers match byte for byte. +- [ ] Lane 9. Build identity. Read `/props.build` from the running server. Save `artifacts/ds4v-3/lane-9-props.json`. Pass when the file names the expected image tag. +- [ ] Lane 10. Cleanup. Run the verify skill cleanup. Save `artifacts/ds4v-3/lane-10-cleanup.log`. Pass when the instance is gone and the proof files remain. + +**Verify, perf.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. + +- [ ] Metric. Decode tok/s on the 2k prompt with 128 output tokens, plus prefill tok/s on the same prompt. +- [ ] Probe. Run the DS4V-1 baseline script and the DS4V-3 vision script interleaved on the same pair. +- [ ] Baseline. Record the DS4V-1 value first. +- [ ] Rule. Vision head stays within budget. Fail when decode trails the text baseline by more than 20 percent. + +**Review gate.** None. DS4V-3 is not review-gated. + +**Merge.** + +- [ ] Root records a clean verdict at the exact head SHA. +- [ ] The owner rebases onto current trunk after the verdict with patch id unchanged. + +## Close the program + +- [ ] Every box above is checked with its evidence. +- [ ] Reply to the operator with the report the execution playbook names. + +## Appendix A. Prototype evidence + +The Lucebox report at `https://www.lucebox.com/blog/deepseek-v4-asymmetric-parallelism` measures 51 tok/s median decode with asymmetric expert parallelism. +PR 604 is merged. It adds the RX 7900 XT plus Strix Halo dual GPU profile with 45 to 47 tok/s decode. +HF API lists 4 prometheusAIR rungs from 66 GiB to 108 GiB plus a sub GiB mmproj file. +OrcaRouter parent keeps vision. Its GGUF is text only. +Unproven. No GPU run happened on this Mac. All throughput numbers above are cited, not measured here. + +## Appendix B. Alternatives rejected + +Re-abliterate from scratch. Rejected. The OrcaRouter parent already bakes the edit with measured evals. +OrcaRouter GGUF directly. Rejected. It drops the vision tower. +Unsloth quants directly. Rejected for now. First shards read empty at check time. +Qwen mmproj path in Lucebox. Rejected for DS4. Lucebox vision gates on Qwen35 only. + +## Appendix C. Risks + +VRAM budget. The 95 GiB rung plus KV at long context nears the pair budget. Watch the 1M context setting. +Upstream llama dot cpp drift. Vision support merged days ago. Pin the commit in the quant script. +Safety. Abliterated weights comply with harmful requests. Keep them local and never serve them publicly. +This checkout lacks `server/` sources. All GPU work runs on the Strix Halo machine with submodules present. + +## Appendix D. Links and reading list + +Read `skills/how/SKILL.md` before the placement review in DS4V-1. +Read `skills/interrogate/SKILL.md` before the quant recipe review in DS4V-3. +Keep the trail per `skills/show-me-your-work/SKILL.md`. +The verify surface is `.pi/skills/verify-lucebox/SKILL.md`. From c6030da7c57394559b2bedcd5f5fc660f71ea5f3 Mon Sep 17 00:00:00 2001 From: Marcelo Ribeiro Mendes Date: Thu, 10 Sep 2026 07:59:37 -0400 Subject: [PATCH 080/123] chore(ds4v): ship the vision qualification kit and reference receipts Adds harness/qualification/deepseek4/ds4v-vision/ so the DS4V vision numerical gate can be reproduced without rebuilding the measurement setup: the component-only and runtime qualification harnesses, the per-component qualification scripts with their review notes, the adopted same-GPU reference policy, the original-source reference tooling and environment receipts (constraints, libtorch/MIOpen linkage, script hashes), the frozen three-way comparison, and the native-hip-first and native-hip-scoped run summaries. Documents the current gate values, the open question of whether the target is the frozen same-GPU source or the absolute 0.9995 threshold, the next experiments, and the known gaps (reference images and guard journals are not included). Text only, about 0.5 MB. --- harness/qualification/README.md | 2 + .../deepseek4/ds4v-vision/README.md | 97 +++ .../ds4v-vision/capture-comparator-runtime.py | 27 + .../ds4v-vision/component-window-review.md | 24 + .../hip-attention-qualification.py | 314 +++++++++ .../hip-linear-qualification-review.md | 40 ++ .../ds4v-vision/hip-linear-qualification.sh | 248 +++++++ .../hip-lt-concurrent-qualification.md | 40 ++ .../hip-lt-concurrent-qualification.py | 230 +++++++ .../ds4v-vision/hip-lt-qualification.sh | 264 ++++++++ .../ds4v-vision/hip-lt-retry-qualification.py | 243 +++++++ .../ds4v-vision/hip-norm-qualification.py | 297 +++++++++ .../ds4v-vision/hip-qualification-README.md | 38 ++ .../ds4v-vision/hip-qualification.sh | 226 +++++++ .../ds4v-vision/hip-unbiased-qualification.py | 263 ++++++++ .../deepseek4/ds4v-vision/how-backend.md | 19 + .../deepseek4/ds4v-vision/how-source.md | 17 + .../ds4v-vision/mmproj-byte-proof.py | 48 ++ .../native-hip-scoped/comparison.exit | 1 + .../ds4v-vision/native-tower-brief.md | 27 + .../ds4v-vision/native-tower-rubric.md | 13 + .../ds4v-vision/patch-bias-diagnostic.py | 58 ++ .../ds4v-vision/reference-fixtures.py | 65 ++ .../ds4v-vision/runtime-qualification.sh | 51 ++ .../compare-corn-three-way.py | 55 ++ .../source-rocm-reference/constraints.txt | 4 + .../source-rocm-reference/cpu-runtime-info.py | 41 ++ .../evidence/cpu-corn.exit | 1 + .../evidence/cpu-corn.log | 83 +++ .../evidence/cpu-corn.time | 23 + .../evidence/cpu-runtime-info.stderr | 7 + .../evidence/cpu-runtime-private.stderr | 0 .../evidence/download.log | 19 + .../source-rocm-reference/evidence/freeze.txt | 15 + .../evidence/frozen-scripts.sha256 | 3 + .../evidence/install.log | 85 +++ .../evidence/libtorch-hip-dynamic.txt | 53 ++ .../evidence/libtorch-hip-ldd.txt | 45 ++ .../evidence/libtorch-hip-private-ldd.txt | 46 ++ .../evidence/miopen-apt-metadata.txt | 18 + .../evidence/miopen-apt-policy.txt | 6 + .../evidence/miopen-ldd.txt | 22 + .../evidence/miopen-repair.log | 4 + .../evidence/official-index.html | 140 ++++ .../evidence/offloading.exit | 1 + .../evidence/private-library-path.txt | 1 + .../reference-freeze-policy-copy-race.log | 5 + .../evidence/reference-freeze.log | 8 + .../source-carrots-first-controller.log | 68 ++ .../source-carrots-repeat-controller.log | 68 ++ .../source-corn-repeat-controller.log | 68 ++ .../evidence/torch-METADATA | 624 ++++++++++++++++++ .../evidence/torch-WHEEL | 5 + .../evidence/torch-url.txt | 1 + .../evidence/torch-version-static.txt | 10 + .../evidence/torch-wheel.sha256 | 1 + .../evidence/venv-config.txt | 5 + .../freeze-source-reference.py | 113 ++++ .../hip-control-initial-failure.md | 17 + .../hip-control-report.md | 51 ++ .../reference-stability-report.md | 59 ++ .../target-hip-qualification-policy-review.md | 42 ++ .../target-hip-qualification-policy.md | 33 + 63 files changed, 4502 insertions(+) create mode 100644 harness/qualification/deepseek4/ds4v-vision/README.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/component-window-review.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh create mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/how-backend.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/how-source.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/native-hip-scoped/comparison.exit create mode 100644 harness/qualification/deepseek4/ds4v-vision/native-tower-brief.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/native-tower-rubric.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/patch-bias-diagnostic.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/reference-fixtures.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/runtime-qualification.sh create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-private.stderr create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md create mode 100644 harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md diff --git a/harness/qualification/README.md b/harness/qualification/README.md index 34a378e4b..4c5d8ed8c 100644 --- a/harness/qualification/README.md +++ b/harness/qualification/README.md @@ -7,3 +7,5 @@ device settings and require machine-specific inputs. - `deepseek4/qualify_ds4_q5_amd.sh`: R9700 plus Strix Halo q=5 qualification - `deepseek4/rocprof_server_wrapper.sh`: delayed ROCm profiler launcher - `deepseek4/analyze_rocprof_overlap.py`: profiler overlap summary +- `deepseek4/ds4v-vision/`: DS4V vision numerical gate reproduction kit (harness, + reference environment receipts, frozen same-GPU reference tooling and run summaries) diff --git a/harness/qualification/deepseek4/ds4v-vision/README.md b/harness/qualification/deepseek4/ds4v-vision/README.md new file mode 100644 index 000000000..8406ca778 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/README.md @@ -0,0 +1,97 @@ +# DS4V vision qualification kit + +Reproduction material for the DS4V vision numerical gate, shipped so the work can be +continued without rebuilding the measurement setup. Context and the full narrative are in +`docs/ds4v-continuation-status.md` and `docs/ds4v-image-serving.md`. + +The scripts are reproduced verbatim from the operator host (Strix Halo + 7900 XT, Linux +ROCm). They carry hard-coded host paths and their own idle/resource guards; read +`hip-qualification-README.md` before running anything. + +## Where the gate stands + +Against the frozen same-GPU original-source reference, thresholds unchanged: + +| Check | Result | Gate | +|---|---|---| +| Embeddings corn / carrots | 0.999525047 / 0.999721786 | pass | +| Corn features (cosine) | 0.999063593 | fail (0.9995) | +| Corn features (maxabs) | 0.947265625 | fail | +| Carrots features | cosine 0.999538399 pass, maxabs 0.26416015625 | fail (maxabs) | + +Earlier and retained results: + +- first native HIP run, runtime `4bf7270`: corn 0.985939, carrots 0.993884, embeddings also + below gate, harness exit 3 (`native-hip-first/`). +- scoped biased-linear-rounding fix `be8b0f1` (biases promoted to F32 operands on GPU + backends only) produced the passing embeddings and the corn cosine above + (`native-hip-scoped/`). +- CPU portability is a separate retained failure: original CPU corn feature cosine + 0.99822935. +- three-way comparison (`source-rocm-reference/hip-supervision-confirmed/three-way.json`): + the original-source corn HIP forward also fails the unchanged CPU gate (0.997729789, + maxabs 2.73828125), and native HIP fails against source HIP (features 0.991196939, + embeddings 0.994697537). CPU/GPU portability therefore does not explain the native + discrepancy. + +## Contents + +- `hip-qualification.sh`, `runtime-qualification.sh` - component-only and runtime windows + with the idle, port, KFD and memory guard. +- `hip-linear-qualification.sh`, `hip-attention-qualification.py`, + `hip-norm-qualification.py`, `hip-unbiased-qualification.py`, `hip-lt-qualification.sh`, + `hip-lt-concurrent-qualification.py`, `hip-lt-retry-qualification.py` - per-component + qualification, each with its review note where one exists. +- `target-hip-qualification-policy.md` (+ review), `component-window-review.md` - the + adopted same-GPU reference policy and the window review. +- `how-source.md`, `how-backend.md`, `native-tower-brief.md`, `native-tower-rubric.md` - + the source/backend contracts and the tower acceptance rubric. +- `source-rocm-reference/` - reference environment receipts (`constraints.txt`, + `cpu-runtime-info.py`, `libtorch-hip-*ldd.txt`, MIOpen receipts, script hashes), + the freeze and comparison tooling (`freeze-source-reference.py`, + `compare-corn-three-way.py`) and the control reports. +- `native-hip-first/`, `native-hip-scoped/` - the run summaries and comparisons. +- `mmproj-byte-proof.py`, `reference-fixtures.py`, `capture-comparator-runtime.py`, + `patch-bias-diagnostic.py` - supporting checks. + +## Reproducing + +1. Pin the reference environment from `source-rocm-reference/constraints.txt` and rebuild + the original-source Torch/ROCm control (see `source-rocm-reference/evidence/` for the + exact library and package receipts). +2. Freeze original-source corn and carrots outputs on the same device + (`freeze-source-reference.py`) and confirm repeat stability. +3. Run the native component window (`hip-qualification.sh --component-only`) under the + guard, then the corrected full-tower run. +4. Compare with `compare-corn-three-way.py`; no threshold is adjusted between runs. + +## Open decision: what counts as done + +The adopted policy keeps every numeric threshold unchanged, but the original-source corn +HIP forward itself fails the CPU feature gate. "Match the frozen same-GPU source" and +"pass 0.9995" are therefore different targets, and the residual corn deviation sits +between them. This needs an owner decision before the tower can be declared qualified. + +## Next experiments + +- Capture the two preselected real corn projections (patch projection, block-0 QKV) and + compare them against the frozen source outputs. The synthetic biased projection already + matches the original GPU source exactly at 1024/1024 values with the source's + first-heuristic and 76 MiB workspace configuration + (`artifacts/ds4v-step2/hipblaslt-real-projections/`, not in this kit). +- The residual is sparse rather than structural: corn cosine is close to the gate while + maxabs is 0.947265625, which points at per-element tie and product-rounding behaviour. + The direct fused source biased output still differs in 88 tiny tie cases, so a fused + versus unfused rounding contract is the leading candidate. +- The sensitive-block diagnostic found no semantic or BF16-boundary discrepancy for corn + blocks 12 and 31, and a single-thread original-source control reproduced the two-thread + reference bitwise. + +## Known gaps + +- The two source images (`corn.jpeg`, `carrots.jpeg`) are not in this repository and the + frozen reference outputs are tied to them. They were deleted from the reference host; + they can be recovered from the base64 payloads in the trial captures or provided on + request. +- Guard journals, per-run `guard.json` snapshots and HTTP captures are not included here. +- No production image-chat acceptance has been run against this tower state. diff --git a/harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py b/harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py new file mode 100644 index 000000000..1c27f3891 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py @@ -0,0 +1,27 @@ +"""Read-only CPU comparator runtime inventory; run only on soulf.""" +import hashlib +import json +import pathlib +import subprocess +import sys +import numpy + +assert sys.flags.isolated +root = pathlib.Path(numpy.__file__).parent +files = {pathlib.Path(sys.executable).resolve()} +files.update(root.rglob('*.py')) +files.update(root.rglob('*.so')) +files.update((root.parent / 'numpy.libs').glob('*')) +for path in list(files): + if path.suffix == '.so' or path.name.startswith('python'): + result = subprocess.run(['/usr/bin/ldd', str(path)], text=True, capture_output=True, check=True) + assert 'not found' not in result.stdout + for line in result.stdout.splitlines(): + parts = line.split() + name = parts[2] if len(parts) > 2 and parts[1] == '=>' else parts[0] if parts else '' + if name.startswith('/'): + files.add(pathlib.Path(name).resolve()) +print(json.dumps({'python': sys.version, 'numpy': numpy.__version__, 'files': { + str(path): hashlib.file_digest(path.open('rb'), 'sha256').hexdigest() + for path in sorted(files) if path.is_file() +}}, indent=2)) diff --git a/harness/qualification/deepseek4/ds4v-vision/component-window-review.md b/harness/qualification/deepseek4/ds4v-vision/component-window-review.md new file mode 100644 index 000000000..364f96b1a --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/component-window-review.md @@ -0,0 +1,24 @@ +# Standalone component GPU-window review + +**PASS** for the narrow `--component-only` mode at local/deployed harness SHA256 `152af330bb37f77f8e6f9c795ae46c12f787e8587c42fc210aeb671b205296d5`. Reviewed `/Users/marcelorm/workspace/lucebox/artifacts/ds4v-step2/hip-qualification.sh` and the matching `/tmp/ds4v-hip-qualification.sh` on soulf. No harness/probe/GPU execution, source edit, service action or converter action occurred. + +The mode skips only the completed-text-proof prerequisite for an explicitly released standalone component window. It still requires the explicit release argument, fresh evidence, pinned runtime/binary/libraries/mmproj/fixtures and actual HIP device identity. Its summary records `component_only=true`, no text proof, and `standalone component; no chat/server acceptance`. The normal text-proof mode still validates its completed proof. Fixed comparisons, exit3 preservation, sequential hip0 probes and owned-child cleanup remain intact. + +`idle_window()` fails closed on an active operator/nonzero MainPID, either TCP listener8016/8217, any KFD compute process, unavailable sysfs data, less than8GiB host available memory, missing/ambiguous discrete card, or less than8GiB free discrete VRAM. It executes once before evidence creation and again immediately before the first GPU probe, after provenance hashing. These are readiness snapshots within the parent's explicitly released window, not an interprocess GPU reservation. + +CPU-only validation used the existing immutable reference venv with `python -I` under the exact clean HOME/PATH/locale/two-thread environment. Bash syntax and complete embedded Python AST parsing passed. Only AST-extracted `check` and `idle_window` function definitions were executed; no other harness statements or imports of model/GPU libraries ran. + +Both independent idle-window invocations returned: + +```text +ActiveState=inactive +MainPID=0 +8016/8217 TCP listeners absent +KFD process directory empty +host_available_bytes=36079882240 +discrete_free_vram_bytes=21430087680 +``` + +The user-service query works in that clean environment with only `XDG_RUNTIME_DIR=/run/user/` added; no inherited DBus variable was needed. The `card[0-9]*/device` glob also visits DRM connector entries, but their nested `device` is a directory, so the `is_file()` predicate correctly excludes them. Exactly `/sys/class/drm/card0/device` has PCI ID `0x744c`; card1 is `0x1586`. The discrete card resolves to PCI `0000:c6:00.0`. No duplicate match or DBus issue was found. + +This approves the prepared component-window gate under the parent's revised ordering. It claims no numerical/HIP result or full-chat acceptance, and does not authorize driving the operator service. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py new file mode 100644 index 000000000..61b118b20 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py @@ -0,0 +1,314 @@ +"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. + +This supervises three sequential Radeon-only lanes through the separate live +operator guard. It makes no isolated performance or HTTP acceptance claim. +""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import signal +import stat +import subprocess +import sys + +HOME = Path('/home/marcelorm') +ROOT = HOME / 'lucebox-ds4v-vision-attention' +BUILD = Path('/tmp/ds4v-attention-hip-build') +SOURCE = '686285f092c961423747a8c961a509767aec1017' +REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' +CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' +PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' +SUPERVISOR = HOME / 'ds4v-work/hipblaslt-stage-diagnostic/log-snapshot-guard.py' +SUPERVISOR_SHA = '8591d28b0b11a1531fe2a657080958cbf401fda9a19dd18d747c09e5edbd06cb' +LANES = ['tiny', 'patch', 'qkv', 'mlp_w1', 'mlp_w2', 'norm782', 'norm2562', 'rotary', 'softmax', 'attention'] +FIXED = { + SUPERVISOR: SUPERVISOR_SHA, + BUILD / 'ds4v_vision_probe': 'd9ad23010e8dae30623442b94697587ca7c160824b2fe5c48ddaa6f6a380949d', + COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', + CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', +} +LIBRARIES = { + 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', 'e14f36e6e2ad059e404c658d307b03a5c20cfbf57ea3ded00049ee872e080f86'), + 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '91d2d975096a3a4597f033ee2250d0df092be1493e212d9bf84ce8099c443590'), + 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '67debd0e1638230a6f2c94690483b660bff9e071e41eeef61f35d386cdfcc817'), + 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', '60cc08a311f0ad9121a8760adac132c46cd028bb8070acd55632e50c89dee397'), + 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), + 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), +} + +def require(ok, why): + if not ok: + raise RuntimeError(why) + +def digest(path): + with Path(path).open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + +def verify_pins(pins): + for path, sha in pins.items(): + require(re.fullmatch('[0-9a-f]{64}', sha) is not None, 'unbound or invalid pin: ' + str(path)) + require(digest(path) == sha, 'pin changed: ' + str(path)) + +def guarded_run(command, log): + child = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT) + previous = {} + def interrupted(signum, frame): + raise InterruptedError(f'qualification interrupted: {signum}') + try: + for signum in (signal.SIGTERM, signal.SIGINT): + previous[signum] = signal.signal(signum, interrupted) + require(child.wait() == 0, 'lane supervision failed') + finally: + # The guard handles SIGTERM by stopping/reaping its direct GPU child. + # Never kill the guard while it might still own a live GPU process. + if child.poll() is None: + child.terminate() + child.wait(timeout=30) + for signum, handler in previous.items(): + signal.signal(signum, handler) + +def attention_counts(name): + softmax = 6 if name == 'softmax' else 1 if name == 'attention' else 0 + av = 1 if name == 'attention' else 0 + return {'explicit_softmax_ops': softmax, 'actual_softmax_launches': softmax, + 'explicit_av_ops': av, 'actual_av_launches': av, + 'actual_rotary_launches': 1 if name in ('rotary', 'attention') else 0} + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=Path, required=True) + parser.add_argument('--config-sha', required=True) + parser.add_argument('--parent-radeon-window-released', action='store_true') + args = parser.parse_args() + require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') + require(re.fullmatch('[0-9a-f]{40}', SOURCE) is not None, 'source commit is not bound') + require(digest(args.config) == args.config_sha, 'config changed') + cfg = json.loads(args.config.read_text()) + require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') + production_path = Path(cfg['production_pins']['path']) + production_sha = cfg['production_pins']['sha256'] + require(re.fullmatch('[0-9a-f]{64}', production_sha) is not None + and digest(production_path) == production_sha, 'production pin manifest changed') + production = json.loads(production_path.read_text()) + require(production.get('schema') == 'ds4v-attention-production-runtime-v1' + and production.get('source_root') == str(ROOT) + and production.get('source_commit') == SOURCE and isinstance(production.get('files'), dict) + and production['files'], 'production source/file pins missing') + linear_path = Path(cfg['production_receipt']['path']) + require(digest(linear_path) == cfg['production_receipt']['sha256'], 'linear acceptance receipt changed') + linear = json.loads(linear_path.read_text()) + require(linear.get('schema') == 'ds4v-attention-native-production-proof-v1' and linear.get('pass') is True + and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') + require(linear['source_commit'] == SOURCE, 'linear source mismatch') + require(linear['pins_sha256'] == production_sha, 'linear production pins mismatch') + require([x['name'] for x in linear['lanes']] == LANES, 'ten production prerequisite lanes required') + require(linear['supervisor'] == production['supervisor'] == {'path': str(SUPERVISOR), 'sha256': SUPERVISOR_SHA}, + 'logging supervisor differs from accepted prerequisites') + require(linear['source_norm'] == production['source_norm'], 'normalization source provenance mismatch') + require(linear['source_attention'] == production['source_attention'], 'attention source provenance mismatch') + require(linear['source_acceptance'] == production['source_acceptance'] == { + 'path': str(SUPERVISOR.parent / 'unbiased-source-acceptance-v1.json'), + 'sha256': '1d0d59f2b3ffc21a366df50ef7257fe6ac2a1eca78cbea10988df3f2398bdbfa'}, 'source acceptance changed') + for lane in linear['lanes']: + require(lane['guard_exit'] == 0 and lane['child_exit'] == 0 + and lane['component_numeric_pass'] is True, 'linear lane not accepted') + require(lane['source_bitwise_mismatches'] == 0, 'production linear differs from frozen source') + norm = lane['name'].startswith('norm') + expected_dispatch = 0 if norm or lane['name'] in ('rotary', 'softmax', 'attention') else 1 if lane['name'].startswith('mlp_') else 2 + require(lane['explicit_ops'] == lane['actual_lt_launches'] == expected_dispatch, + 'wrong production linear dispatch count') + require(lane['explicit_norm_ops'] == lane['actual_norm_launches'] == (1 if norm else 0), + 'wrong production normalization dispatch count') + for field, expected in attention_counts(lane['name']).items(): + require(lane[field] == expected, 'wrong production attention dispatch count: ' + field) + if norm: + require(lane['reference_layout'] == ('original' if lane['name'] == 'norm782' else 'rowwise-tiled-original'), + 'normalization reference layout changed') + require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') + pins = dict(FIXED) + pins[args.config] = args.config_sha + pins[linear_path] = cfg['production_receipt']['sha256'] + pins[production_path] = production_sha + pins.update({path: sha for path, sha in LIBRARIES.values()}) + for path, sha in production['files'].items(): + require(Path(path).is_absolute(), 'absolute production file pin required') + require(Path(path) not in pins or pins[Path(path)] == sha, 'production pin conflicts with qualification pin: ' + path) + pins[Path(path)] = sha + for path, sha in linear['input_artifact_hashes'].items(): + require(Path(path).is_absolute() and (Path(path) not in pins or pins[Path(path)] == sha), + 'prerequisite provenance conflicts with qualification pins') + pins[Path(path)] = sha + for lane in linear['lanes']: + pins[Path(lane['guard_report_path'])] = lane['guard_report_sha256'] + runtime = Path(cfg['comparator_runtime']) + policy = Path(cfg['qualification_policy']) + pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', + policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) + verify_pins(pins) + for path, sha in json.loads(runtime.read_text())['files'].items(): + require(Path(path) not in pins or pins[Path(path)] == sha, 'comparator runtime pin conflict') + pins[Path(path)] = sha + for directory in (REFERENCE, CPU_REFERENCE): + manifest = json.loads((directory / 'manifest.json').read_text()) + require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') + for label, entry in manifest['images'].items(): + require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') + for stage in ('patches', 'features', 'embeddings'): + path = directory / entry[stage]['file'] + require(path.parent == directory, 'fixture path escapes reference') + pins[path] = entry[stage]['sha256'] + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) + require('not found' not in ldd, 'unresolved dependency') + resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) + for soname, (path, _) in LIBRARIES.items(): + # The standalone probe links the backend libraries directly; the umbrella + # libggml is built/pinned but omitted by the linker's --as-needed rule. + if soname == 'libggml.so.0' and soname not in resolved: + continue + require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) + verify_pins(pins) + # Guard code and lane policies are frozen before importing or invoking them. + guard_dir = Path(cfg['guard_dir']) + require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') + pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) + verify_pins(pins) + require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') + sys.path.insert(0, str(guard_dir)) + from run import load_policy, launch_command + from host import Host + from guard import prepare_preflight + require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') + policies = [] + for lane in cfg['lanes']: + p = load_policy(Path(lane['policy']), lane['sha256']) + pins[Path(lane['policy'])] = lane['sha256'] + isolation_pins = p.get('isolation_pins', {}) + require('/usr/bin/python3.12' in isolation_pins and + all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), + 'namespace runtime is not included in guarded component pins') + for path, sha in p['component_pins'].items(): + require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) + pins[Path(path)] = sha + label = lane['name'].split('-')[0] + h, w = (42, 61) if label == 'carrots' else (23, 34) + out = guard_dir / p['run_name'] + expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), + str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] + require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') + require('hip_vision_linear_launches=131 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') + require('hip_vision_norm_launches=65' in p['required_log_lines'], 'normalization dispatch contract missing') + for name, count in (('rotary', 1), ('softmax', 32), ('av', 32)): + require('hip_vision_' + name + '_launches=' + str(count) in p['required_log_lines'], + 'attention dispatch contract missing: ' + name) + require(p['component_pins'].get(str(SUPERVISOR)) == SUPERVISOR_SHA, 'logging supervisor is not pinned') + sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, + str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} + require(set(p['required_outputs']) == set(sizes), 'wrong output contract') + require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') + policies.append((p, out, label)) + require(len({str(out) for _, out, _ in policies}) == 3, 'full lane evidence directories must be distinct') + evidence = Path(cfg['evidence']) + require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') + evidence.mkdir() + report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', + 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), + 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'production_receipt': cfg['production_receipt'], + 'production_pins': cfg['production_pins'], 'supervisor': production['supervisor'], 'lanes': []} + try: + for lane, (p, out, label) in zip(cfg['lanes'], policies): + verify_pins(pins) + supervisor_log = evidence / (lane['name'] + '-supervisor.log') + with supervisor_log.open('x') as log: + guarded_run([str(PYTHON), '-I', '-B', str(SUPERVISOR), '--policy', lane['policy'], + '--policy-sha', lane['sha256'], '--parent-radeon-window-released'], log) + pins[supervisor_log] = digest(supervisor_log) + result = json.loads((out / 'guard.json').read_text()) + require(result['pass'] and result['device_proof_verified'], 'guard/result failed') + require(result.get('namespace_verified') is True, 'private NPU namespace not verified') + require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') + require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] + and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', + 'guard report belongs to a different command/policy/outcome') + require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') + for file, meta in result['output_evidence'].items(): + require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') + require(digest(file) == meta['sha256'], 'guard output changed before copying') + pins[Path(file)] = meta['sha256'] + pins[out / 'guard.json'] = digest(out / 'guard.json') + log = (out / 'child.log').read_text() + require(re.findall(r'^hip_vision_linear_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('131', '79691776')], 'ambiguous/incomplete full-tower dispatch') + require(re.findall(r'^hip_vision_norm_launches=(\d+)$', log, re.M) == ['65'], + 'ambiguous/incomplete full-tower normalization dispatch') + for name, count in (('rotary', 1), ('softmax', 32), ('av', 32)): + require(re.findall(r'^hip_vision_' + name + r'_launches=(\d+)$', log, re.M) == [str(count)], + 'ambiguous/incomplete full-tower attention dispatch: ' + name) + pins[out / 'child.log'] = digest(out / 'child.log') + report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], + 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], + 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], + 'command': p['command'], 'launch_command': result['launch_command'], + 'namespace_verified': result['namespace_verified'], + 'actual_lt_launches': 131, 'actual_norm_launches': 65, + 'actual_rotary_launches': 1, 'actual_softmax_launches': 32, 'actual_av_launches': 32, + 'supervisor_log_path': str(supervisor_log), 'supervisor_log_sha256': pins[supervisor_log], + 'child_exit': result['exit'], 'outputs': result['output_evidence']}) + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for index, (_, out, label) in enumerate(policies): + for stage in ('features', 'embeddings'): + original = out / f'{label}-{stage}.f32' + copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied output differs from guarded output') + pins[copied] = pins[original] + for stage in ('features', 'embeddings'): + original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied repeat carrots differ') + pins[copied] = pins[original] + codes = {} + for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), + ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: + verify_pins(pins) + with (evidence / f'{name}.log').open('w') as log: + result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], + stdout=log, stderr=subprocess.STDOUT, timeout=120) + require(result.returncode in (0, 3), 'comparator execution failed: ' + name) + codes[name] = result.returncode + report['comparisons'] = codes + report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) + verify_pins(pins) + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + p = policies[-1][0] + host = Host(p) + fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) + try: + st = os.fstat(fd) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) + finally: + os.close(fd) + (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') + report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] + except Exception as error: + report['error'] = f'{type(error).__name__}: {error}' + finally: + (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') + print(json.dumps(report, indent=2)) + return 0 if report['pass'] else 3 + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md b/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md new file mode 100644 index 000000000..34a845407 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md @@ -0,0 +1,40 @@ +# Scoped HIP linear qualification harness review + +**Verdict: PASS for execution only after the scoped tiny regression and CPU +identity gates pass and the parent explicitly releases the GPU lane.** Reviewed +`hip-linear-qualification.sh` at SHA256 +`8d3e34a5df237da0e4bb3098ff03e1d6694e91d547700879e927ddb2ff5c6806`. +This is a read-only harness review, not a tower qualification result. + +The harness pins candidate source `be8b0f1b07f1a3a034ce1d7333fd0d3402754c60`, +probe `79f4928a5172001eef205b100d7578f77560a566d4179fe6755951a15438b483`, +the three reused GGML libraries, projector, comparator, original CPU manifest, +canonical 7900 XT source manifest and its freeze. Source policy SHA256 +`62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f` +matches the adopted prospective policy. The frozen target manifest and freeze +are `677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86` +and `8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0`. +They record first-output selection, byte-identical source repeats for both images, +the exact runner and device, and unchanged thresholds before candidate execution. + +The revised integrity boundary is complete. It hashes all target and original CPU +patch, feature and embedding bytes before the first GPU operation, includes both +sets in provenance, and rehashes both after all comparisons. It also rechecks all +pinned files. Candidate execution uses only the frozen target patches. The two +original-CPU comparisons are reported separately and their numerical exit 3 does +not control target acceptance. + +Target acceptance requires both the first and repeat comparisons to complete +normally and apply every unchanged feature and embedding gate to both images. +Corn output must also repeat byte for byte. Either target comparator exit 3 or a +repeat mismatch produces final exit 3. Execution, shape, hash, load, device or +timeout failures remain unqualified. Once a target comparator records numerical +exit 3, the finalizer cannot turn it into success or hide it behind a later error. + +The idle-window, exact device, no-fallback, owned-child cleanup, timeout and fresh +evidence-directory controls are retained from the previously reviewed harness. +The run remains scoped to the 7900 XT tower and does not qualify CPU portability, +gfx1151, HTTP image behavior, the decoder, text regression or full serving. + +No build, model execution, GPU operation, server action or source edit was made +during this review. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh b/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh new file mode 100644 index 000000000..ebd5f7d88 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env bash +# Scoped GPU candidate; execute only after tiny-regression PASS and explicit lane release. +# Standalone component checks may precede text proof in an idle, explicitly released GPU window. +set -euo pipefail +[[ $(hostname) = soulf && $# = 3 && $3 = --gpu-window-released ]] || { + echo 'Usage on soulf: hip-qualification.sh COMPLETED_TEXT_PROOF_OR_--component-only NEW_EVIDENCE_DIR --gpu-window-released' >&2; exit 2; +} +python="$HOME/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python" +# No inherited device masks, overrides, GGML/DS4/DFLASH controls, LD_PRELOAD, or Python settings. +exec env -i HOME="$HOME" PATH=/usr/bin:/bin LANG=C LC_ALL=C \ + OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MKL_NUM_THREADS=2 \ + "$python" -I - "$1" "$2" "$(realpath "$0")" <<'PY' +import hashlib, json, os, re, signal, subprocess, sys, time +from pathlib import Path + +home = Path.home() +root = home / 'lucebox-ds4v-linear-rounding' +build = Path('/tmp/ds4v-linear-rounding-scoped-build') +ggml_build = Path('/tmp/ds4v-runtime-hip-build') +binary = build / 'ds4v_vision_probe' +cpu_reference = home / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +reference = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference') +freeze_manifest = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json') +policy_sha = '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f' +mmproj = home / 'ds4v-work/ds4v-mmproj.gguf' +compare = root / 'server/tools/ds4v_vision/compare.py' +component_only = sys.argv[1] == '--component-only' +text_proof = None if component_only else Path(sys.argv[1]) +evidence, harness = map(Path, sys.argv[2:]) +source_sha = 'be8b0f1b07f1a3a034ce1d7333fd0d3402754c60' +pinned = { + binary: '79f4928a5172001eef205b100d7578f77560a566d4179fe6755951a15438b483', + compare: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + mmproj: '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + cpu_reference / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + reference / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', + freeze_manifest: '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', + ggml_build / 'ggml/src/libggml-base.so.0': '378b6c81052532d19bd86b32de236553cc1e57f824ce35fb733569470786ed29', + ggml_build / 'ggml/src/libggml-cpu.so.0': 'b33faf3a600eeff2bea8b692360cff6de397aaf3082ea0c73a9f3a1af9ee70d2', + ggml_build / 'ggml/src/ggml-hip/libggml-hip.so.0': 'b8991450ee422983b91cfbfcdf8d6b612e92f62f1128c6cce0c6b3e37ff8ef7e', +} + +def check(ok, message): + if not ok: raise RuntimeError(message) + +def digest(path): + with open(path, 'rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() + +def dump(name, value): + (evidence / name).write_text(json.dumps(value, indent=2) + '\n') + +def idle_window(): + bus_env = dict(os.environ, XDG_RUNTIME_DIR=f'/run/user/{os.getuid()}') + state = subprocess.check_output(['systemctl', '--user', 'show', 'deepseek-dflash.service', + '--property=ActiveState', '--property=MainPID'], env=bus_env, text=True) + properties = dict(line.split('=', 1) for line in state.splitlines()) + check(properties.get('ActiveState') in ('inactive', 'failed') and properties.get('MainPID') == '0', + 'operator service is not down') + listeners = subprocess.check_output(['ss', '-ltn'], text=True) + check(not any(len(row.split()) > 3 and row.split()[3].endswith((':8016', ':8217')) + for row in listeners.splitlines()), 'operator or private text port is occupied') + processes = list(Path('/sys/class/kfd/kfd/proc').iterdir()) + check(not processes, 'GPU compute processes already exist') + available = int(next(row.split()[1] for row in Path('/proc/meminfo').read_text().splitlines() + if row.startswith('MemAvailable:'))) * 1024 + check(available >= 8 * 1024**3, 'less than 8 GiB host memory available for component check') + cards = [p for p in Path('/sys/class/drm').glob('card[0-9]*/device') + if (p / 'device').is_file() and (p / 'device').read_text().strip() == '0x744c'] + check(len(cards) == 1, 'expected exactly one RX 7900 XT device') + free_vram = int((cards[0] / 'mem_info_vram_total').read_text()) - int((cards[0] / 'mem_info_vram_used').read_text()) + check(free_vram >= 8 * 1024**3, 'less than 8 GiB discrete VRAM available') + return {'operator': properties, 'kfd_processes': [], 'host_available_bytes': available, + 'discrete_free_vram_bytes': free_vram} + +# No GPU call or evidence mutation before the explicit mode and idle-window gates. +if not component_only: + text_proof = text_proof.resolve(strict=True) + allowed = [home / f'{tree}/artifacts/fitter-fix' for tree in + ('lucebox-ds4v-mix-fix', 'lucebox-ds4v-mix-parallel')] + check(text_proof.parent in allowed and text_proof.name.startswith('load-proof-'), 'unexpected text-proof directory') + check((text_proof / 'harness.exit').read_text().strip() == '0', 'private text proof did not complete successfully') + check(bool((text_proof / 'cleanup.txt').read_text().strip()), 'private text proof cleanup missing') + verdict = json.loads((text_proof / 'verdict.json').read_text()) + check(all(verdict.get(k) == 'PASS' for k in ('text_load_smoke', 'math_answer', 'longer_decode_speculation')), + 'private text verdict is not PASS') + server_pid = int((text_proof / 'server.pid').read_text()) + check(server_pid > 1 and not Path(f'/proc/{server_pid}').exists(), 'private text server PID remains present') +initial_window = idle_window() +check(evidence.is_absolute() and not evidence.exists(), 'provide a fresh absolute evidence directory') +evidence.mkdir() # Parent must already exist; never remove or reuse evidence. +print(f'Evidence: {evidence}', flush=True) +active = None +summary = {'status': 'ISSUES', 'features': 'ISSUES', 'embeddings': 'NOT_QUALIFIED', + 'source_sha': source_sha, 'device': 'hip:0', 'text_proof': str(text_proof) if text_proof else None, + 'scope': '7900XT target-source fidelity only; CPU portability separate; no chat/server acceptance', 'policy_sha256': policy_sha, 'reference': str(reference), 'component_only': component_only, + 'initial_window': initial_window, 'lanes': []} +exit_code = 1 + +def interrupted(signum, frame): + raise InterruptedError(signum) + +signal.signal(signal.SIGINT, interrupted) +signal.signal(signal.SIGTERM, interrupted) + +def memory(): + result = {'monotonic_seconds': time.monotonic(), 'host': Path('/proc/meminfo').read_text()} + result['drm'] = {str(p): p.read_text().strip() for p in Path('/sys/class/drm').glob('card*/device/mem_info_*') + if p.name in ('mem_info_vram_total', 'mem_info_vram_used', 'mem_info_gtt_used')} + return result + +def stop_owned(): + global active + if active is not None: + # This unreaped direct child cannot have its PID reused. Never pkill or signal other processes. + active.terminate() + try: active.wait(timeout=5) + except subprocess.TimeoutExpired: active.kill(); active.wait() + lane = summary['lanes'][-1] + lane.update(exit=active.returncode, stopped_by_harness=True) + (evidence / f"{lane['name']}.exit").write_text(str(active.returncode) + '\n') + dump(f"{lane['name']}.time.json", lane) + active = None + +def run(name, command, timeout=900): + global active + lane = {'name': name, 'command': list(map(str, command))} + summary['lanes'].append(lane) + started = time.monotonic() + with open(evidence / f'{name}.log', 'w') as log, open(evidence / f'{name}.memory.jsonl', 'w') as samples: + active = subprocess.Popen(lane['command'], stdout=log, stderr=subprocess.STDOUT) + lane['pid'] = active.pid + (evidence / f'{name}.pid').write_text(str(active.pid) + '\n') + dump('summary.json', summary) + while True: + pid, status, usage = os.wait4(active.pid, os.WNOHANG) + if pid: + code = os.waitstatus_to_exitcode(status) + active.returncode = code + active = None + lane.update(exit=code, elapsed_seconds=time.monotonic()-started, + user_seconds=usage.ru_utime, system_seconds=usage.ru_stime, max_rss_kib=usage.ru_maxrss) + (evidence / f'{name}.exit').write_text(str(code) + '\n') + dump(f'{name}.time.json', lane) + dump('summary.json', summary) + return code + sample = memory() + try: sample['process_status'] = Path(f'/proc/{active.pid}/status').read_text() + except FileNotFoundError: pass + samples.write(json.dumps(sample) + '\n'); samples.flush() + if time.monotonic()-started > timeout: raise TimeoutError(f'{name} exceeded {timeout}s') + time.sleep(0.5) + +def verify_device(name): + log = (evidence / f'{name}.log').read_text() + check(re.search(r'^backend=ROCm0 requested=hip:0$', log, re.M), f'{name}: not the requested HIP backend') + check(re.search(r'Device 0: [^\n]*7900 XT[^\n]*gfx1100', log), f'{name}: device0 is not 7900 XT/gfx1100') + check(re.search(r'Device 1: [^\n]*gfx1151', log), f'{name}: device1 order changed') + +try: + check(subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() == source_sha, + 'runtime source commit changed') + subprocess.run(['git', '-C', str(root), 'diff', '--quiet', 'HEAD', '--'], check=True) + for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed: {path}') + manifest = json.loads((reference / 'manifest.json').read_text()) + check(set(manifest['images']) == {'carrots', 'corn'}, 'unexpected fixture set') + fixtures = {} + for label, grid in [('carrots', [42, 61]), ('corn', [23, 34])]: + entry = manifest['images'][label] + check(entry['vit_grid'] == grid, f'{label}: grid changed') + for stage in ('patches', 'features', 'embeddings'): + meta = entry[stage]; path = reference / meta['file'] + check(path.parent == reference and digest(path) == meta['sha256'], f'{label}/{stage}: fixture changed') + fixtures[str(path)] = meta + cpu_fixtures = {} + cpu_manifest = json.loads((cpu_reference / 'manifest.json').read_text()) + check(set(cpu_manifest['images']) == {'carrots', 'corn'}, 'unexpected CPU fixture set') + for label, entry in cpu_manifest['images'].items(): + for stage in ('patches', 'features', 'embeddings'): + meta = entry[stage]; path = cpu_reference / meta['file'] + check(path.parent == cpu_reference and digest(path) == meta['sha256'], f'CPU fixture changed: {label}/{stage}') + cpu_fixtures[str(path)] = meta + ldd = subprocess.check_output(['/usr/bin/ldd', str(binary)], text=True) + check('not found' not in ldd and 'libggml-hip.so' in ldd, 'HIP dependency resolution failed') + (evidence / 'ldd.txt').write_text(ldd) + libraries = sorted(set(re.findall(r'(?:=>\s+|^\s*)(/[^\s]+)\s+\(', ldd, re.M))) + software = {path: digest(path) for path in libraries} + software[str(Path(sys.executable).resolve())] = digest(Path(sys.executable).resolve()) + dump('provenance.json', {'source_sha': source_sha, 'pinned': {str(p): s for p, s in pinned.items()}, + 'shared_libraries_and_python': software, 'fixtures': fixtures, 'cpu_fixtures': cpu_fixtures, + 'environment': dict(os.environ), 'python': sys.version, 'harness_sha256': digest(harness), + 'text_proof_files': {p.name: digest(p) for p in text_proof.iterdir() if p.is_file()} if text_proof else {}}) + dump('memory-before.json', memory()) + dump('window-before-gpu.json', idle_window()) + check(run('device-check', [binary, mmproj, '--load-only', '4096', '129280', 'hip:0']) == 0, + 'HIP load-only/device check failed') + verify_device('device-check') + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for label, h, w, output, lane in [('carrots', 42, 61, native, 'carrots'), + ('corn', 23, 34, native, 'corn'), ('corn', 23, 34, repeat, 'corn-repeat')]: + check(run(lane, [binary, mmproj, reference / f'{label}-patches.f32', str(h), str(w), output, label, '0', 'hip:0']) == 0, + f'{lane}: HIP encode failed') + verify_device(lane) + # compare.py requires both fixture names: explicitly reuse the first carrots result in repeat comparison. + for stage in ('features', 'embeddings'): + (repeat / f'carrots-{stage}.f32').symlink_to(native / f'carrots-{stage}.f32') + cpu_statuses = [run(name, [sys.executable, '-I', compare, cpu_reference, output, '--output', evidence / f'{name}.json']) + for name, output in [('native-vs-cpu', native), ('source-hip-vs-cpu', reference)]] + check(all(code in (0, 3) for code in cpu_statuses), 'CPU portability comparison execution/shape/hash failure') + summary['cpu_portability'] = dict(zip(('native_vs_cpu', 'source_hip_vs_cpu'), + ('PASS' if code == 0 else 'ISSUES' for code in cpu_statuses))) + statuses = [run(name, [sys.executable, '-I', compare, reference, output, '--output', evidence / f'{name}.json']) + for name, output in [('comparison', native), ('repeat-comparison', repeat)]] + check(all(code in (0, 3) for code in statuses), 'comparison execution/shape/hash failure') + comparisons = [json.loads((evidence / f'{name}.json').read_text()) for name in ('comparison', 'repeat-comparison')] + summary['features'] = 'PASS' if all(c[label]['features']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' + summary['embeddings'] = 'PASS' if all(c[label]['embeddings']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' + summary['corn_repeat_byte_identical'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') + for s in ('features', 'embeddings')) + dump('outputs.sha256.json', {str(p.relative_to(evidence)): digest(p) for directory in (native, repeat) for p in directory.glob('*.f32')}) + for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed during qualification: {path}') + for path, meta in fixtures.items(): check(digest(path) == meta['sha256'], f'reference changed during qualification: {path}') + for path, meta in cpu_fixtures.items(): check(digest(path) == meta['sha256'], f'CPU reference changed during qualification: {path}') + exit_code = 3 if 3 in statuses or not summary['corn_repeat_byte_identical'] else 0 + summary['status'] = 'PASS' if exit_code == 0 else 'ISSUES' +except InterruptedError as error: + exit_code = 128 + int(error.args[0]); summary['error'] = f'interrupted by signal {error.args[0]}' + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +except TimeoutError as error: + exit_code = 124; summary['error'] = str(error) + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +except Exception as error: + summary['error'] = f'{type(error).__name__}: {error}' + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +finally: + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + stop_owned() + # Once the fixed comparator returned 3, no later command may turn that into success or mask it. + if any(lane['name'] in ('comparison', 'repeat-comparison') and lane.get('exit') == 3 for lane in summary['lanes']): + exit_code = 3 + summary['exit'] = exit_code + dump('summary.json', summary) + dump('memory-after.json', memory()) + (evidence / 'harness.exit').write_text(str(exit_code) + '\n') + print(json.dumps(summary, indent=2), flush=True) +raise SystemExit(exit_code) +PY diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md b/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md new file mode 100644 index 000000000..da8ad7db2 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md @@ -0,0 +1,40 @@ +# Native HIP image qualification + +Prepared, not executed. `hip-lt-concurrent-qualification.py` supersedes the +idle-only draft for this prospective numerical window. It does not start a +server or authorize a paired GPU HTTP test. + +The candidate is `6137f4305400247fed98d2144634c184e2bc6b13`. Its clean HIP +probe is `635a49b45d116a62d405898230389f41e77aa51d47437c02ffa647919fd458d7`. +CPU execution already preserves all four prior full-image outputs exactly; +the older CPU-versus-source feature failures remain visible. + +Release requires the reviewed concurrent guard, concrete immutable workload +policies, and the accepted six-lane linear receipt for this same candidate. +The three full-image lanes are carrots, corn, and a second corn execution. +Each is a separate direct child of the live operator guard, sees one Radeon, +and must execute all 67 fused biased projections with the fixed 76 MiB +workspace. The guard checks the existing Strix operator and its resources +before, during, and after every lane. Unrelated opaque non-KFD processes are +a recorded visibility limitation; this is not exclusive device ownership or +a performance benchmark. + +The original target policy is unchanged, SHA256 +`62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f`: + +| Output | Maximum error | RMSE | Minimum cosine | +| --- | ---: | ---: | ---: | +| Features | 0.25 | 0.03 | 0.9995 | +| Embeddings | 0.75 | 0.08 | 0.9990 | + +All stages and both images must pass against the frozen first original-source +Radeon outputs. Both corn outputs must repeat byte-for-byte. Separate CPU +portability reports cannot replace the target comparison or hide its failure. +The comparator, references, complete Python/numpy runtime inventory, candidate +binary and actual linked HIP libraries are pinned. The umbrella `libggml.so` +is a pinned build artifact but is not linked into this standalone probe. + +After comparisons, input/source checks repeat and a final locked read-only +operator preflight must pass before the report can indicate success. A +successful numerical report would permit the planned production integration; +it would not establish that HTTP image input or image-based answers work. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py new file mode 100644 index 000000000..2014028a5 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py @@ -0,0 +1,230 @@ +"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. + +This supervises three sequential Radeon-only lanes through the separate live +operator guard. It makes no isolated performance or HTTP acceptance claim. +""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import signal +import stat +import subprocess +import sys + +HOME = Path('/home/marcelorm') +ROOT = HOME / 'lucebox-ds4v-vision-hipblaslt' +BUILD = Path('/tmp/ds4v-lt-hip-build') +SOURCE = '6137f4305400247fed98d2144634c184e2bc6b13' +REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' +CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' +PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' +FIXED = { + BUILD / 'ds4v_vision_probe': '635a49b45d116a62d405898230389f41e77aa51d47437c02ffa647919fd458d7', + COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', + CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', +} +LIBRARIES = { + 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', '1bdc3462382f5a208272badc459aee4f1c8c46536241f6e40fdbaee6c7ebeef1'), + 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '09c3721cb3e4c3689752fed9163c62dc4e20e5e3136a806a72ef7b544b7f89b3'), + 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '6c7f4bf85b8fc98c6b1109de5d2de4b5e9beb09680d47f7fd01414e70c556955'), + 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', 'd71378079c9ea008269964b34e9aa48a06db703e6c69db92b1aa7c803e5eee72'), + 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), + 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), +} + +def require(ok, why): + if not ok: + raise RuntimeError(why) + +def digest(path): + with Path(path).open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + +def verify_pins(pins): + for path, sha in pins.items(): + require(digest(path) == sha, 'pin changed: ' + str(path)) + +def guarded_run(command): + child = subprocess.Popen(command) + previous = {} + def interrupted(signum, frame): + raise InterruptedError(f'qualification interrupted: {signum}') + try: + for signum in (signal.SIGTERM, signal.SIGINT): + previous[signum] = signal.signal(signum, interrupted) + require(child.wait() == 0, 'lane supervision failed') + finally: + # The guard handles SIGTERM by stopping/reaping its direct GPU child. + # Never kill the guard while it might still own a live GPU process. + if child.poll() is None: + child.terminate() + child.wait(timeout=30) + for signum, handler in previous.items(): + signal.signal(signum, handler) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=Path, required=True) + parser.add_argument('--config-sha', required=True) + parser.add_argument('--parent-radeon-window-released', action='store_true') + args = parser.parse_args() + require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') + require(digest(args.config) == args.config_sha, 'config changed') + cfg = json.loads(args.config.read_text()) + linear_path = Path(cfg['linear_receipt']['path']) + require(digest(linear_path) == cfg['linear_receipt']['sha256'], 'linear acceptance receipt changed') + linear = json.loads(linear_path.read_text()) + require(linear.get('schema') == 'ds4v-lt-concurrent-linear-proof-v1' and linear.get('pass') is True + and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') + require(linear['source_commit'] == SOURCE and linear['red_commit'] == '983be861d878681a26f9f8c9e8cd4f804ab0f14d', 'linear source mismatch') + require(linear['pins_sha256'] == '0b2fb11d1b8d2b39716d9e19a3c61f4340ec21c7e15b3242e11f7d4ccfe660e5', 'linear pins mismatch') + require([x['name'] for x in linear['lanes']] == ['redtiny', 'redpatch', 'redqkv', 'greentiny', 'greenpatch', 'greenqkv'], 'six linear lanes required') + for lane in linear['lanes']: + red = lane['name'].startswith('red') + require(lane['guard_exit'] == 0 and lane['child_exit'] == (3 if red else 0) + and lane['component_numeric_pass'] is True, 'linear lane not accepted') + require((lane['source_bitwise_mismatches'] > 0) if red else (lane['source_bitwise_mismatches'] == 0), 'wrong linear numerical outcome') + require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') + pins = dict(FIXED) + pins[args.config] = args.config_sha + pins[linear_path] = cfg['linear_receipt']['sha256'] + pins.update({path: sha for path, sha in LIBRARIES.values()}) + runtime = Path(cfg['comparator_runtime']) + policy = Path(cfg['qualification_policy']) + pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', + policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) + verify_pins(pins) + pins.update(json.loads(runtime.read_text())['files']) + for directory in (REFERENCE, CPU_REFERENCE): + manifest = json.loads((directory / 'manifest.json').read_text()) + require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') + for label, entry in manifest['images'].items(): + require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') + for stage in ('patches', 'features', 'embeddings'): + path = directory / entry[stage]['file'] + require(path.parent == directory, 'fixture path escapes reference') + pins[path] = entry[stage]['sha256'] + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) + require('not found' not in ldd, 'unresolved dependency') + resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) + for soname, (path, _) in LIBRARIES.items(): + # The standalone probe links the backend libraries directly; the umbrella + # libggml is built/pinned but omitted by the linker's --as-needed rule. + if soname == 'libggml.so.0' and soname not in resolved: + continue + require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) + verify_pins(pins) + # Guard code and lane policies are frozen before importing or invoking them. + guard_dir = Path(cfg['guard_dir']) + pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) + verify_pins(pins) + require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py'}, 'guard pin set incomplete') + sys.path.insert(0, str(guard_dir)) + from run import load_policy + from host import Host + from guard import prepare_preflight + require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') + policies = [] + for lane in cfg['lanes']: + p = load_policy(Path(lane['policy']), lane['sha256']) + pins[Path(lane['policy'])] = lane['sha256'] + label = lane['name'].split('-')[0] + h, w = (42, 61) if label == 'carrots' else (23, 34) + out = HOME / 'ds4v-work/radeon-numerical-guard' / p['run_name'] + expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), + str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] + require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') + require('hip_fused_bias_launches=67 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') + sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, + str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} + require(set(p['required_outputs']) == set(sizes), 'wrong output contract') + require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') + policies.append((p, out, label)) + evidence = Path(cfg['evidence']) + require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') + evidence.mkdir() + report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', + 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), + 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'linear_receipt': cfg['linear_receipt'], 'lanes': []} + try: + for lane, (p, out, label) in zip(cfg['lanes'], policies): + guarded_run([str(PYTHON), '-I', str(guard_dir / 'run.py'), '--policy', lane['policy'], + '--policy-sha', lane['sha256'], '--parent-radeon-window-released']) + result = json.loads((out / 'guard.json').read_text()) + require(result['pass'] and result['device_proof_verified'], 'guard/result failed') + require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] + and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', + 'guard report belongs to a different command/policy/outcome') + require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') + for file, meta in result['output_evidence'].items(): + require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') + require(digest(file) == meta['sha256'], 'guard output changed before copying') + pins[Path(file)] = meta['sha256'] + pins[out / 'guard.json'] = digest(out / 'guard.json') + log = (out / 'child.log').read_text() + require(re.findall(r'^hip_fused_bias_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('67', '79691776')], 'ambiguous/incomplete full-tower dispatch') + pins[out / 'child.log'] = digest(out / 'child.log') + report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], + 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], + 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], + 'command': p['command'], 'child_exit': result['exit'], 'outputs': result['output_evidence']}) + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for index, (_, out, label) in enumerate(policies): + for stage in ('features', 'embeddings'): + original = out / f'{label}-{stage}.f32' + copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied output differs from guarded output') + pins[copied] = pins[original] + for stage in ('features', 'embeddings'): + original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied repeat carrots differ') + pins[copied] = pins[original] + codes = {} + for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), + ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: + verify_pins(pins) + with (evidence / f'{name}.log').open('w') as log: + result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], + stdout=log, stderr=subprocess.STDOUT, timeout=120) + require(result.returncode in (0, 3), 'comparator execution failed: ' + name) + codes[name] = result.returncode + report['comparisons'] = codes + report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) + verify_pins(pins) + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + p = policies[-1][0] + host = Host(p) + fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) + try: + st = os.fstat(fd) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) + finally: + os.close(fd) + (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') + report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] + except Exception as error: + report['error'] = f'{type(error).__name__}: {error}' + finally: + (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') + print(json.dumps(report, indent=2)) + return 0 if report['pass'] else 3 + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh b/harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh new file mode 100644 index 000000000..daacaa895 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# Pinned clean HIP build; execute only after source-linear regression acceptance. +# Execute only after the three source-linear regressions PASS and an explicitly released idle GPU window. +set -euo pipefail +[[ $(hostname) = soulf && $# = 3 && $3 = --gpu-window-released ]] || { + echo 'Usage on soulf: hip-qualification.sh COMPLETED_TEXT_PROOF_OR_--component-only NEW_EVIDENCE_DIR --gpu-window-released' >&2; exit 2; +} +python="$HOME/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python" +# No inherited device masks, overrides, GGML/DS4/DFLASH controls, LD_PRELOAD, or Python settings. +exec env -i HOME="$HOME" PATH=/usr/bin:/bin LANG=C LC_ALL=C \ + OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MKL_NUM_THREADS=2 \ + "$python" -I - "$1" "$2" "$(realpath "$0")" <<'PY' +import hashlib, json, os, re, signal, subprocess, sys, time +from pathlib import Path + +home = Path.home() +root = home / 'lucebox-ds4v-vision-hipblaslt' +build = Path('/tmp/ds4v-lt-hip-build') +ggml_build = build +binary = build / 'ds4v_vision_probe' +cpu_reference = home / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +reference = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference') +freeze_manifest = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json') +policy_sha = '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f' +mmproj = home / 'ds4v-work/ds4v-mmproj.gguf' +compare = root / 'server/tools/ds4v_vision/compare.py' +component_only = sys.argv[1] == '--component-only' +text_proof = None if component_only else Path(sys.argv[1]) +evidence, harness = map(Path, sys.argv[2:]) +source_sha = '6137f4305400247fed98d2144634c184e2bc6b13' +pinned = { + binary: '635a49b45d116a62d405898230389f41e77aa51d47437c02ffa647919fd458d7', + compare: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + mmproj: '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + cpu_reference / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + reference / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', + freeze_manifest: '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', + ggml_build / 'ggml/src/libggml-base.so.0': '1bdc3462382f5a208272badc459aee4f1c8c46536241f6e40fdbaee6c7ebeef1', + ggml_build / 'ggml/src/libggml-cpu.so.0': '09c3721cb3e4c3689752fed9163c62dc4e20e5e3136a806a72ef7b544b7f89b3', + ggml_build / 'ggml/src/ggml-hip/libggml-hip.so.0': '6c7f4bf85b8fc98c6b1109de5d2de4b5e9beb09680d47f7fd01414e70c556955', + ggml_build / 'ggml/src/libggml.so.0': 'd71378079c9ea008269964b34e9aa48a06db703e6c69db92b1aa7c803e5eee72', + Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'): '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950', + Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'): 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac', +} + +def check(ok, message): + if not ok: raise RuntimeError(message) + +def digest(path): + with open(path, 'rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() + +def dump(name, value): + (evidence / name).write_text(json.dumps(value, indent=2) + '\n') + +def idle_window(): + bus_env = dict(os.environ, XDG_RUNTIME_DIR=f'/run/user/{os.getuid()}') + state = subprocess.check_output(['systemctl', '--user', 'show', 'deepseek-dflash.service', + '--property=ActiveState', '--property=MainPID'], env=bus_env, text=True) + properties = dict(line.split('=', 1) for line in state.splitlines()) + check(properties.get('ActiveState') in ('inactive', 'failed') and properties.get('MainPID') == '0', + 'operator service is not down') + listeners = subprocess.check_output(['ss', '-ltn'], text=True) + check(not any(len(row.split()) > 3 and row.split()[3].endswith((':8016', ':8217')) + for row in listeners.splitlines()), 'operator or private text port is occupied') + processes = list(Path('/sys/class/kfd/kfd/proc').iterdir()) + check(not processes, 'GPU compute processes already exist') + available = int(next(row.split()[1] for row in Path('/proc/meminfo').read_text().splitlines() + if row.startswith('MemAvailable:'))) * 1024 + check(available >= 8 * 1024**3, 'less than 8 GiB host memory available for component check') + cards = [p for p in Path('/sys/class/drm').glob('card[0-9]*/device') + if (p / 'device').is_file() and (p / 'device').read_text().strip() == '0x744c'] + check(len(cards) == 1, 'expected exactly one RX 7900 XT device') + free_vram = int((cards[0] / 'mem_info_vram_total').read_text()) - int((cards[0] / 'mem_info_vram_used').read_text()) + check(free_vram >= 8 * 1024**3, 'less than 8 GiB discrete VRAM available') + return {'operator': properties, 'kfd_processes': [], 'host_available_bytes': available, + 'discrete_free_vram_bytes': free_vram} + +# No GPU call or evidence mutation before the explicit mode and idle-window gates. +check(source_sha != 'SOURCE_PIN_PENDING' and all(re.fullmatch(r'[0-9a-f]{64}', sha) for sha in pinned.values()), + 'candidate source/binary/library pins are unfinished') +check(not component_only, 'this candidate requires the completed text proof') +if not component_only: + text_proof = text_proof.resolve(strict=True) + allowed = [home / f'{tree}/artifacts/fitter-fix' for tree in + ('lucebox-ds4v-mix-fix', 'lucebox-ds4v-mix-parallel')] + check(text_proof.parent in allowed and text_proof.name.startswith('load-proof-'), 'unexpected text-proof directory') + check((text_proof / 'harness.exit').read_text().strip() == '0', 'private text proof did not complete successfully') + check(bool((text_proof / 'cleanup.txt').read_text().strip()), 'private text proof cleanup missing') + verdict = json.loads((text_proof / 'verdict.json').read_text()) + check(digest(text_proof / 'verdict.json') == 'f4f53d102e3386c45ac619d6c66e1b1dd50b6fd227b092474e310926919a1d5f', 'accepted text verdict changed') + check((text_proof / 'source.sha').read_text().strip() == '707194695703c023a8bf026684102d7c597d15b6', 'accepted text source changed') + check(all(verdict.get(k) == 'PASS' for k in ('text_load_smoke', 'math_answer', 'longer_decode_speculation')), + 'private text verdict is not PASS') + server_pid = int((text_proof / 'server.pid').read_text()) + check(server_pid > 1 and not Path(f'/proc/{server_pid}').exists(), 'private text server PID remains present') +initial_window = idle_window() +check(evidence.is_absolute() and not evidence.exists(), 'provide a fresh absolute evidence directory') +evidence.mkdir() # Parent must already exist; never remove or reuse evidence. +print(f'Evidence: {evidence}', flush=True) +active = None +summary = {'status': 'ISSUES', 'features': 'ISSUES', 'embeddings': 'NOT_QUALIFIED', + 'source_sha': source_sha, 'device': 'hip:0', 'text_proof': str(text_proof) if text_proof else None, + 'scope': '7900XT target-source fidelity only; CPU portability separate; no chat/server acceptance', 'policy_sha256': policy_sha, 'reference': str(reference), 'component_only': component_only, + 'initial_window': initial_window, 'lanes': []} +exit_code = 1 + +def interrupted(signum, frame): + raise InterruptedError(signum) + +signal.signal(signal.SIGINT, interrupted) +signal.signal(signal.SIGTERM, interrupted) + +def memory(): + result = {'monotonic_seconds': time.monotonic(), 'host': Path('/proc/meminfo').read_text()} + result['drm'] = {str(p): p.read_text().strip() for p in Path('/sys/class/drm').glob('card*/device/mem_info_*') + if p.name in ('mem_info_vram_total', 'mem_info_vram_used', 'mem_info_gtt_used')} + return result + +def stop_owned(): + global active + if active is not None: + # This unreaped direct child cannot have its PID reused. Never pkill or signal other processes. + active.terminate() + try: active.wait(timeout=5) + except subprocess.TimeoutExpired: active.kill(); active.wait() + lane = summary['lanes'][-1] + lane.update(exit=active.returncode, stopped_by_harness=True) + (evidence / f"{lane['name']}.exit").write_text(str(active.returncode) + '\n') + dump(f"{lane['name']}.time.json", lane) + active = None + +def run(name, command, timeout=900): + global active + lane = {'name': name, 'command': list(map(str, command))} + summary['lanes'].append(lane) + started = time.monotonic() + with open(evidence / f'{name}.log', 'w') as log, open(evidence / f'{name}.memory.jsonl', 'w') as samples: + active = subprocess.Popen(lane['command'], stdout=log, stderr=subprocess.STDOUT) + lane['pid'] = active.pid + (evidence / f'{name}.pid').write_text(str(active.pid) + '\n') + dump('summary.json', summary) + while True: + pid, status, usage = os.wait4(active.pid, os.WNOHANG) + if pid: + code = os.waitstatus_to_exitcode(status) + active.returncode = code + active = None + lane.update(exit=code, elapsed_seconds=time.monotonic()-started, + user_seconds=usage.ru_utime, system_seconds=usage.ru_stime, max_rss_kib=usage.ru_maxrss) + (evidence / f'{name}.exit').write_text(str(code) + '\n') + dump(f'{name}.time.json', lane) + dump('summary.json', summary) + return code + sample = memory() + try: sample['process_status'] = Path(f'/proc/{active.pid}/status').read_text() + except FileNotFoundError: pass + samples.write(json.dumps(sample) + '\n'); samples.flush() + if time.monotonic()-started > timeout: raise TimeoutError(f'{name} exceeded {timeout}s') + time.sleep(0.5) + +def verify_device(name): + log = (evidence / f'{name}.log').read_text() + check(re.search(r'^backend=ROCm0 requested=hip:0$', log, re.M), f'{name}: not the requested HIP backend') + check(re.search(r'Device 0: [^\n]*7900 XT[^\n]*gfx1100', log), f'{name}: device0 is not 7900 XT/gfx1100') + check(re.search(r'Device 1: [^\n]*gfx1151', log), f'{name}: device1 order changed') + +def verify_lt_dispatch(name): + log = (evidence / f'{name}.log').read_text() + check(re.search(r'^hip_fused_bias_launches=67 retained_workspace_bytes=79691776$', log, re.M), + f'{name}: expected actual source-style HIP operations and retained workspace are missing') + +try: + check(subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() == source_sha, + 'runtime source commit changed') + subprocess.run(['git', '-C', str(root), 'diff', '--quiet', 'HEAD', '--'], check=True) + for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed: {path}') + manifest = json.loads((reference / 'manifest.json').read_text()) + check(set(manifest['images']) == {'carrots', 'corn'}, 'unexpected fixture set') + fixtures = {} + for label, grid in [('carrots', [42, 61]), ('corn', [23, 34])]: + entry = manifest['images'][label] + check(entry['vit_grid'] == grid, f'{label}: grid changed') + for stage in ('patches', 'features', 'embeddings'): + meta = entry[stage]; path = reference / meta['file'] + check(path.parent == reference and digest(path) == meta['sha256'], f'{label}/{stage}: fixture changed') + fixtures[str(path)] = meta + cpu_fixtures = {} + cpu_manifest = json.loads((cpu_reference / 'manifest.json').read_text()) + check(set(cpu_manifest['images']) == {'carrots', 'corn'}, 'unexpected CPU fixture set') + for label, entry in cpu_manifest['images'].items(): + for stage in ('patches', 'features', 'embeddings'): + meta = entry[stage]; path = cpu_reference / meta['file'] + check(path.parent == cpu_reference and digest(path) == meta['sha256'], f'CPU fixture changed: {label}/{stage}') + cpu_fixtures[str(path)] = meta + ldd = subprocess.check_output(['/usr/bin/ldd', str(binary)], text=True) + check('not found' not in ldd and 'libggml-hip.so' in ldd, 'HIP dependency resolution failed') + (evidence / 'ldd.txt').write_text(ldd) + libraries = sorted(set(re.findall(r'(?:=>\s+|^\s*)(/[^\s]+)\s+\(', ldd, re.M))) + software = {path: digest(path) for path in libraries} + software[str(Path(sys.executable).resolve())] = digest(Path(sys.executable).resolve()) + dump('provenance.json', {'source_sha': source_sha, 'pinned': {str(p): s for p, s in pinned.items()}, + 'shared_libraries_and_python': software, 'fixtures': fixtures, 'cpu_fixtures': cpu_fixtures, + 'environment': dict(os.environ), 'python': sys.version, 'harness_sha256': digest(harness), + 'text_proof_files': {p.name: digest(p) for p in text_proof.iterdir() if p.is_file()} if text_proof else {}}) + dump('memory-before.json', memory()) + dump('window-before-gpu.json', idle_window()) + check(run('device-check', [binary, mmproj, '--load-only', '4096', '129280', 'hip:0']) == 0, + 'HIP load-only/device check failed') + verify_device('device-check') + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for label, h, w, output, lane in [('carrots', 42, 61, native, 'carrots'), + ('corn', 23, 34, native, 'corn'), ('corn', 23, 34, repeat, 'corn-repeat')]: + dump(f'{lane}.window-before.json', idle_window()) + check(run(lane, [binary, mmproj, reference / f'{label}-patches.f32', str(h), str(w), output, label, '0', 'hip:0']) == 0, + f'{lane}: HIP encode failed') + verify_device(lane) + verify_lt_dispatch(lane) + dump(f'{lane}.window-after.json', idle_window()) + # compare.py requires both fixture names: explicitly reuse the first carrots result in repeat comparison. + for stage in ('features', 'embeddings'): + (repeat / f'carrots-{stage}.f32').symlink_to(native / f'carrots-{stage}.f32') + cpu_statuses = [run(name, [sys.executable, '-I', compare, cpu_reference, output, '--output', evidence / f'{name}.json']) + for name, output in [('native-vs-cpu', native), ('source-hip-vs-cpu', reference)]] + check(all(code in (0, 3) for code in cpu_statuses), 'CPU portability comparison execution/shape/hash failure') + summary['cpu_portability'] = dict(zip(('native_vs_cpu', 'source_hip_vs_cpu'), + ('PASS' if code == 0 else 'ISSUES' for code in cpu_statuses))) + statuses = [run(name, [sys.executable, '-I', compare, reference, output, '--output', evidence / f'{name}.json']) + for name, output in [('comparison', native), ('repeat-comparison', repeat)]] + check(all(code in (0, 3) for code in statuses), 'comparison execution/shape/hash failure') + comparisons = [json.loads((evidence / f'{name}.json').read_text()) for name in ('comparison', 'repeat-comparison')] + summary['features'] = 'PASS' if all(c[label]['features']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' + summary['embeddings'] = 'PASS' if all(c[label]['embeddings']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' + summary['corn_repeat_byte_identical'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') + for s in ('features', 'embeddings')) + dump('outputs.sha256.json', {str(p.relative_to(evidence)): digest(p) for directory in (native, repeat) for p in directory.glob('*.f32')}) + for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed during qualification: {path}') + for path, meta in fixtures.items(): check(digest(path) == meta['sha256'], f'reference changed during qualification: {path}') + for path, meta in cpu_fixtures.items(): check(digest(path) == meta['sha256'], f'CPU reference changed during qualification: {path}') + exit_code = 3 if 3 in statuses or not summary['corn_repeat_byte_identical'] else 0 + summary['status'] = 'PASS' if exit_code == 0 else 'ISSUES' +except InterruptedError as error: + exit_code = 128 + int(error.args[0]); summary['error'] = f'interrupted by signal {error.args[0]}' + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +except TimeoutError as error: + exit_code = 124; summary['error'] = str(error) + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +except Exception as error: + summary['error'] = f'{type(error).__name__}: {error}' + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +finally: + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + stop_owned() + # Once the fixed comparator returned 3, no later command may turn that into success or mask it. + if any(lane['name'] in ('comparison', 'repeat-comparison') and lane.get('exit') == 3 for lane in summary['lanes']): + exit_code = 3 + summary['exit'] = exit_code + dump('summary.json', summary) + dump('memory-after.json', memory()) + (evidence / 'harness.exit').write_text(str(exit_code) + '\n') + print(json.dumps(summary, indent=2), flush=True) +raise SystemExit(exit_code) +PY diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py new file mode 100644 index 000000000..fb328182f --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py @@ -0,0 +1,243 @@ +"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. + +This supervises three sequential Radeon-only lanes through the separate live +operator guard. It makes no isolated performance or HTTP acceptance claim. +""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import signal +import stat +import subprocess +import sys + +HOME = Path('/home/marcelorm') +ROOT = HOME / 'lucebox-ds4v-vision-hipblaslt' +BUILD = Path('/tmp/ds4v-lt-hip-build') +SOURCE = '3191e7eaee3f5b4d0caa8e8b228c09a9d52b5f3f' +REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' +CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' +PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' +FIXED = { + BUILD / 'ds4v_vision_probe': '71515dbb84a48a764ed109ae55bfe94ebc40f1327bac0ddd9c49ab2268e32be3', + COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', + CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', +} +LIBRARIES = { + 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', '1bdc3462382f5a208272badc459aee4f1c8c46536241f6e40fdbaee6c7ebeef1'), + 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '09c3721cb3e4c3689752fed9163c62dc4e20e5e3136a806a72ef7b544b7f89b3'), + 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '6c7f4bf85b8fc98c6b1109de5d2de4b5e9beb09680d47f7fd01414e70c556955'), + 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', '0c7a845a117b3f57b27b10b7a5a8e8821631e1e7be17374d1e758c6ab407f719'), + 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), + 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), +} + +def require(ok, why): + if not ok: + raise RuntimeError(why) + +def digest(path): + with Path(path).open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + +def verify_pins(pins): + for path, sha in pins.items(): + require(digest(path) == sha, 'pin changed: ' + str(path)) + +def guarded_run(command): + child = subprocess.Popen(command) + previous = {} + def interrupted(signum, frame): + raise InterruptedError(f'qualification interrupted: {signum}') + try: + for signum in (signal.SIGTERM, signal.SIGINT): + previous[signum] = signal.signal(signum, interrupted) + require(child.wait() == 0, 'lane supervision failed') + finally: + # The guard handles SIGTERM by stopping/reaping its direct GPU child. + # Never kill the guard while it might still own a live GPU process. + if child.poll() is None: + child.terminate() + child.wait(timeout=30) + for signum, handler in previous.items(): + signal.signal(signum, handler) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=Path, required=True) + parser.add_argument('--config-sha', required=True) + parser.add_argument('--parent-radeon-window-released', action='store_true') + args = parser.parse_args() + require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') + require(digest(args.config) == args.config_sha, 'config changed') + cfg = json.loads(args.config.read_text()) + require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') + linear_path = Path(cfg['linear_receipt']['path']) + require(digest(linear_path) == cfg['linear_receipt']['sha256'], 'linear acceptance receipt changed') + linear = json.loads(linear_path.read_text()) + require(linear.get('schema') == 'ds4v-lt-concurrent-linear-proof-v1' and linear.get('pass') is True + and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') + require(linear['source_commit'] == SOURCE and linear['red_commit'] == 'cce69498d6b01541d06fcf77364f68fdbee4627d', 'linear source mismatch') + require(linear['pins_sha256'] == '832017215ddea57d145e95e31564c1ef7444020a75ba4c9092cefced7b23241b', 'linear pins mismatch') + require([x['name'] for x in linear['lanes']] == ['redtiny', 'redpatch', 'redqkv', 'greentiny', 'greenpatch', 'greenqkv'], 'six linear lanes required') + for lane in linear['lanes']: + red = lane['name'].startswith('red') + require(lane['guard_exit'] == 0 and lane['child_exit'] == (3 if red else 0) + and lane['component_numeric_pass'] is True, 'linear lane not accepted') + require((lane['source_bitwise_mismatches'] > 0) if red else (lane['source_bitwise_mismatches'] == 0), 'wrong linear numerical outcome') + require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') + pins = dict(FIXED) + pins[args.config] = args.config_sha + pins[linear_path] = cfg['linear_receipt']['sha256'] + pins.update({path: sha for path, sha in LIBRARIES.values()}) + runtime = Path(cfg['comparator_runtime']) + policy = Path(cfg['qualification_policy']) + pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', + policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) + verify_pins(pins) + pins.update(json.loads(runtime.read_text())['files']) + for directory in (REFERENCE, CPU_REFERENCE): + manifest = json.loads((directory / 'manifest.json').read_text()) + require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') + for label, entry in manifest['images'].items(): + require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') + for stage in ('patches', 'features', 'embeddings'): + path = directory / entry[stage]['file'] + require(path.parent == directory, 'fixture path escapes reference') + pins[path] = entry[stage]['sha256'] + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) + require('not found' not in ldd, 'unresolved dependency') + resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) + for soname, (path, _) in LIBRARIES.items(): + # The standalone probe links the backend libraries directly; the umbrella + # libggml is built/pinned but omitted by the linker's --as-needed rule. + if soname == 'libggml.so.0' and soname not in resolved: + continue + require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) + verify_pins(pins) + # Guard code and lane policies are frozen before importing or invoking them. + guard_dir = Path(cfg['guard_dir']) + require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') + pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) + verify_pins(pins) + require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') + sys.path.insert(0, str(guard_dir)) + from run import load_policy, launch_command + from host import Host + from guard import prepare_preflight + require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') + policies = [] + for lane in cfg['lanes']: + p = load_policy(Path(lane['policy']), lane['sha256']) + pins[Path(lane['policy'])] = lane['sha256'] + isolation_pins = p.get('isolation_pins', {}) + require('/usr/bin/python3.12' in isolation_pins and + all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), + 'namespace runtime is not included in guarded component pins') + for path, sha in p['component_pins'].items(): + require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) + pins[Path(path)] = sha + label = lane['name'].split('-')[0] + h, w = (42, 61) if label == 'carrots' else (23, 34) + out = guard_dir / p['run_name'] + expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), + str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] + require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') + require('hip_fused_bias_launches=67 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') + sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, + str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} + require(set(p['required_outputs']) == set(sizes), 'wrong output contract') + require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') + policies.append((p, out, label)) + evidence = Path(cfg['evidence']) + require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') + evidence.mkdir() + report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', + 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), + 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'linear_receipt': cfg['linear_receipt'], 'lanes': []} + try: + for lane, (p, out, label) in zip(cfg['lanes'], policies): + guarded_run([str(PYTHON), '-I', str(guard_dir / 'run.py'), '--policy', lane['policy'], + '--policy-sha', lane['sha256'], '--parent-radeon-window-released']) + result = json.loads((out / 'guard.json').read_text()) + require(result['pass'] and result['device_proof_verified'], 'guard/result failed') + require(result.get('namespace_verified') is True, 'private NPU namespace not verified') + require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') + require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] + and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', + 'guard report belongs to a different command/policy/outcome') + require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') + for file, meta in result['output_evidence'].items(): + require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') + require(digest(file) == meta['sha256'], 'guard output changed before copying') + pins[Path(file)] = meta['sha256'] + pins[out / 'guard.json'] = digest(out / 'guard.json') + log = (out / 'child.log').read_text() + require(re.findall(r'^hip_fused_bias_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('67', '79691776')], 'ambiguous/incomplete full-tower dispatch') + pins[out / 'child.log'] = digest(out / 'child.log') + report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], + 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], + 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], + 'command': p['command'], 'launch_command': result['launch_command'], + 'namespace_verified': result['namespace_verified'], + 'child_exit': result['exit'], 'outputs': result['output_evidence']}) + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for index, (_, out, label) in enumerate(policies): + for stage in ('features', 'embeddings'): + original = out / f'{label}-{stage}.f32' + copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied output differs from guarded output') + pins[copied] = pins[original] + for stage in ('features', 'embeddings'): + original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied repeat carrots differ') + pins[copied] = pins[original] + codes = {} + for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), + ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: + verify_pins(pins) + with (evidence / f'{name}.log').open('w') as log: + result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], + stdout=log, stderr=subprocess.STDOUT, timeout=120) + require(result.returncode in (0, 3), 'comparator execution failed: ' + name) + codes[name] = result.returncode + report['comparisons'] = codes + report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) + verify_pins(pins) + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + p = policies[-1][0] + host = Host(p) + fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) + try: + st = os.fstat(fd) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) + finally: + os.close(fd) + (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') + report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] + except Exception as error: + report['error'] = f'{type(error).__name__}: {error}' + finally: + (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') + print(json.dumps(report, indent=2)) + return 0 if report['pass'] else 3 + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py new file mode 100644 index 000000000..c72835924 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py @@ -0,0 +1,297 @@ +"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. + +This supervises three sequential Radeon-only lanes through the separate live +operator guard. It makes no isolated performance or HTTP acceptance claim. +""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import signal +import stat +import subprocess +import sys + +HOME = Path('/home/marcelorm') +ROOT = HOME / 'lucebox-ds4v-vision-norm' +BUILD = Path('/tmp/ds4v-norm-hip-build') +SOURCE = 'ed661d01a5dfebb02009f23816aec18f8cc87178' +REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' +CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' +PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' +SUPERVISOR = HOME / 'ds4v-work/hipblaslt-stage-diagnostic/log-snapshot-guard.py' +SUPERVISOR_SHA = '8591d28b0b11a1531fe2a657080958cbf401fda9a19dd18d747c09e5edbd06cb' +LANES = ['tiny', 'patch', 'qkv', 'mlp_w1', 'mlp_w2', 'norm782', 'norm2562'] +FIXED = { + SUPERVISOR: SUPERVISOR_SHA, + BUILD / 'ds4v_vision_probe': '75215a951ca640e250c040e31010bb202723da89774f992566525e8c56f9c332', + COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', + CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', +} +LIBRARIES = { + 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', 'bdaf7f896e931898241e2f77775511d1a959af9293e0eb8c2c3d2c323095a253'), + 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '1622da3087c042dcd5bcf27487890a8301a9baaa4f57fe11c98c40b1898e6e10'), + 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', 'bb296f9dd83d5ea5a9dbf844e02e8ac426358d7035fb9db1642d6a8f63eae462'), + 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', 'e0e2a256cdeac6139f779a417ae11fed414e92b43781c16a0835c003054d680c'), + 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), + 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), +} + +def require(ok, why): + if not ok: + raise RuntimeError(why) + +def digest(path): + with Path(path).open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + +def verify_pins(pins): + for path, sha in pins.items(): + require(re.fullmatch('[0-9a-f]{64}', sha) is not None, 'unbound or invalid pin: ' + str(path)) + require(digest(path) == sha, 'pin changed: ' + str(path)) + +def guarded_run(command, log): + child = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT) + previous = {} + def interrupted(signum, frame): + raise InterruptedError(f'qualification interrupted: {signum}') + try: + for signum in (signal.SIGTERM, signal.SIGINT): + previous[signum] = signal.signal(signum, interrupted) + require(child.wait() == 0, 'lane supervision failed') + finally: + # The guard handles SIGTERM by stopping/reaping its direct GPU child. + # Never kill the guard while it might still own a live GPU process. + if child.poll() is None: + child.terminate() + child.wait(timeout=30) + for signum, handler in previous.items(): + signal.signal(signum, handler) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=Path, required=True) + parser.add_argument('--config-sha', required=True) + parser.add_argument('--parent-radeon-window-released', action='store_true') + args = parser.parse_args() + require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') + require(re.fullmatch('[0-9a-f]{40}', SOURCE) is not None, 'source commit is not bound') + require(digest(args.config) == args.config_sha, 'config changed') + cfg = json.loads(args.config.read_text()) + require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') + production_path = Path(cfg['production_pins']['path']) + production_sha = cfg['production_pins']['sha256'] + require(re.fullmatch('[0-9a-f]{64}', production_sha) is not None + and digest(production_path) == production_sha, 'production pin manifest changed') + production = json.loads(production_path.read_text()) + require(production.get('schema') == 'ds4v-norm-production-runtime-v1' + and production.get('source_root') == str(ROOT) + and production.get('source_commit') == SOURCE and isinstance(production.get('files'), dict) + and production['files'], 'production source/file pins missing') + linear_path = Path(cfg['production_receipt']['path']) + require(digest(linear_path) == cfg['production_receipt']['sha256'], 'linear acceptance receipt changed') + linear = json.loads(linear_path.read_text()) + require(linear.get('schema') == 'ds4v-norm-native-production-proof-v1' and linear.get('pass') is True + and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') + require(linear['source_commit'] == SOURCE, 'linear source mismatch') + require(linear['pins_sha256'] == production_sha, 'linear production pins mismatch') + require([x['name'] for x in linear['lanes']] == LANES, 'seven production prerequisite lanes required') + require(linear['supervisor'] == production['supervisor'] == {'path': str(SUPERVISOR), 'sha256': SUPERVISOR_SHA}, + 'logging supervisor differs from accepted prerequisites') + require(linear['source_norm'] == production['source_norm'], 'normalization source provenance mismatch') + require(linear['source_acceptance'] == production['source_acceptance'] == { + 'path': str(SUPERVISOR.parent / 'unbiased-source-acceptance-v1.json'), + 'sha256': '1d0d59f2b3ffc21a366df50ef7257fe6ac2a1eca78cbea10988df3f2398bdbfa'}, 'source acceptance changed') + for lane in linear['lanes']: + require(lane['guard_exit'] == 0 and lane['child_exit'] == 0 + and lane['component_numeric_pass'] is True, 'linear lane not accepted') + require(lane['source_bitwise_mismatches'] == 0, 'production linear differs from frozen source') + norm = lane['name'].startswith('norm') + expected_dispatch = 0 if norm else 1 if lane['name'].startswith('mlp_') else 2 + require(lane['explicit_ops'] == lane['actual_lt_launches'] == expected_dispatch, + 'wrong production linear dispatch count') + require(lane['explicit_norm_ops'] == lane['actual_norm_launches'] == (1 if norm else 0), + 'wrong production normalization dispatch count') + if norm: + require(lane['reference_layout'] == ('original' if lane['name'] == 'norm782' else 'rowwise-tiled-original'), + 'normalization reference layout changed') + require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') + pins = dict(FIXED) + pins[args.config] = args.config_sha + pins[linear_path] = cfg['production_receipt']['sha256'] + pins[production_path] = production_sha + pins.update({path: sha for path, sha in LIBRARIES.values()}) + for path, sha in production['files'].items(): + require(Path(path).is_absolute(), 'absolute production file pin required') + require(Path(path) not in pins or pins[Path(path)] == sha, 'production pin conflicts with qualification pin: ' + path) + pins[Path(path)] = sha + for path, sha in linear['input_artifact_hashes'].items(): + require(Path(path).is_absolute() and (Path(path) not in pins or pins[Path(path)] == sha), + 'prerequisite provenance conflicts with qualification pins') + pins[Path(path)] = sha + for lane in linear['lanes']: + pins[Path(lane['guard_report_path'])] = lane['guard_report_sha256'] + runtime = Path(cfg['comparator_runtime']) + policy = Path(cfg['qualification_policy']) + pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', + policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) + verify_pins(pins) + for path, sha in json.loads(runtime.read_text())['files'].items(): + require(Path(path) not in pins or pins[Path(path)] == sha, 'comparator runtime pin conflict') + pins[Path(path)] = sha + for directory in (REFERENCE, CPU_REFERENCE): + manifest = json.loads((directory / 'manifest.json').read_text()) + require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') + for label, entry in manifest['images'].items(): + require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') + for stage in ('patches', 'features', 'embeddings'): + path = directory / entry[stage]['file'] + require(path.parent == directory, 'fixture path escapes reference') + pins[path] = entry[stage]['sha256'] + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) + require('not found' not in ldd, 'unresolved dependency') + resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) + for soname, (path, _) in LIBRARIES.items(): + # The standalone probe links the backend libraries directly; the umbrella + # libggml is built/pinned but omitted by the linker's --as-needed rule. + if soname == 'libggml.so.0' and soname not in resolved: + continue + require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) + verify_pins(pins) + # Guard code and lane policies are frozen before importing or invoking them. + guard_dir = Path(cfg['guard_dir']) + require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') + pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) + verify_pins(pins) + require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') + sys.path.insert(0, str(guard_dir)) + from run import load_policy, launch_command + from host import Host + from guard import prepare_preflight + require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') + policies = [] + for lane in cfg['lanes']: + p = load_policy(Path(lane['policy']), lane['sha256']) + pins[Path(lane['policy'])] = lane['sha256'] + isolation_pins = p.get('isolation_pins', {}) + require('/usr/bin/python3.12' in isolation_pins and + all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), + 'namespace runtime is not included in guarded component pins') + for path, sha in p['component_pins'].items(): + require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) + pins[Path(path)] = sha + label = lane['name'].split('-')[0] + h, w = (42, 61) if label == 'carrots' else (23, 34) + out = guard_dir / p['run_name'] + expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), + str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] + require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') + require('hip_vision_linear_launches=131 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') + require('hip_vision_norm_launches=65' in p['required_log_lines'], 'normalization dispatch contract missing') + require(p['component_pins'].get(str(SUPERVISOR)) == SUPERVISOR_SHA, 'logging supervisor is not pinned') + sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, + str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} + require(set(p['required_outputs']) == set(sizes), 'wrong output contract') + require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') + policies.append((p, out, label)) + require(len({str(out) for _, out, _ in policies}) == 3, 'full lane evidence directories must be distinct') + evidence = Path(cfg['evidence']) + require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') + evidence.mkdir() + report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', + 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), + 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'production_receipt': cfg['production_receipt'], + 'production_pins': cfg['production_pins'], 'supervisor': production['supervisor'], 'lanes': []} + try: + for lane, (p, out, label) in zip(cfg['lanes'], policies): + verify_pins(pins) + supervisor_log = evidence / (lane['name'] + '-supervisor.log') + with supervisor_log.open('x') as log: + guarded_run([str(PYTHON), '-I', '-B', str(SUPERVISOR), '--policy', lane['policy'], + '--policy-sha', lane['sha256'], '--parent-radeon-window-released'], log) + pins[supervisor_log] = digest(supervisor_log) + result = json.loads((out / 'guard.json').read_text()) + require(result['pass'] and result['device_proof_verified'], 'guard/result failed') + require(result.get('namespace_verified') is True, 'private NPU namespace not verified') + require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') + require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] + and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', + 'guard report belongs to a different command/policy/outcome') + require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') + for file, meta in result['output_evidence'].items(): + require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') + require(digest(file) == meta['sha256'], 'guard output changed before copying') + pins[Path(file)] = meta['sha256'] + pins[out / 'guard.json'] = digest(out / 'guard.json') + log = (out / 'child.log').read_text() + require(re.findall(r'^hip_vision_linear_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('131', '79691776')], 'ambiguous/incomplete full-tower dispatch') + require(re.findall(r'^hip_vision_norm_launches=(\d+)$', log, re.M) == ['65'], + 'ambiguous/incomplete full-tower normalization dispatch') + pins[out / 'child.log'] = digest(out / 'child.log') + report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], + 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], + 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], + 'command': p['command'], 'launch_command': result['launch_command'], + 'namespace_verified': result['namespace_verified'], + 'actual_lt_launches': 131, 'actual_norm_launches': 65, + 'supervisor_log_path': str(supervisor_log), 'supervisor_log_sha256': pins[supervisor_log], + 'child_exit': result['exit'], 'outputs': result['output_evidence']}) + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for index, (_, out, label) in enumerate(policies): + for stage in ('features', 'embeddings'): + original = out / f'{label}-{stage}.f32' + copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied output differs from guarded output') + pins[copied] = pins[original] + for stage in ('features', 'embeddings'): + original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied repeat carrots differ') + pins[copied] = pins[original] + codes = {} + for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), + ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: + verify_pins(pins) + with (evidence / f'{name}.log').open('w') as log: + result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], + stdout=log, stderr=subprocess.STDOUT, timeout=120) + require(result.returncode in (0, 3), 'comparator execution failed: ' + name) + codes[name] = result.returncode + report['comparisons'] = codes + report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) + verify_pins(pins) + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + p = policies[-1][0] + host = Host(p) + fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) + try: + st = os.fstat(fd) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) + finally: + os.close(fd) + (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') + report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] + except Exception as error: + report['error'] = f'{type(error).__name__}: {error}' + finally: + (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') + print(json.dumps(report, indent=2)) + return 0 if report['pass'] else 3 + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md b/harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md new file mode 100644 index 000000000..0e5da7acb --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md @@ -0,0 +1,38 @@ +# Prepared native HIP qualification + +**Executed: numerical ISSUES (exit3).** The first standalone component run is recorded in `native-hip-first/`. All probe processes completed; corn repeat was byte-identical, but feature and embedding comparisons failed unchanged thresholds. The harness never launches/stops converters, a text server, or the operator service. + +Current deployed harness SHA256 is `152af330bb37f77f8e6f9c795ae46c12f787e8587c42fc210aeb671b205296d5`. It accepts a completed text-proof directory or `--component-only` as argument1, a fresh absolute evidence directory as argument2, and `--gpu-window-released` as argument3. Component-only execution is independently authorized while CPU conversion runs; it makes no text/chat acceptance claim. Both modes require a released window and double checks for inactive operator/PID0, free8016/8217, empty KFD process inventory, and at least8GiB host and discrete VRAM available. Component mode passed independent review in `component-window-review.md`. + +The selected runtime is `4bf727077cf007352997798edc52f32c1f887023` in `soulf:~/lucebox-ds4v-runtime`. The existing `/tmp/ds4v-runtime-hip-build/ds4v_vision_probe` and its three GGML shared libraries are pinned by SHA256 in the harness. Probe SHA256 is `4dc9430cd56ca5afe6e649adfa22ad5065b2c97cede86d0ac0b75b2f9296377e`. The executed device check confirmed hip:0 as Radeon RX7900XT/gfx1100; device1 enumerates as gfx1151. + +For the completed-text-proof mode, run manually with **both exact directories**, never a `latest` pointer: + +```sh +bash /tmp/ds4v-hip-qualification.sh \ + /home/marcelorm/lucebox-ds4v-mix-parallel/artifacts/fitter-fix/load-proof-EXACT_COMPLETED_RUN \ + /home/marcelorm/lucebox-ds4v-runtime/artifacts/hip-qualification-UNIQUE_RUN \ + --gpu-window-released +``` + +These example directory suffixes are placeholders. The text evidence must be an existing `load-proof-*` directory under the serial or parallel converter's `artifacts/fitter-fix`. It must contain `harness.exit=0`, a cleanup record, the passing text/math/speculation verdict and a recorded server PID that is no longer present. The new absolute HIP evidence directory must not exist, and its parent must exist. Gate failures create no HIP evidence and make no GPU call. The explicit release flag records the caller's already-granted GPU window; it is not automatic authorization. + +The harness then: + +1. Uses the existing immutable CPU-reference venv interpreter with `-I` under `env -i`. Only HOME, fixed PATH/locale and two-thread CPU math limits remain. All inherited device masks/overrides, GGML/DS4/DFLASH controls, Python settings and dynamic-loader overrides are removed. +2. Verifies the exact source commit, clean tracked source, pinned binary/libraries/compare script, accepted mmproj SHA256 `58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c`, original reference manifest and every used patch/feature/embedding file. Records resolved transitive shared-library hashes, Python, environment, harness and text-proof file hashes in `provenance.json`. +3. Runs `--load-only 4096 129280 hip:0` and requires the actual log to identify `backend=ROCm0 requested=hip:0`, device0 as 7900 XT/gfx1100 and device1 as gfx1151. Only hip:0 receives a runtime/weight allocation. Device enumeration does not execute the graph on device1. An unavailable HIP backend fails; there is no CPU fallback command. +4. Runs carrots 42×61, corn 23×34, then corn again, each as a separate sequential hip:0 process. Stage dumping is disabled to keep scratch bounded. The existing probe reports weights/scratch/encode duration and exercises its release/error checks. This measures complete probe behavior including its host transfers; it is not a warmed persistent-runtime throughput benchmark. +5. Runs the **unchanged** selected `compare.py` twice. The second native directory contains the new corn result and explicit symlinks to the first carrots result because the comparator requires both names. It does not run carrots twice or replace any reference. Shape, finite, max-absolute, RMSE and cosine measurements remain in `comparison.json` and `repeat-comparison.json`. Corn repeat byte identity is reported separately. + +The fixed gates remain features max-absolute≤0.25, RMSE≤0.03, cosine≥0.9995; embeddings max-absolute≤0.75, RMSE≤0.08, cosine≥0.9990. Either fixed comparison's exit **3** is preserved as the final harness exit even if a later step also fails; it never becomes a success. Features and embeddings have separate summary statuses, so passing embeddings cannot erase feature ISSUES. A repeat-byte mismatch also yields overall ISSUES/exit3; it is separately identified rather than changing the numerical thresholds. Execution/shape/provenance failures remain unqualified, with their error and individual process exits recorded. Interrupted/timed-out runs exit 130/143 or 124 unless an already-recorded numerical exit3 takes precedence. + +Each process has an exact command/PID/log/exit and `*.time.json` containing `wait4` user/system time, wall time and peak RSS KiB. `*.memory.jsonl` samples process status, host memory and DRM VRAM/GTT usage every half-second. DRM observations are device-wide and may include unrelated allocations; they are not an isolated per-process GPU peak. `memory-before.json`, `memory-after.json`, `outputs.sha256.json`, `summary.json` and `harness.exit` complete the evidence. Each process has a 900-second ceiling. + +On interruption or timeout, cleanup terminates only the harness's unreaped direct child whose PID it recorded; after five seconds it may kill that same child. It uses no name matching, port cleanup, GPU reset or other-process signal. Inputs, references, venv, source and libraries are read-only. Output files remain in the fresh evidence directory even on failure. + +Preparation checks: soulf `bash -n` and embedded Python `ast.parse` both passed without executing qualification code or the probe. Parent subsequently reviewed the actual probe/comparator contract, text-verdict schema, pinned inputs, clean environment, PID ownership and preserved numerical failure status, then copied the script to soulf and verified its hash and shell syntax. No qualification PASS, HIP feature parity, maximum-grid behavior or decoder/HTTP acceptance is claimed by preparation. + +## First component result + +`native-hip-first/summary.json` records all commands, PIDs and exits. All native probes exit0; both comparisons exit3. Corn feature cosine0.98593858015, carrots0.99388401645; embedding cosine0.99263256728 and0.99645496479 respectively. All four fail their unchanged cosine gate. Corn repeat is byte-identical. No HTTP/decoder image support is qualified by this result. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh b/harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh new file mode 100644 index 000000000..5dbf90e65 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh @@ -0,0 +1,226 @@ +#!/usr/bin/env bash +# Standalone component checks may precede text proof in an idle, explicitly released GPU window. +set -euo pipefail +[[ $(hostname) = soulf && $# = 3 && $3 = --gpu-window-released ]] || { + echo 'Usage on soulf: hip-qualification.sh COMPLETED_TEXT_PROOF_OR_--component-only NEW_EVIDENCE_DIR --gpu-window-released' >&2; exit 2; +} +python="$HOME/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python" +# No inherited device masks, overrides, GGML/DS4/DFLASH controls, LD_PRELOAD, or Python settings. +exec env -i HOME="$HOME" PATH=/usr/bin:/bin LANG=C LC_ALL=C \ + OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MKL_NUM_THREADS=2 \ + "$python" -I - "$1" "$2" "$(realpath "$0")" <<'PY' +import hashlib, json, os, re, signal, subprocess, sys, time +from pathlib import Path + +home = Path.home() +root = home / 'lucebox-ds4v-runtime' +build = Path('/tmp/ds4v-runtime-hip-build') +binary = build / 'ds4v_vision_probe' +reference = home / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +mmproj = home / 'ds4v-work/ds4v-mmproj.gguf' +compare = root / 'server/tools/ds4v_vision/compare.py' +component_only = sys.argv[1] == '--component-only' +text_proof = None if component_only else Path(sys.argv[1]) +evidence, harness = map(Path, sys.argv[2:]) +source_sha = '4bf727077cf007352997798edc52f32c1f887023' +pinned = { + binary: '4dc9430cd56ca5afe6e649adfa22ad5065b2c97cede86d0ac0b75b2f9296377e', + compare: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + mmproj: '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + reference / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + build / 'ggml/src/libggml-base.so.0': '378b6c81052532d19bd86b32de236553cc1e57f824ce35fb733569470786ed29', + build / 'ggml/src/libggml-cpu.so.0': 'b33faf3a600eeff2bea8b692360cff6de397aaf3082ea0c73a9f3a1af9ee70d2', + build / 'ggml/src/ggml-hip/libggml-hip.so.0': 'b8991450ee422983b91cfbfcdf8d6b612e92f62f1128c6cce0c6b3e37ff8ef7e', +} + +def check(ok, message): + if not ok: raise RuntimeError(message) + +def digest(path): + with open(path, 'rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() + +def dump(name, value): + (evidence / name).write_text(json.dumps(value, indent=2) + '\n') + +def idle_window(): + bus_env = dict(os.environ, XDG_RUNTIME_DIR=f'/run/user/{os.getuid()}') + state = subprocess.check_output(['systemctl', '--user', 'show', 'deepseek-dflash.service', + '--property=ActiveState', '--property=MainPID'], env=bus_env, text=True) + properties = dict(line.split('=', 1) for line in state.splitlines()) + check(properties.get('ActiveState') in ('inactive', 'failed') and properties.get('MainPID') == '0', + 'operator service is not down') + listeners = subprocess.check_output(['ss', '-ltn'], text=True) + check(not any(len(row.split()) > 3 and row.split()[3].endswith((':8016', ':8217')) + for row in listeners.splitlines()), 'operator or private text port is occupied') + processes = list(Path('/sys/class/kfd/kfd/proc').iterdir()) + check(not processes, 'GPU compute processes already exist') + available = int(next(row.split()[1] for row in Path('/proc/meminfo').read_text().splitlines() + if row.startswith('MemAvailable:'))) * 1024 + check(available >= 8 * 1024**3, 'less than 8 GiB host memory available for component check') + cards = [p for p in Path('/sys/class/drm').glob('card[0-9]*/device') + if (p / 'device').is_file() and (p / 'device').read_text().strip() == '0x744c'] + check(len(cards) == 1, 'expected exactly one RX 7900 XT device') + free_vram = int((cards[0] / 'mem_info_vram_total').read_text()) - int((cards[0] / 'mem_info_vram_used').read_text()) + check(free_vram >= 8 * 1024**3, 'less than 8 GiB discrete VRAM available') + return {'operator': properties, 'kfd_processes': [], 'host_available_bytes': available, + 'discrete_free_vram_bytes': free_vram} + +# No GPU call or evidence mutation before the explicit mode and idle-window gates. +if not component_only: + text_proof = text_proof.resolve(strict=True) + allowed = [home / f'{tree}/artifacts/fitter-fix' for tree in + ('lucebox-ds4v-mix-fix', 'lucebox-ds4v-mix-parallel')] + check(text_proof.parent in allowed and text_proof.name.startswith('load-proof-'), 'unexpected text-proof directory') + check((text_proof / 'harness.exit').read_text().strip() == '0', 'private text proof did not complete successfully') + check(bool((text_proof / 'cleanup.txt').read_text().strip()), 'private text proof cleanup missing') + verdict = json.loads((text_proof / 'verdict.json').read_text()) + check(all(verdict.get(k) == 'PASS' for k in ('text_load_smoke', 'math_answer', 'longer_decode_speculation')), + 'private text verdict is not PASS') + server_pid = int((text_proof / 'server.pid').read_text()) + check(server_pid > 1 and not Path(f'/proc/{server_pid}').exists(), 'private text server PID remains present') +initial_window = idle_window() +check(evidence.is_absolute() and not evidence.exists(), 'provide a fresh absolute evidence directory') +evidence.mkdir() # Parent must already exist; never remove or reuse evidence. +print(f'Evidence: {evidence}', flush=True) +active = None +summary = {'status': 'ISSUES', 'features': 'ISSUES', 'embeddings': 'NOT_QUALIFIED', + 'source_sha': source_sha, 'device': 'hip:0', 'text_proof': str(text_proof) if text_proof else None, + 'scope': 'standalone component; no chat/server acceptance', 'component_only': component_only, + 'initial_window': initial_window, 'lanes': []} +exit_code = 1 + +def interrupted(signum, frame): + raise InterruptedError(signum) + +signal.signal(signal.SIGINT, interrupted) +signal.signal(signal.SIGTERM, interrupted) + +def memory(): + result = {'monotonic_seconds': time.monotonic(), 'host': Path('/proc/meminfo').read_text()} + result['drm'] = {str(p): p.read_text().strip() for p in Path('/sys/class/drm').glob('card*/device/mem_info_*') + if p.name in ('mem_info_vram_total', 'mem_info_vram_used', 'mem_info_gtt_used')} + return result + +def stop_owned(): + global active + if active is not None: + # This unreaped direct child cannot have its PID reused. Never pkill or signal other processes. + active.terminate() + try: active.wait(timeout=5) + except subprocess.TimeoutExpired: active.kill(); active.wait() + lane = summary['lanes'][-1] + lane.update(exit=active.returncode, stopped_by_harness=True) + (evidence / f"{lane['name']}.exit").write_text(str(active.returncode) + '\n') + dump(f"{lane['name']}.time.json", lane) + active = None + +def run(name, command, timeout=900): + global active + lane = {'name': name, 'command': list(map(str, command))} + summary['lanes'].append(lane) + started = time.monotonic() + with open(evidence / f'{name}.log', 'w') as log, open(evidence / f'{name}.memory.jsonl', 'w') as samples: + active = subprocess.Popen(lane['command'], stdout=log, stderr=subprocess.STDOUT) + lane['pid'] = active.pid + (evidence / f'{name}.pid').write_text(str(active.pid) + '\n') + dump('summary.json', summary) + while True: + pid, status, usage = os.wait4(active.pid, os.WNOHANG) + if pid: + code = os.waitstatus_to_exitcode(status) + active.returncode = code + active = None + lane.update(exit=code, elapsed_seconds=time.monotonic()-started, + user_seconds=usage.ru_utime, system_seconds=usage.ru_stime, max_rss_kib=usage.ru_maxrss) + (evidence / f'{name}.exit').write_text(str(code) + '\n') + dump(f'{name}.time.json', lane) + dump('summary.json', summary) + return code + sample = memory() + try: sample['process_status'] = Path(f'/proc/{active.pid}/status').read_text() + except FileNotFoundError: pass + samples.write(json.dumps(sample) + '\n'); samples.flush() + if time.monotonic()-started > timeout: raise TimeoutError(f'{name} exceeded {timeout}s') + time.sleep(0.5) + +def verify_device(name): + log = (evidence / f'{name}.log').read_text() + check(re.search(r'^backend=ROCm0 requested=hip:0$', log, re.M), f'{name}: not the requested HIP backend') + check(re.search(r'Device 0: [^\n]*7900 XT[^\n]*gfx1100', log), f'{name}: device0 is not 7900 XT/gfx1100') + check(re.search(r'Device 1: [^\n]*gfx1151', log), f'{name}: device1 order changed') + +try: + check(subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() == source_sha, + 'runtime source commit changed') + subprocess.run(['git', '-C', str(root), 'diff', '--quiet', 'HEAD', '--'], check=True) + for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed: {path}') + manifest = json.loads((reference / 'manifest.json').read_text()) + check(set(manifest['images']) == {'carrots', 'corn'}, 'unexpected fixture set') + fixtures = {} + for label, grid in [('carrots', [42, 61]), ('corn', [23, 34])]: + entry = manifest['images'][label] + check(entry['vit_grid'] == grid, f'{label}: grid changed') + for stage in ('patches', 'features', 'embeddings'): + meta = entry[stage]; path = reference / meta['file'] + check(path.parent == reference and digest(path) == meta['sha256'], f'{label}/{stage}: fixture changed') + fixtures[str(path)] = meta + ldd = subprocess.check_output(['/usr/bin/ldd', str(binary)], text=True) + check('not found' not in ldd and 'libggml-hip.so' in ldd, 'HIP dependency resolution failed') + (evidence / 'ldd.txt').write_text(ldd) + libraries = sorted(set(re.findall(r'(?:=>\s+|^\s*)(/[^\s]+)\s+\(', ldd, re.M))) + software = {path: digest(path) for path in libraries} + software[str(Path(sys.executable).resolve())] = digest(Path(sys.executable).resolve()) + dump('provenance.json', {'source_sha': source_sha, 'pinned': {str(p): s for p, s in pinned.items()}, + 'shared_libraries_and_python': software, 'fixtures': fixtures, + 'environment': dict(os.environ), 'python': sys.version, 'harness_sha256': digest(harness), + 'text_proof_files': {p.name: digest(p) for p in text_proof.iterdir() if p.is_file()} if text_proof else {}}) + dump('memory-before.json', memory()) + dump('window-before-gpu.json', idle_window()) + check(run('device-check', [binary, mmproj, '--load-only', '4096', '129280', 'hip:0']) == 0, + 'HIP load-only/device check failed') + verify_device('device-check') + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for label, h, w, output, lane in [('carrots', 42, 61, native, 'carrots'), + ('corn', 23, 34, native, 'corn'), ('corn', 23, 34, repeat, 'corn-repeat')]: + check(run(lane, [binary, mmproj, reference / f'{label}-patches.f32', str(h), str(w), output, label, '0', 'hip:0']) == 0, + f'{lane}: HIP encode failed') + verify_device(lane) + # compare.py requires both fixture names: explicitly reuse the first carrots result in repeat comparison. + for stage in ('features', 'embeddings'): + (repeat / f'carrots-{stage}.f32').symlink_to(native / f'carrots-{stage}.f32') + statuses = [run(name, [sys.executable, '-I', compare, reference, output, '--output', evidence / f'{name}.json']) + for name, output in [('comparison', native), ('repeat-comparison', repeat)]] + check(all(code in (0, 3) for code in statuses), 'comparison execution/shape/hash failure') + comparisons = [json.loads((evidence / f'{name}.json').read_text()) for name in ('comparison', 'repeat-comparison')] + summary['features'] = 'PASS' if all(c[label]['features']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' + summary['embeddings'] = 'PASS' if all(c[label]['embeddings']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' + summary['corn_repeat_byte_identical'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') + for s in ('features', 'embeddings')) + dump('outputs.sha256.json', {str(p.relative_to(evidence)): digest(p) for directory in (native, repeat) for p in directory.glob('*.f32')}) + for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed during qualification: {path}') + exit_code = 3 if 3 in statuses or not summary['corn_repeat_byte_identical'] else 0 + summary['status'] = 'PASS' if exit_code == 0 else 'ISSUES' +except InterruptedError as error: + exit_code = 128 + int(error.args[0]); summary['error'] = f'interrupted by signal {error.args[0]}' + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +except TimeoutError as error: + exit_code = 124; summary['error'] = str(error) + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +except Exception as error: + summary['error'] = f'{type(error).__name__}: {error}' + summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') +finally: + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + stop_owned() + # Once the fixed comparator returned 3, no later command may turn that into success or mask it. + if any(lane['name'] in ('comparison', 'repeat-comparison') and lane.get('exit') == 3 for lane in summary['lanes']): + exit_code = 3 + summary['exit'] = exit_code + dump('summary.json', summary) + dump('memory-after.json', memory()) + (evidence / 'harness.exit').write_text(str(exit_code) + '\n') + print(json.dumps(summary, indent=2), flush=True) +raise SystemExit(exit_code) +PY diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py new file mode 100644 index 000000000..e932c7903 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py @@ -0,0 +1,263 @@ +"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. + +This supervises three sequential Radeon-only lanes through the separate live +operator guard. It makes no isolated performance or HTTP acceptance claim. +""" +import argparse +import fcntl +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import signal +import stat +import subprocess +import sys + +HOME = Path('/home/marcelorm') +ROOT = HOME / 'lucebox-ds4v-vision-unbiased' +BUILD = Path('/tmp/ds4v-unbiased-hip-build') +SOURCE = '53bf07d8d327f93fc4a4f479220023907154cb3a' +REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' +CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' +COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' +PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' +FIXED = { + BUILD / 'ds4v_vision_probe': 'd0e610716acd357cf295013d018caa26066d520722a9b35f21ba60013d5b36cc', + COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', + HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', + REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', + CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', + REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', +} +LIBRARIES = { + 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', 'ddd61d98d11d5209466e74a803c421079dbddba5dcb92d2acb6c638d55b78278'), + 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '744352d31cc70b3f4afcbf17f56f49399056f80b493748f65e05de556572bfe9'), + 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '17d1f73ffeb9ac6701bc5662b6a888bc35a9545c2e6c8025e46117eb7ad4aa49'), + 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', 'a18bc2a1adbebc448c652e79b3e97862fd1b09926044025e339ab951a515ae68'), + 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), + 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), +} + +def require(ok, why): + if not ok: + raise RuntimeError(why) + +def digest(path): + with Path(path).open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + +def verify_pins(pins): + for path, sha in pins.items(): + require(re.fullmatch('[0-9a-f]{64}', sha) is not None, 'unbound or invalid pin: ' + str(path)) + require(digest(path) == sha, 'pin changed: ' + str(path)) + +def guarded_run(command): + child = subprocess.Popen(command) + previous = {} + def interrupted(signum, frame): + raise InterruptedError(f'qualification interrupted: {signum}') + try: + for signum in (signal.SIGTERM, signal.SIGINT): + previous[signum] = signal.signal(signum, interrupted) + require(child.wait() == 0, 'lane supervision failed') + finally: + # The guard handles SIGTERM by stopping/reaping its direct GPU child. + # Never kill the guard while it might still own a live GPU process. + if child.poll() is None: + child.terminate() + child.wait(timeout=30) + for signum, handler in previous.items(): + signal.signal(signum, handler) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--config', type=Path, required=True) + parser.add_argument('--config-sha', required=True) + parser.add_argument('--parent-radeon-window-released', action='store_true') + args = parser.parse_args() + require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') + require(digest(args.config) == args.config_sha, 'config changed') + cfg = json.loads(args.config.read_text()) + require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') + production_path = Path(cfg['production_pins']['path']) + production_sha = cfg['production_pins']['sha256'] + require(re.fullmatch('[0-9a-f]{64}', production_sha) is not None + and digest(production_path) == production_sha, 'production pin manifest changed') + production = json.loads(production_path.read_text()) + require(production.get('schema') == 'ds4v-unbiased-production-runtime-v1' + and production.get('source_root') == str(ROOT) + and production.get('source_commit') == SOURCE and isinstance(production.get('files'), dict) + and production['files'], 'production source/file pins missing') + linear_path = Path(cfg['linear_receipt']['path']) + require(digest(linear_path) == cfg['linear_receipt']['sha256'], 'linear acceptance receipt changed') + linear = json.loads(linear_path.read_text()) + require(linear.get('schema') == 'ds4v-unbiased-native-linear-proof-v1' and linear.get('pass') is True + and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') + require(linear['source_commit'] == SOURCE, 'linear source mismatch') + require(linear['pins_sha256'] == production_sha, 'linear production pins mismatch') + require([x['name'] for x in linear['lanes']] == ['tiny', 'patch', 'qkv', 'mlp_w1', 'mlp_w2'], 'five production linear lanes required') + for lane in linear['lanes']: + require(lane['guard_exit'] == 0 and lane['child_exit'] == 0 + and lane['component_numeric_pass'] is True, 'linear lane not accepted') + require(lane['source_bitwise_mismatches'] == 0, 'production linear differs from frozen source') + expected_dispatch = 1 if lane['name'] in ('mlp_w1', 'mlp_w2') else 2 + require(lane['explicit_ops'] == lane['actual_lt_launches'] == expected_dispatch, + 'wrong production linear dispatch count') + require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') + pins = dict(FIXED) + pins[args.config] = args.config_sha + pins[linear_path] = cfg['linear_receipt']['sha256'] + pins[production_path] = production_sha + pins.update({path: sha for path, sha in LIBRARIES.values()}) + for path, sha in production['files'].items(): + require(Path(path).is_absolute(), 'absolute production file pin required') + require(Path(path) not in pins or pins[Path(path)] == sha, 'production pin conflicts with qualification pin: ' + path) + pins[Path(path)] = sha + for lane in linear['lanes']: + pins[Path(lane['guard_report_path'])] = lane['guard_report_sha256'] + runtime = Path(cfg['comparator_runtime']) + policy = Path(cfg['qualification_policy']) + pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', + policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) + verify_pins(pins) + pins.update(json.loads(runtime.read_text())['files']) + for directory in (REFERENCE, CPU_REFERENCE): + manifest = json.loads((directory / 'manifest.json').read_text()) + require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') + for label, entry in manifest['images'].items(): + require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') + for stage in ('patches', 'features', 'embeddings'): + path = directory / entry[stage]['file'] + require(path.parent == directory, 'fixture path escapes reference') + pins[path] = entry[stage]['sha256'] + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) + require('not found' not in ldd, 'unresolved dependency') + resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) + for soname, (path, _) in LIBRARIES.items(): + # The standalone probe links the backend libraries directly; the umbrella + # libggml is built/pinned but omitted by the linker's --as-needed rule. + if soname == 'libggml.so.0' and soname not in resolved: + continue + require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) + verify_pins(pins) + # Guard code and lane policies are frozen before importing or invoking them. + guard_dir = Path(cfg['guard_dir']) + require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') + pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) + verify_pins(pins) + require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') + sys.path.insert(0, str(guard_dir)) + from run import load_policy, launch_command + from host import Host + from guard import prepare_preflight + require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') + policies = [] + for lane in cfg['lanes']: + p = load_policy(Path(lane['policy']), lane['sha256']) + pins[Path(lane['policy'])] = lane['sha256'] + isolation_pins = p.get('isolation_pins', {}) + require('/usr/bin/python3.12' in isolation_pins and + all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), + 'namespace runtime is not included in guarded component pins') + for path, sha in p['component_pins'].items(): + require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) + pins[Path(path)] = sha + label = lane['name'].split('-')[0] + h, w = (42, 61) if label == 'carrots' else (23, 34) + out = guard_dir / p['run_name'] + expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), + str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] + require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') + require('hip_vision_linear_launches=131 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') + sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, + str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} + require(set(p['required_outputs']) == set(sizes), 'wrong output contract') + require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') + policies.append((p, out, label)) + evidence = Path(cfg['evidence']) + require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') + evidence.mkdir() + report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', + 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), + 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'linear_receipt': cfg['linear_receipt'], + 'production_pins': cfg['production_pins'], 'lanes': []} + try: + for lane, (p, out, label) in zip(cfg['lanes'], policies): + guarded_run([str(PYTHON), '-I', str(guard_dir / 'run.py'), '--policy', lane['policy'], + '--policy-sha', lane['sha256'], '--parent-radeon-window-released']) + result = json.loads((out / 'guard.json').read_text()) + require(result['pass'] and result['device_proof_verified'], 'guard/result failed') + require(result.get('namespace_verified') is True, 'private NPU namespace not verified') + require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') + require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] + and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', + 'guard report belongs to a different command/policy/outcome') + require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') + for file, meta in result['output_evidence'].items(): + require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') + require(digest(file) == meta['sha256'], 'guard output changed before copying') + pins[Path(file)] = meta['sha256'] + pins[out / 'guard.json'] = digest(out / 'guard.json') + log = (out / 'child.log').read_text() + require(re.findall(r'^hip_vision_linear_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('131', '79691776')], 'ambiguous/incomplete full-tower dispatch') + pins[out / 'child.log'] = digest(out / 'child.log') + report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], + 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], + 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], + 'command': p['command'], 'launch_command': result['launch_command'], + 'namespace_verified': result['namespace_verified'], + 'child_exit': result['exit'], 'outputs': result['output_evidence']}) + native, repeat = evidence / 'native', evidence / 'repeat' + native.mkdir(); repeat.mkdir() + for index, (_, out, label) in enumerate(policies): + for stage in ('features', 'embeddings'): + original = out / f'{label}-{stage}.f32' + copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied output differs from guarded output') + pins[copied] = pins[original] + for stage in ('features', 'embeddings'): + original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' + shutil.copyfile(original, copied) + require(digest(copied) == pins[original], 'copied repeat carrots differ') + pins[copied] = pins[original] + codes = {} + for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), + ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: + verify_pins(pins) + with (evidence / f'{name}.log').open('w') as log: + result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], + stdout=log, stderr=subprocess.STDOUT, timeout=120) + require(result.returncode in (0, 3), 'comparator execution failed: ' + name) + codes[name] = result.returncode + report['comparisons'] = codes + report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) + verify_pins(pins) + require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') + subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) + p = policies[-1][0] + host = Host(p) + fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) + try: + st = os.fstat(fd) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) + finally: + os.close(fd) + (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') + report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] + except Exception as error: + report['error'] = f'{type(error).__name__}: {error}' + finally: + (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') + print(json.dumps(report, indent=2)) + return 0 if report['pass'] else 3 + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/how-backend.md b/harness/qualification/deepseek4/ds4v-vision/how-backend.md new file mode 100644 index 000000000..5dbd51460 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/how-backend.md @@ -0,0 +1,19 @@ +# Backend how pass + +PASS. Read-only explorer at07e3284. Inherited session model. + +HTTP entry is server/src/server/http_server.cpp route_request:2127. normalize_chat_messages:1031-1044 retains text blocks and silently discards image_url. render_and_tokenize_request:2043-2056 renders only text. Context validation:2060 counts those tokens. ParsedRequest in http_server.h:269 has original messages and tokens but no image data. GenerateRequest in common/model_backend.h:170 is token-only. DS4 uses the serialworker at http_server.cpp:3917, not the sequence engine. process_job:3926 builds GenerateRequest and dispatches generate/restore_and_generate:4078-4081. + +DeepSeek4Backend::generate_from_state:2342 selects fresh or restored prefill. do_prefill:1861 chunks, may split at snapshot or speculative capture boundaries, embeds at2041-2044, and dispatches paired graph at2086-2111. deepseek4_step_layer_range in deepseek4_graph.cpp accepts F32embeddings and original token IDs independently. It expands embeddings into HC at7080-7086. This is the embedding injection boundary. CpuEmbedder in qwen35/gguf_target_loader.cpp:83-93 rejects IDs outside vocabulary; DS4 ignores that return at backend.cpp:2044. + +Model loader deepseek4_loader.cpp:237-258 drops unknown global tensors. Binding1830-1900 lacks vision, aligner, image delimiters and bias_vl. Ordinary bias binds at1886. DeepSeek4Layer at internal.h:135 needs the image routing bias. + +Routing has several paths. GPU build_moe_routing at graph.cpp:3256-3299; old host hybrid4481-4520; sparse paired host5847-5900; standard layer-major6619-6629; generic layer-range8096-8184. Fused paths5424and5638also use hash routing. Sparse paired path5864explicitly rejects token IDs>=vocab. All image sentinel tokens require learned routing in hash layers. Mixture weights remain unbiased. + +Layer-major attention mask at graph.cpp:2023-2050 masks future rows and rows beyond ordinary sliding window. Reference model.py:283-305 gives image tokens left/right visibility across the complete image span. Raw KVrows already combine prior,current,compressed at1924-1928. Preserve compressed visibility while changing raw-image masking. Graph compressor-boundary recursion6973-7022and backend snapshot/capture splitting must not split an image span. A chunk1fallback is incorrect for vision. + +Token-only cache lookup at http_server.cpp:3159-3179 and3258-3264will collide for different images with equal layouts. Either incorporate image bytes and preprocessing/model identity or disable multimodal reuse initially. prepare_prompt2949may rewrite tokens through PFlash/FlowKV; PPP2149and3094may rearrange. These need a multimodal policy preserving alignment. process_job3951logs message JSON before truncation, so redact dataURLs. + +Backend factory435-455selects monolithic DS4 by default; hybrid expert parallelism lives inside this backend and is distinct from layer split. + +No runtime vision claims. Source parity and image HTTP lanes remain mandatory. diff --git a/harness/qualification/deepseek4/ds4v-vision/how-source.md b/harness/qualification/deepseek4/ds4v-vision/how-source.md new file mode 100644 index 000000000..947007404 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/how-source.md @@ -0,0 +1,17 @@ +# Parent source how pass + +PASS. Source files came from the verified parent checkpoint on soulf. The CPU reference fixtures ran the original Python files with torch 2.10.0+cpu, numpy, Pillow, and safetensors 0.7.0. Both supplied photos produced finite tower outputs and aligned embeddings. The manifest records source file hashes, image hashes, dimensions, and fixture hashes. + +The tower has 32 blocks, width 1024, 16 heads, and 14 by 14 RGB patches. Each block has RMSNorm with epsilon 1e-6, combined QKV projection and bias, full bidirectional attention, output projection and bias, then another RMSNorm and a fused gate/up SwiGLU MLP. The final tower norm also uses epsilon 1e-6. + +The two-dimensional rotary code uses a 64-dimensional head. It splits the head into two halves, each width 32. Height and width frequencies occupy 16 values each within those halves. It does not use adjacent-pair rotation. The positional phase derives from the row-major patch grid. This must match the reference rather than borrowing a different model's RoPE arrangement. + +The aligner pads the patch grid on its bottom and right to a multiple of 3. It gathers nonoverlapping 3 by 3 patches in channel-first unfold order. The resulting input width is 9216. A biased linear maps to 4096, exact GELU follows, and another biased 4096 linear produces language embeddings. + +Preprocessing uses the parent's resize budget, aspect-ratio policy, RGB padding, and pixel normalization. It casts normalized pixels to BF16 before arranging channel-major patches. ImageOps.pad and Pillow's resize behavior are part of the numerical reference. A replacement decoder/resizer needs an empirical comparison, not an assumption that bilinear resizing is equivalent. + +build_image_block introduces compression-alignment padding based on the current prompt position. It interleaves pairs of image rows in N order and carries a separate permutation for aligned image embeddings. Types are start=0, pad=1, image=2, newline=3, end=4. Sentinel IDs are vocabulary size plus type. All sentinel types use image routing, including padding outside the bidirectional start/end interval. + +The reference images are already present on soulf at the parent inference/examples/images directory. Carrots uses a 42 by 61 ViT grid and a 14 by 21 aligner grid. Corn uses a 23 by 34 ViT grid and an 8 by 12 aligner grid. Fixtures cover start positions 0, 1, 2, 3, and 127. + +The reference data stays on soulf under ~/lucebox-ds4v-mix-fix/artifacts/vision-reference. Only the manifest and run log have been copied to this Mac. diff --git a/harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py b/harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py new file mode 100644 index 000000000..389a031cc --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py @@ -0,0 +1,48 @@ +import hashlib +import json +from pathlib import Path +import struct +import sys + +repo, source, output, evidence = map(Path, sys.argv[1:]) +sys.path.insert(0, str(repo / 'server/deps/llama.cpp/gguf-py')) +import gguf + +weight_map = json.loads((source / 'model.safetensors.index.json').read_text())['weight_map'] +selected = {name: shard for name, shard in weight_map.items() + if name.startswith(('vision.', 'aligner.', 'image_'))} +reader = gguf.GGUFReader(output) +assert set(selected) == {tensor.name for tensor in reader.tensors} +assert len(selected) == 267 +assert reader.get_field('general.architecture').contents() == 'deepseek4_vision' +headers = {} +for shard in set(selected.values()): + with (source / shard).open('rb') as handle: + size = struct.unpack(' "$proof/comparison.log" 2>&1 +comparison_exit=$? +set -e +printf '%s\n' "$comparison_exit" > "$proof/comparison.exit" +[ "$comparison_exit" = 3 ] +"$python" - "$proof" <<'PY' +import json, sys +from pathlib import Path +import numpy as np +p=Path(sys.argv[1]) +best=(0,0,0) +for h in range(1,1153): + for w in range(1,1153): + a,b=(h+2)//3,(w+2)//3 + rows=a+a%2 + block=rows*(b+1)+2+(rows//2*(b+1)%2)*2 + if block+3<=384 and h*w>best[0]: + best=(h*w,h,w) +assert best==(3366,6,561),best +values=((np.arange(best[0]*588,dtype=np.int32)%31)-15).astype(np.float32)/16 +values.tofile(p/'maximum-patches.f32') +(p/'maximum-grid.json').write_text(json.dumps(dict(patches=best[0],height=best[1],width=best[2],purpose='largest grid permitted by full block budget, not a resize aspect-policy fixture'))+'\n') +PY +for stages in 0 1; do + /usr/bin/time -v "$probe" "$HOME/ds4v-work/ds4v-mmproj.gguf" "$proof/maximum-patches.f32" 6 561 "$proof/maximum-$stages" maximum "$stages" > "$proof/maximum-$stages.log" 2>&1 +done +"$python" - "$proof" <<'PY' +from pathlib import Path +import hashlib,json,sys +import numpy as np +p=Path(sys.argv[1]); result={} +for stage in ('features','embeddings'): + paths=[p/f'maximum-{i}'/f'maximum-{stage}.f32' for i in (0,1)] + arrays=[np.fromfile(path,np.float32) for path in paths] + assert all(np.isfinite(a).all() for a in arrays) + assert arrays[0].size==(3366*1024 if stage=='features' else 374*4096) + assert np.array_equal(*arrays) + result[stage]=dict(finite=True,observer_invariance='PASS',elements=arrays[0].size,sha256=hashlib.sha256(paths[0].read_bytes()).hexdigest()) +(p/'maximum-verdict.json').write_text(json.dumps(result,indent=2)+'\n') +PY +date -u +%FT%TZ > "$proof/qualification.finished" diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py new file mode 100644 index 000000000..5476f430e --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""CPU-only three-way comparison of already completed corn output files.""" +import hashlib +import json +import os +from pathlib import Path +import sys +import numpy as np + +assert sys.flags.isolated == 1 +for key in ('HIP_VISIBLE_DEVICES','ROCR_VISIBLE_DEVICES','CUDA_VISIBLE_DEVICES'): + assert os.environ.get(key) == '-1' +reference, source, native, output = map(Path, sys.argv[1:]) +assert not output.exists() +manifest_bytes = (reference/'manifest.json').read_bytes() +sha = lambda b: hashlib.sha256(b).hexdigest() +assert sha(manifest_bytes) == '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f' +manifest = json.loads(manifest_bytes) +source_report = json.loads((source/'report.json').read_text()) +assert source_report['script_sha256'] == 'cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c' +assert source_report['device']['name'] == 'Radeon RX 7900 XT' +assert source_report['device']['gcn_arch'].split(':')[0] == 'gfx1100' +assert source_report['image'] == 'corn' +native_sha = {'features':'59bd19a13750d07f7f1018c32c5a43c4ae2cd7a7ff132da3f6201b08c400cc4e', + 'embeddings':'a398c9c10a7b2bbeb63f4b910bf5f71bb9fefd0aa388b276a3bf06ca3e280a06'} +gates = {'features':{'max_abs':.25,'rmse':.03,'cosine':.9995}, + 'embeddings':{'max_abs':.75,'rmse':.08,'cosine':.9990}} +report = {'diagnostic_only':True, 'native_acceptance':'NOT_QUALIFIED', 'image':'corn', + 'script_sha256':sha(Path(__file__).read_bytes()), 'hashes':{}, 'comparisons':{}} +for stage,gate in gates.items(): + expected = manifest['images']['corn'][stage] + raw_cpu = (reference/expected['file']).read_bytes() + raw_source = (source/source_report['outputs'][stage]['file']).read_bytes() + raw_native = (native/('corn-'+stage+'.f32')).read_bytes() + assert sha(raw_cpu) == expected['sha256'] + assert sha(raw_source) == source_report['outputs'][stage]['sha256'] + assert sha(raw_native) == native_sha[stage] + values = {name:np.frombuffer(raw,np.float32).reshape(expected['shape']) + for name,raw in [('original_cpu',raw_cpu),('source_hip',raw_source),('native_hip',raw_native)]} + report['hashes'][stage] = {'original_cpu':sha(raw_cpu),'source_hip':sha(raw_source),'native_hip':sha(raw_native)} + report['comparisons'][stage] = {} + for name,left,right in [('source_hip_vs_original_cpu','source_hip','original_cpu'), + ('native_hip_vs_source_hip','native_hip','source_hip'), + ('native_hip_vs_original_cpu','native_hip','original_cpu')]: + a,b=values[left].astype(np.float64).ravel(),values[right].astype(np.float64).ravel() + d=a-b + row={'shape':expected['shape'],'finite':bool(np.isfinite(a).all() and np.isfinite(b).all()), + 'max_abs':float(np.abs(d).max()),'rmse':float(np.sqrt(np.mean(d*d))), + 'cosine':float(np.dot(a,b)/(np.linalg.norm(a)*np.linalg.norm(b))), + 'exact_fraction':float(np.mean(a==b)), + 'byte_identical':report['hashes'][stage][left]==report['hashes'][stage][right], 'gate':gate} + row['pass']=row['finite'] and row['max_abs']<=gate['max_abs'] and row['rmse']<=gate['rmse'] and row['cosine']>=gate['cosine'] + report['comparisons'][stage][name]=row +output.write_text(json.dumps(report,indent=2)+'\n') +print(json.dumps(report,indent=2)) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt new file mode 100644 index 000000000..d28a76065 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt @@ -0,0 +1,4 @@ +torch==2.10.0+rocm7.2.4.lw.git3d3aa833 +numpy==2.5.2 +Pillow==12.3.0 +safetensors==0.7.0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py new file mode 100644 index 000000000..5acf5c03c --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +"""CPU-only import metadata: deliberately makes no torch.cuda calls.""" +import importlib.metadata +import hashlib +import json +import os +from pathlib import Path +import platform +import sys + +assert sys.flags.isolated == 1 +assert sys.prefix != sys.base_prefix +for key in ('HIP_VISIBLE_DEVICES','ROCR_VISIBLE_DEVICES','CUDA_VISIBLE_DEVICES'): + assert os.environ.get(key) == '-1' +private_lib = Path.home()/'ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib' +assert os.environ.get('LD_LIBRARY_PATH') == str(private_lib) +with (private_lib/'libMIOpen.so.1').open('rb') as f: + assert hashlib.file_digest(f,'sha256').hexdigest() == 'bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd' +sys.dont_write_bytecode = True +import torch +import numpy +import PIL +import safetensors +assert importlib.metadata.version('torch') == '2.10.0+rocm7.2.4.lw.git3d3aa833' +assert torch.__version__ == '2.10.0+rocm7.2.4.git3d3aa833' +torch.set_num_threads(2) +torch.set_num_interop_threads(2) +libs = sorted({line.split()[-1] for line in Path('/proc/self/maps').read_text().splitlines() + if '.so' in line and line.split()[-1].startswith('/')}) +report = dict(torch=torch.__version__, torch_git=torch.version.git_version, + hip_version=torch.version.hip, rocm_version=torch.version.rocm, torch_module=torch.__file__, torch_extension=torch._C.__file__, + numpy=numpy.__version__, pillow=PIL.__version__, safetensors=safetensors.__version__, + python=sys.version, prefix=sys.prefix, base_prefix=sys.base_prefix, path=sys.path, + platform=platform.platform(), system_rocm=Path('/opt/rocm/.info/version').read_text().strip(), + venv_config=(Path(sys.prefix)/'pyvenv.cfg').read_text(), + environment={k:os.environ.get(k) for k in ('HIP_VISIBLE_DEVICES','ROCR_VISIBLE_DEVICES','CUDA_VISIBLE_DEVICES', + 'OMP_NUM_THREADS','MKL_NUM_THREADS','OPENBLAS_NUM_THREADS','LD_LIBRARY_PATH','LD_PRELOAD')}, + packages={d.metadata['Name']:d.version for d in importlib.metadata.distributions()}, + loaded_libraries=libs, torch_config=torch.__config__.show(), + parallel_info=torch.__config__.parallel_info(), gpu_api_called=False) +print(json.dumps(report,indent=2,sort_keys=True)) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit new file mode 100644 index 000000000..573541ac9 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit @@ -0,0 +1 @@ +0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log new file mode 100644 index 000000000..af30448e5 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log @@ -0,0 +1,83 @@ +{ + "diagnostic_only": true, + "native_acceptance": "NOT_QUALIFIED", + "device": { + "kind": "cpu", + "gpu_api_called": false + }, + "torch": "2.10.0+rocm7.2.4.git3d3aa833", + "torch_git": "3d3aa833db84eed6b7f5595cb5f162c2f78300a4", + "torch_hip": "7.2.53211", + "numpy": "2.5.2", + "private_library_path": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", + "threads": [ + 2, + 2 + ], + "script_sha256": "17ba9d66260d25c056ecc56a35192748a662b040605bb0bf8864bd704a66267b", + "reference_manifest_sha256": "38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f", + "source_hashes": { + "config.json": "6cd841bdd6702f5e2ac34671bc78047ed80817102465525ae2a41c502abbcd75", + "inference/vision.py": "a4f089069310398d42ca17fd4496cec82da64cbbfde9b0230679ce1537cc0bb1", + "inference/image_processor.py": "cac2ff6af15207ce53d0319dc52c6ed1fa5f4fed21f75795fe0fc014632e9086" + }, + "index_sha256": "507977e3d3818865264e68c0fdab139aa7f3929d0d0cf693dacc47428da56395", + "weight_inventory_sha256": "b0556c40a8bff3f4c2c262d57137a97123cbdbf7444a7fae495ef17cd28469ee", + "image": "corn", + "patch_sha256": "6ef206f7dbe317c0d456de4e9501dc5e734b05d91d7a5341d463187a28233d3a", + "outputs": { + "features": { + "file": "corn-features.f32", + "shape": [ + 782, + 1024 + ], + "sha256": "aa7c43be7182759f83881cf823661bf52c14b73222645d1ec37b14c6502bc982" + }, + "embeddings": { + "file": "corn-embeddings.f32", + "shape": [ + 96, + 4096 + ], + "sha256": "c96d59ae722ad8ac31299aabb4e758b788a1ee4be30ea94b833c753721229040" + } + }, + "comparisons": { + "features": { + "finite": true, + "max_abs": 0.0, + "rmse": 0.0, + "cosine": 1.0, + "exact_fraction": 1.0, + "byte_identical": true, + "gate": { + "max_abs": 0.25, + "rmse": 0.03, + "cosine": 0.9995 + }, + "pass": true + }, + "embeddings": { + "finite": true, + "max_abs": 0.0, + "rmse": 0.0, + "cosine": 0.9999999999999998, + "exact_fraction": 1.0, + "byte_identical": true, + "gate": { + "max_abs": 0.75, + "rmse": 0.08, + "cosine": 0.999 + }, + "pass": true + } + }, + "forward_seconds": 1.8079554990399629, + "attention": "Unchanged original SDPA call, default dispatch; no backend forcing/autocast/compile/patch", + "rotary_context": "cpu", + "default_dtype": "torch.bfloat16", + "source_portability_gate": "PASS", + "elapsed_seconds_after_imports": 4.954526190995239, + "max_rss_kib": 2418408 +} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time new file mode 100644 index 000000000..7088b89ef --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time @@ -0,0 +1,23 @@ + Command being timed: "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python -I -B /home/marcelorm/ds4v-work/source-rocm210-reference/source-forward.py --device cpu --image corn --source /home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored --reference /home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference --output /home/marcelorm/ds4v-work/source-rocm210-reference/cpu-corn" + User time (seconds): 6.40 + System time (seconds): 1.03 + Percent of CPU this job got: 113% + Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.58 + Average shared text size (kbytes): 0 + Average unshared data size (kbytes): 0 + Average stack size (kbytes): 0 + Average total size (kbytes): 0 + Maximum resident set size (kbytes): 2418408 + Average resident set size (kbytes): 0 + Major (requiring I/O) page faults: 380 + Minor (reclaiming a frame) page faults: 1013278 + Voluntary context switches: 3482 + Involuntary context switches: 108 + Swaps: 0 + File system inputs: 1848888 + File system outputs: 9496 + Socket messages sent: 0 + Socket messages received: 0 + Signals delivered: 0 + Page size (bytes): 4096 + Exit status: 0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr new file mode 100644 index 000000000..9378d942a --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr @@ -0,0 +1,7 @@ +Traceback (most recent call last): + File "/home/marcelorm/ds4v-work/source-rocm210-reference/cpu-runtime-info.py", line 15, in + import torch + File "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/__init__.py", line 431, in + from torch._C import * # noqa: F403 + ^^^^^^^^^^^^^^^^^^^^^^ +ImportError: libMIOpen.so.1: cannot open shared object file: No such file or directory diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-private.stderr b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-private.stderr new file mode 100644 index 000000000..e69de29bb diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log new file mode 100644 index 000000000..bf9a0da6d --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log @@ -0,0 +1,19 @@ +3.12.3 (main, Jun 19 2026, 12:46:00) [GCC 13.3.0] +/home/marcelorm/ds4v-work/source-rocm210-reference/.venv +/usr +https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/torch-2.10.0%2Brocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl +wheel bytes 1647409999 sha256 e3a4b7f11eacc4037bc405fbf8beacf2ce19cc135ad283bb653b93a127f379d0 +Name: torch +Version: 2.10.0+rocm7.2.4.lw.git3d3aa833 +Requires-Python: >=3.10 +Requires-Dist: filelock +Requires-Dist: typing-extensions>=4.10.0 +Requires-Dist: setuptools; python_version >= "3.12" +Requires-Dist: sympy>=1.13.3 +Requires-Dist: networkx>=2.5.1 +Requires-Dist: jinja2 +Requires-Dist: fsspec>=0.8.5 +Requires-Dist: triton==3.6.0+rocm7.2.4.git4ed88892; platform_system == "Linux" and platform_machine == "x86_64" +Requires-Dist: optree>=0.13.0; extra == "optree" +Requires-Dist: opt-einsum>=3.3; extra == "opt-einsum" +Requires-Dist: pyyaml; extra == "pyyaml" diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt new file mode 100644 index 000000000..2f204abc4 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt @@ -0,0 +1,15 @@ +filelock==3.32.5 +fsspec==2026.7.0 +Jinja2==3.1.6 +MarkupSafe==3.0.3 +mpmath==1.3.0 +networkx==3.6.1 +numpy==2.5.2 +pillow==12.3.0 +pip==24.0 +safetensors==0.7.0 +setuptools==84.0.0 +sympy==1.14.0 +torch==2.10.0+rocm7.2.4.lw.git3d3aa833 +triton==3.6.0+rocm7.2.4.git4ed88892 +typing_extensions==4.16.0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 new file mode 100644 index 000000000..0a3e5814a --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 @@ -0,0 +1,3 @@ +17ba9d66260d25c056ecc56a35192748a662b040605bb0bf8864bd704a66267b /home/marcelorm/ds4v-work/source-rocm210-reference/source-forward.py +301fac1f88cc476e713e4e0217c298330ec8fda14e8c13958cbcd3121988b2a4 /home/marcelorm/ds4v-work/source-rocm210-reference/cpu-runtime-info.py +8799d8267cf66d7a4d9f22f7e18cb2cf28b6d75f6dad805b4995b4f953ee41c3 /home/marcelorm/ds4v-work/source-rocm210-reference/run-cpu-control.sh diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log new file mode 100644 index 000000000..9fa591451 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log @@ -0,0 +1,85 @@ +Looking in links: /home/marcelorm/ds4v-work/source-rocm210-reference/wheels, https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/ +Collecting torch==2.10.0+rocm7.2.4.lw.git3d3aa833 + File was already downloaded /home/marcelorm/ds4v-work/source-rocm210-reference/wheels/torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl +Collecting numpy==2.5.2 + Downloading numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB) +Collecting Pillow==12.3.0 + Downloading pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB) +Collecting safetensors==0.7.0 + Downloading safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.1 kB) +Collecting filelock (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading filelock-3.32.5-py3-none-any.whl.metadata (2.0 kB) +Collecting typing-extensions>=4.10.0 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB) +Collecting setuptools (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading setuptools-84.0.0-py3-none-any.whl.metadata (6.6 kB) +Collecting sympy>=1.13.3 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB) +Collecting networkx>=2.5.1 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB) +Collecting jinja2 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB) +Collecting fsspec>=0.8.5 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB) +Collecting triton==3.6.0+rocm7.2.4.git4ed88892 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/triton-3.6.0%2Brocm7.2.4.git4ed88892-cp312-cp312-linux_x86_64.whl (298.5 MB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 298.5/298.5 MB 27.4 MB/s eta 0:00:00 +Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB) +Collecting MarkupSafe>=2.0 (from jinja2->torch==2.10.0+rocm7.2.4.lw.git3d3aa833) + Downloading markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB) +Downloading numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.7 MB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.7/16.7 MB 75.3 MB/s eta 0:00:00 +Downloading pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 98.9 MB/s eta 0:00:00 +Downloading safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (507 kB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 507.2/507.2 kB 71.1 MB/s eta 0:00:00 +Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 44.7 MB/s eta 0:00:00 +Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 60.6 MB/s eta 0:00:00 +Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 97.5 MB/s eta 0:00:00 +Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 12.7 MB/s eta 0:00:00 +Downloading filelock-3.32.5-py3-none-any.whl (100 kB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0/100.0 kB 24.4 MB/s eta 0:00:00 +Downloading jinja2-3.1.6-py3-none-any.whl (134 kB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 35.8 MB/s eta 0:00:00 +Downloading setuptools-84.0.0-py3-none-any.whl (818 kB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 818.2/818.2 kB 81.4 MB/s eta 0:00:00 +Downloading markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB) +Downloading mpmath-1.3.0-py3-none-any.whl (536 kB) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 67.2 MB/s eta 0:00:00 +Saved ./ds4v-work/source-rocm210-reference/wheels/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/triton-3.6.0+rocm7.2.4.git4ed88892-cp312-cp312-linux_x86_64.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/fsspec-2026.7.0-py3-none-any.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/networkx-3.6.1-py3-none-any.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/sympy-1.14.0-py3-none-any.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/typing_extensions-4.16.0-py3-none-any.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/filelock-3.32.5-py3-none-any.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/jinja2-3.1.6-py3-none-any.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/setuptools-84.0.0-py3-none-any.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl +Saved ./ds4v-work/source-rocm210-reference/wheels/mpmath-1.3.0-py3-none-any.whl +Successfully downloaded torch numpy Pillow safetensors triton fsspec networkx sympy typing-extensions filelock jinja2 setuptools MarkupSafe mpmath +Looking in links: /home/marcelorm/ds4v-work/source-rocm210-reference/wheels +Processing ./ds4v-work/source-rocm210-reference/wheels/filelock-3.32.5-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 1)) +Processing ./ds4v-work/source-rocm210-reference/wheels/fsspec-2026.7.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 2)) +Processing ./ds4v-work/source-rocm210-reference/wheels/jinja2-3.1.6-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 3)) +Processing ./ds4v-work/source-rocm210-reference/wheels/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 4)) +Processing ./ds4v-work/source-rocm210-reference/wheels/mpmath-1.3.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 5)) +Processing ./ds4v-work/source-rocm210-reference/wheels/networkx-3.6.1-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 6)) +Processing ./ds4v-work/source-rocm210-reference/wheels/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 7)) +Processing ./ds4v-work/source-rocm210-reference/wheels/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 8)) +Processing ./ds4v-work/source-rocm210-reference/wheels/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 9)) +Processing ./ds4v-work/source-rocm210-reference/wheels/setuptools-84.0.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 10)) +Processing ./ds4v-work/source-rocm210-reference/wheels/sympy-1.14.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 11)) +Processing ./ds4v-work/source-rocm210-reference/wheels/torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 12)) +Processing ./ds4v-work/source-rocm210-reference/wheels/triton-3.6.0+rocm7.2.4.git4ed88892-cp312-cp312-linux_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 13)) +Processing ./ds4v-work/source-rocm210-reference/wheels/typing_extensions-4.16.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 14)) +Installing collected packages: mpmath, typing_extensions, triton, sympy, setuptools, safetensors, pillow, numpy, networkx, MarkupSafe, fsspec, filelock, Jinja2, torch +Successfully installed Jinja2-3.1.6 MarkupSafe-3.0.3 filelock-3.32.5 fsspec-2026.7.0 mpmath-1.3.0 networkx-3.6.1 numpy-2.5.2 pillow-12.3.0 safetensors-0.7.0 setuptools-84.0.0 sympy-1.14.0 torch-2.10.0+rocm7.2.4.lw.git3d3aa833 triton-3.6.0+rocm7.2.4.git4ed88892 typing_extensions-4.16.0 +No broken requirements found. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt new file mode 100644 index 000000000..1e14fc6a3 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt @@ -0,0 +1,53 @@ + +Dynamic section at offset 0x15edae48 contains 50 entries: + Tag Type Name/Value + 0x0000000000000001 (NEEDED) Shared library: [libc10_hip.so] + 0x0000000000000001 (NEEDED) Shared library: [libMIOpen.so.1] + 0x0000000000000001 (NEEDED) Shared library: [libhiprtc.so.7] + 0x0000000000000001 (NEEDED) Shared library: [libhipblas.so.3] + 0x0000000000000001 (NEEDED) Shared library: [libhipfft.so.0] + 0x0000000000000001 (NEEDED) Shared library: [libhiprand.so.1] + 0x0000000000000001 (NEEDED) Shared library: [libhipsparse.so.4] + 0x0000000000000001 (NEEDED) Shared library: [libhipsolver.so.1] + 0x0000000000000001 (NEEDED) Shared library: [librocsolver.so.0] + 0x0000000000000001 (NEEDED) Shared library: [libhipsparselt.so.0] + 0x0000000000000001 (NEEDED) Shared library: [libaotriton_v2.so.0.11.1] + 0x0000000000000001 (NEEDED) Shared library: [librccl.so.1] + 0x0000000000000001 (NEEDED) Shared library: [librocm_smi64.so.1] + 0x0000000000000001 (NEEDED) Shared library: [libc10.so] + 0x0000000000000001 (NEEDED) Shared library: [libtorch_cpu.so] + 0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0] + 0x0000000000000001 (NEEDED) Shared library: [librocblas.so.5] + 0x0000000000000001 (NEEDED) Shared library: [libhipblaslt.so.1] + 0x0000000000000001 (NEEDED) Shared library: [libamdhip64.so.7] + 0x0000000000000001 (NEEDED) Shared library: [libmagma.so] + 0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6] + 0x0000000000000001 (NEEDED) Shared library: [libm.so.6] + 0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1] + 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] + 0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2] + 0x000000000000000e (SONAME) Library soname: [libtorch_hip.so] + 0x000000000000000f (RPATH) Library rpath: [$ORIGIN] + 0x000000000000000c (INIT) 0xcf8000 + 0x000000000000000d (FINI) 0x394f674 + 0x0000000000000019 (INIT_ARRAY) 0x15e0d948 + 0x000000000000001b (INIT_ARRAYSZ) 5744 (bytes) + 0x000000000000001a (FINI_ARRAY) 0x15e0efb8 + 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) + 0x000000006ffffef5 (GNU_HASH) 0x298 + 0x0000000000000005 (STRTAB) 0x11e5b0 + 0x0000000000000006 (SYMTAB) 0x48818 + 0x000000000000000a (STRSZ) 9342546 (bytes) + 0x000000000000000b (SYMENT) 24 (bytes) + 0x0000000000000003 (PLTGOT) 0x15eed000 + 0x0000000000000002 (PLTRELSZ) 365472 (bytes) + 0x0000000000000014 (PLTREL) RELA + 0x0000000000000017 (JMPREL) 0xc9dcc0 + 0x0000000000000007 (RELA) 0xa193f8 + 0x0000000000000008 (RELASZ) 2640072 (bytes) + 0x0000000000000009 (RELAENT) 24 (bytes) + 0x000000006ffffffe (VERNEED) 0xa19128 + 0x000000006fffffff (VERNEEDNUM) 8 + 0x000000006ffffff0 (VERSYM) 0xa07402 + 0x000000006ffffff9 (RELACOUNT) 81085 + 0x0000000000000000 (NULL) 0x0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt new file mode 100644 index 000000000..909161b5f --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt @@ -0,0 +1,45 @@ + linux-vdso.so.1 (0x000077139c61b000) + libc10_hip.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10_hip.so (0x000077138649a000) + libMIOpen.so.1 => not found + libhiprtc.so.7 => /opt/rocm/lib/libhiprtc.so.7 (0x00007713863c7000) + libhipblas.so.3 => /opt/rocm/lib/libhipblas.so.3 (0x00007713862eb000) + libhipfft.so.0 => /opt/rocm/lib/libhipfft.so.0 (0x000077139c5f7000) + libhiprand.so.1 => /opt/rocm/lib/libhiprand.so.1 (0x000077139c5ef000) + libhipsparse.so.4 => /opt/rocm/lib/libhipsparse.so.4 (0x000077139c5aa000) + libhipsolver.so.1 => /opt/rocm/lib/libhipsolver.so.1 (0x00007713862a5000) + librocsolver.so.0 => /opt/rocm/lib/librocsolver.so.0 (0x0000771352600000) + libhipsparselt.so.0 => /opt/rocm/lib/libhipsparselt.so.0 (0x0000771352000000) + libaotriton_v2.so.0.11.1 => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libaotriton_v2.so.0.11.1 (0x000077134ec00000) + librccl.so.1 => /opt/rocm/lib/librccl.so.1 (0x000077132c800000) + librocm_smi64.so.1 => /opt/rocm/lib/librocm_smi64.so.1 (0x00007713524aa000) + libc10.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10.so (0x000077138618f000) + libtorch_cpu.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libtorch_cpu.so (0x0000771317e00000) + libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x000077139c5a3000) + librocblas.so.5 => /opt/rocm/lib/librocblas.so.5 (0x0000771314e00000) + libhipblaslt.so.1 => /opt/rocm/lib/libhipblaslt.so.1 (0x0000771314800000) + libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7 (0x0000771312e00000) + libmagma.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libmagma.so (0x00007712ce600000) + libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007712ce200000) + libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x000077134eb17000) + libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x000077139c571000) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007712cde00000) + /lib64/ld-linux-x86-64.so.2 (0x000077139c61d000) + librocfft.so.0 => /opt/rocm/lib/librocfft.so.0 (0x00007712cc400000) + librocrand.so.1 => /opt/rocm/lib/librocrand.so.1 (0x00007712a0800000) + librocsparse.so.1 => /opt/rocm/lib/librocsparse.so.1 (0x0000771283600000) + libroctx64.so.4 => /opt/rocm/lib/libroctx64.so.4 (0x000077139c56a000) + libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x000077139c565000) + liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x000077134eae5000) + librocprofiler-register.so.0 => /opt/rocm/lib/librocprofiler-register.so.0 (0x000077134ea56000) + libnuma.so.1 => /lib/x86_64-linux-gnu/libnuma.so.1 (0x0000771351ff2000) + librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x0000771386188000) + libgomp.so.1 => /lib/x86_64-linux-gnu/libgomp.so.1 (0x0000771317daa000) + libroctracer64.so.4 => /opt/rocm/lib/libroctracer64.so.4 (0x0000771312d91000) + librocroller.so.1 => /opt/rocm/lib/librocroller.so.1 (0x0000771281400000) + libhsa-runtime64.so.1 => /opt/rocm/lib/libhsa-runtime64.so.1 (0x0000771280e00000) + libamd_comgr.so.3 => /opt/rocm/lib/../lib/libamd_comgr.so.3 (0x0000771277400000) + libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x0000771351fd3000) + libdrm.so.2 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm.so.2 (0x000077134ea3d000) + libdrm_amdgpu.so.1 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm_amdgpu.so.1 (0x000077134ea2e000) + libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x000077134ea12000) + libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007712ce546000) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt new file mode 100644 index 000000000..5072ffcf4 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt @@ -0,0 +1,46 @@ + linux-vdso.so.1 (0x00007b570a556000) + libc10_hip.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10_hip.so (0x00007b570a3e8000) + libMIOpen.so.1 => /home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib/libMIOpen.so.1 (0x00007b56b5200000) + libhiprtc.so.7 => /opt/rocm/lib/libhiprtc.so.7 (0x00007b56f432d000) + libhipblas.so.3 => /opt/rocm/lib/libhipblas.so.3 (0x00007b56b5124000) + libhipfft.so.0 => /opt/rocm/lib/libhipfft.so.0 (0x00007b570a3cc000) + libhiprand.so.1 => /opt/rocm/lib/libhiprand.so.1 (0x00007b570a3c4000) + libhipsparse.so.4 => /opt/rocm/lib/libhipsparse.so.4 (0x00007b570a37f000) + libhipsolver.so.1 => /opt/rocm/lib/libhipsolver.so.1 (0x00007b56f42e7000) + librocsolver.so.0 => /opt/rocm/lib/librocsolver.so.0 (0x00007b5681400000) + libhipsparselt.so.0 => /opt/rocm/lib/libhipsparselt.so.0 (0x00007b5680e00000) + libaotriton_v2.so.0.11.1 => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libaotriton_v2.so.0.11.1 (0x00007b567da00000) + librccl.so.1 => /opt/rocm/lib/librccl.so.1 (0x00007b565b600000) + librocm_smi64.so.1 => /opt/rocm/lib/librocm_smi64.so.1 (0x00007b56b4fce000) + libc10.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10.so (0x00007b56812ea000) + libtorch_cpu.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libtorch_cpu.so (0x00007b5646c00000) + libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007b570a378000) + librocblas.so.5 => /opt/rocm/lib/librocblas.so.5 (0x00007b5643c00000) + libhipblaslt.so.1 => /opt/rocm/lib/libhipblaslt.so.1 (0x00007b5643600000) + libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7 (0x00007b5641c00000) + libmagma.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libmagma.so (0x00007b55fd400000) + libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007b55fd000000) + libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007b567d917000) + libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007b56b4fa0000) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007b55fcc00000) + /lib64/ld-linux-x86-64.so.2 (0x00007b570a558000) + libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007b567d85d000) + libamd_comgr.so.3 => /opt/rocm/lib/libamd_comgr.so.3 (0x00007b55f3200000) + librocm-core.so.1 => /opt/rocm/lib/librocm-core.so.1 (0x00007b570a36f000) + libroctx64.so.4 => /opt/rocm/lib/libroctx64.so.4 (0x00007b570a368000) + librocfft.so.0 => /opt/rocm/lib/librocfft.so.0 (0x00007b55f1800000) + librocrand.so.1 => /opt/rocm/lib/librocrand.so.1 (0x00007b55c5c00000) + librocsparse.so.1 => /opt/rocm/lib/librocsparse.so.1 (0x00007b55a8a00000) + libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007b56f42e2000) + liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007b56812b8000) + librocprofiler-register.so.0 => /opt/rocm/lib/librocprofiler-register.so.0 (0x00007b5641b71000) + libnuma.so.1 => /lib/x86_64-linux-gnu/libnuma.so.1 (0x00007b56f42d4000) + librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007b56f42cf000) + libgomp.so.1 => /lib/x86_64-linux-gnu/libgomp.so.1 (0x00007b5646baa000) + libroctracer64.so.4 => /opt/rocm/lib/libroctracer64.so.4 (0x00007b55fd391000) + librocroller.so.1 => /opt/rocm/lib/librocroller.so.1 (0x00007b55a6800000) + libhsa-runtime64.so.1 => /opt/rocm/lib/libhsa-runtime64.so.1 (0x00007b55a6200000) + libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007b5680de4000) + libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x00007b567d83e000) + libdrm.so.2 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm.so.2 (0x00007b567d825000) + libdrm_amdgpu.so.1 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm_amdgpu.so.1 (0x00007b56b4f8f000) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt new file mode 100644 index 000000000..3bd0c6183 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt @@ -0,0 +1,18 @@ +Package: miopen-hip +Architecture: amd64 +Conflicts: miopen-opencl +Depends: hip-runtime-amd, comgr, roctracer, rocblas, hipblaslt, rocm-core, rocrand, rocm-core +Priority: optional +Section: devel +Filename: pool/main/m/miopen-hip/miopen-hip_3.5.1.70204-93~24.04_amd64.deb +Size: 307969764 +SHA256: b5759989f8d95b367d83309f4d9da3c55c1e5868703aaede1647434df375b6c2 +SHA1: d86fc79231d175df7f833b022487e72d9a0d2a98 +MD5sum: 0109619cddbabe2347e757d157ea9540 +Description: AMD DNN Library +Description-md5: +Maintainer: MIOpen Maintainer +Recommends: miopen-hip-dev (>=3.5.1.70204) +Version: 3.5.1.70204-93~24.04 +Installed-Size: 2931663 + diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt new file mode 100644 index 000000000..b3490c4eb --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt @@ -0,0 +1,6 @@ +miopen-hip: + Installed: (none) + Candidate: 3.5.1.70204-93~24.04 + Version table: + 3.5.1.70204-93~24.04 600 + 600 https://repo.radeon.com/rocm/apt/7.2.4 noble/main amd64 Packages diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt new file mode 100644 index 000000000..0158e8dd3 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt @@ -0,0 +1,22 @@ + linux-vdso.so.1 (0x000076d7c8a0c000) + libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x000076d7c8941000) + libhiprtc.so.7 => /opt/rocm/lib/libhiprtc.so.7 (0x000076d78972d000) + libamd_comgr.so.3 => /opt/rocm/lib/libamd_comgr.so.3 (0x000076d77fc00000) + librocm-core.so.1 => /opt/rocm/lib/librocm-core.so.1 (0x000076d7c893c000) + librocblas.so.5 => /opt/rocm/lib/librocblas.so.5 (0x000076d77cc00000) + libhipblaslt.so.1 => /opt/rocm/lib/libhipblaslt.so.1 (0x000076d77c600000) + libroctx64.so.4 => /opt/rocm/lib/libroctx64.so.4 (0x000076d7c8935000) + libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7 (0x000076d77ac00000) + libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x000076d77a800000) + libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x000076d789644000) + libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x000076d7c8907000) + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000076d77a400000) + /lib64/ld-linux-x86-64.so.2 (0x000076d7c8a0e000) + libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x000076d7c88e9000) + librocroller.so.1 => /opt/rocm/lib/librocroller.so.1 (0x000076d778200000) + librocprofiler-register.so.0 => /opt/rocm/lib/librocprofiler-register.so.0 (0x000076d7895b5000) + libhsa-runtime64.so.1 => /opt/rocm/lib/libhsa-runtime64.so.1 (0x000076d777c00000) + libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x000076d789596000) + libdrm.so.2 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm.so.2 (0x000076d7c88ce000) + libdrm_amdgpu.so.1 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm_amdgpu.so.1 (0x000076d789587000) + libnuma.so.1 => /lib/x86_64-linux-gnu/libnuma.so.1 (0x000076d789579000) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log new file mode 100644 index 000000000..3f265bc25 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log @@ -0,0 +1,4 @@ +Get:1 https://repo.radeon.com/rocm/apt/7.2.4 noble/main amd64 miopen-hip amd64 3.5.1.70204-93~24.04 [308 MB] +Fetched 308 MB in 3s (90.3 MB/s) +verified miopen-hip_3.5.1.70204-93~24.04_amd64.deb b5759989f8d95b367d83309f4d9da3c55c1e5868703aaede1647434df375b6c2 +private library /home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib/libMIOpen.so.1.0.70204 sha256 bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html new file mode 100644 index 000000000..b5a01ebec --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html @@ -0,0 +1,140 @@ + +Index of /rocm/manylinux/rocm-rel-7.2.4/ + +

Release notes

+For information on available ROCm releases, please refer to the + +ROCm Release Notes

+For information on available Radeon Software for Linux releases, +please refer to + +Linux® Drivers for AMD Radeon™ and Radeon PRO™ Graphics.

+ +

Index of /rocm/manylinux/rocm-rel-7.2.4/


../
+apex-1.10.0+rocm7.2.4.git751f5dd5-cp310-cp310-l..> 22-May-2026 03:58            21035408
+apex-1.10.0+rocm7.2.4.git751f5dd5-cp311-cp311-l..> 22-May-2026 02:02            21035409
+apex-1.10.0+rocm7.2.4.git751f5dd5-cp312-cp312-l..> 22-May-2026 02:00            21035407
+apex-1.10.0+rocm7.2.4.git751f5dd5-cp313-cp313-l..> 22-May-2026 01:56            21035407
+apex-1.11.0+rocm7.2.4.gitc0f56f7e-cp312-cp312-l..> 17-Aug-2026 16:01            21043771
+apex-1.7.0+rocm7.2.4.git215398d0-cp310-cp310-li..> 22-May-2026 04:37            96426289
+apex-1.7.0+rocm7.2.4.git215398d0-cp311-cp311-li..> 22-May-2026 03:17            96889282
+apex-1.7.0+rocm7.2.4.git215398d0-cp312-cp312-li..> 22-May-2026 04:38            96929010
+apex-1.7.0+rocm7.2.4.git215398d0-cp313-cp313-li..> 22-May-2026 02:11            96929504
+apex-1.8.0+rocm7.2.4.gitb1302357-cp310-cp310-li..> 21-May-2026 22:00            21035400
+apex-1.8.0+rocm7.2.4.gitb1302357-cp311-cp311-li..> 21-May-2026 19:24            21035400
+apex-1.8.0+rocm7.2.4.gitb1302357-cp312-cp312-li..> 21-May-2026 19:33            21035399
+apex-1.8.0+rocm7.2.4.gitb1302357-cp313-cp313-li..> 21-May-2026 20:58            21035400
+apex-1.9.0+rocm7.2.4.git355db9b8-cp310-cp310-li..> 25-May-2026 01:28            21035401
+apex-1.9.0+rocm7.2.4.git355db9b8-cp311-cp311-li..> 21-May-2026 20:33            21035401
+apex-1.9.0+rocm7.2.4.git355db9b8-cp312-cp312-li..> 25-May-2026 01:19            21035399
+apex-1.9.0+rocm7.2.4.git355db9b8-cp313-cp313-li..> 25-May-2026 01:03            21035401
+jax_rocm7_pjrt-0.8.2+rocm7.2.4-py3-none-linux_x..> 21-May-2026 16:57           193640359
+jax_rocm7_pjrt-0.8.2+rocm7.2.4-py3-none-manylin..> 21-May-2026 16:57           193640343
+jax_rocm7_pjrt-0.8.2+rocm7.2.4-py3-none-manylin..> 21-May-2026 16:57           193640725
+jax_rocm7_plugin-0.8.2+rocm7.2.4-cp311-cp311-li..> 21-May-2026 16:57             7943490
+jax_rocm7_plugin-0.8.2+rocm7.2.4-cp311-cp311-ma..> 21-May-2026 16:57             7943500
+jax_rocm7_plugin-0.8.2+rocm7.2.4-cp311-cp311-ma..> 21-May-2026 16:57             7943776
+jax_rocm7_plugin-0.8.2+rocm7.2.4-cp312-cp312-li..> 21-May-2026 16:57             7938671
+jax_rocm7_plugin-0.8.2+rocm7.2.4-cp312-cp312-ma..> 21-May-2026 16:57             7938677
+jax_rocm7_plugin-0.8.2+rocm7.2.4-cp312-cp312-ma..> 21-May-2026 16:57             7938953
+jaxlib-0.8.2+rocm7.2.4-cp311-cp311-linux_x86_64..> 21-May-2026 16:57            90176250
+jaxlib-0.8.2+rocm7.2.4-cp311-cp311-manylinux_2_..> 21-May-2026 16:57            90176259
+jaxlib-0.8.2+rocm7.2.4-cp311-cp311-manylinux_2_..> 21-May-2026 16:57            90178532
+jaxlib-0.8.2+rocm7.2.4-cp312-cp312-linux_x86_64..> 21-May-2026 16:57            90187829
+jaxlib-0.8.2+rocm7.2.4-cp312-cp312-manylinux_2_..> 21-May-2026 16:57            90187837
+jaxlib-0.8.2+rocm7.2.4-cp312-cp312-manylinux_2_..> 21-May-2026 16:57            90190110
+onnxruntime_migraphx-1.23.2-cp310-cp310-manylin..> 21-May-2026 16:19            20597183
+onnxruntime_migraphx-1.23.2-cp310-cp310-manylin..> 21-May-2026 16:19           498983680
+onnxruntime_migraphx-1.23.2-cp312-cp312-manylin..> 21-May-2026 16:11            20599434
+onnxruntime_migraphx-1.23.2-cp312-cp312-manylin..> 21-May-2026 16:11           499060628
+tensorflow_rocm-2.18.1-cp310-cp310-manylinux_2_..> 21-May-2026 15:01           494043826
+tensorflow_rocm-2.18.1-cp312-cp312-manylinux_2_..> 21-May-2026 14:31           494346732
+tensorflow_rocm-2.19.1-cp310-cp310-manylinux_2_..> 21-May-2026 14:48           523401732
+tensorflow_rocm-2.19.1-cp312-cp312-manylinux_2_..> 21-May-2026 18:57           523705480
+tensorflow_rocm-2.20.0.dev0+selfbuilt-cp310-cp3..> 21-May-2026 14:16           500660116
+tensorflow_rocm-2.20.0.dev0+selfbuilt-cp312-cp3..> 21-May-2026 14:18           500942603
+tf_nightly_rocm-2.21.0.dev0+selfbuilt-cp310-cp3..> 21-May-2026 14:42           526713985
+tf_nightly_rocm-2.21.0.dev0+selfbuilt-cp312-cp3..> 21-May-2026 15:06           527116041
+torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp310-cp3..> 22-May-2026 03:58          1647327128
+torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp311-cp3..> 22-May-2026 02:02          1647356819
+torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp3..> 22-May-2026 02:00          1647409999
+torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp313-cp3..> 22-May-2026 01:56          1647417939
+torch-2.11.0+rocm7.2.4.lw.git5fbd98f3-cp312-cp3..> 17-Aug-2026 16:01          1661396411
+torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp310-cp31..> 22-May-2026 04:37          1157090225
+torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp311-cp31..> 22-May-2026 03:17          1157111687
+torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp312-cp31..> 22-May-2026 04:38          1156966596
+torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp313-cp31..> 22-May-2026 02:11          1156973421
+torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp39-cp39-..> 22-May-2026 02:22          1157085617
+torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp310-cp31..> 21-May-2026 22:00          1546165243
+torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp311-cp31..> 21-May-2026 19:24          1546186143
+torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp312-cp31..> 21-May-2026 19:33          1546039806
+torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp313-cp31..> 21-May-2026 20:58          1546046386
+torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp310-cp31..> 13-Aug-2026 19:25          1546155487
+torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp311-cp31..> 13-Aug-2026 19:25          1546176166
+torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp312-cp31..> 13-Aug-2026 19:28          1546031167
+torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp313-cp31..> 13-Aug-2026 19:20          1546038149
+torch-2.9.1+rocm7.2.4.lw.git39497456-cp310-cp31..> 25-May-2026 01:28          1650524563
+torch-2.9.1+rocm7.2.4.lw.git39497456-cp311-cp31..> 21-May-2026 20:33          1650547330
+torch-2.9.1+rocm7.2.4.lw.git39497456-cp312-cp31..> 25-May-2026 01:19          1650466828
+torch-2.9.1+rocm7.2.4.lw.git39497456-cp313-cp31..> 25-May-2026 01:03          1650467274
+torchaudio-2.10.0+rocm7.2.4.git5047768f-cp310-c..> 22-May-2026 03:58              409174
+torchaudio-2.10.0+rocm7.2.4.git5047768f-cp311-c..> 22-May-2026 02:02              410596
+torchaudio-2.10.0+rocm7.2.4.git5047768f-cp312-c..> 22-May-2026 02:01              410816
+torchaudio-2.10.0+rocm7.2.4.git5047768f-cp313-c..> 22-May-2026 01:56              411175
+torchaudio-2.11.0+rocm7.2.4.git143129b5-cp312-c..> 17-Aug-2026 16:02             1542328
+torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp310-cp..> 22-May-2026 04:37             1795413
+torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp311-cp..> 22-May-2026 03:17             1802570
+torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp312-cp..> 22-May-2026 04:39             1802292
+torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp313-cp..> 22-May-2026 02:11             1802009
+torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp310-cp..> 21-May-2026 22:01             1806058
+torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp311-cp..> 21-May-2026 19:24             1813761
+torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp312-cp..> 21-May-2026 19:33             1813780
+torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp313-cp..> 21-May-2026 20:58             1813690
+torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp310-cp..> 25-May-2026 01:28              487390
+torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp311-cp..> 21-May-2026 20:33              488959
+torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp312-cp..> 25-May-2026 01:20              488600
+torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp313-cp..> 25-May-2026 01:04              488428
+torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp310-..> 22-May-2026 04:37             3039072
+torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp311-..> 22-May-2026 03:17             3040859
+torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp312-..> 22-May-2026 04:39             3042060
+torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp313-..> 22-May-2026 02:11             3042184
+torchvision-0.23.0+rocm7.2.4.git824e8c87-cp310-..> 21-May-2026 22:01             2929689
+torchvision-0.23.0+rocm7.2.4.git824e8c87-cp311-..> 21-May-2026 19:24             2931331
+torchvision-0.23.0+rocm7.2.4.git824e8c87-cp312-..> 21-May-2026 19:33             2933024
+torchvision-0.23.0+rocm7.2.4.git824e8c87-cp313-..> 21-May-2026 20:58             2932920
+torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp310-..> 25-May-2026 01:28             2942796
+torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp311-..> 21-May-2026 20:33             2944670
+torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp312-..> 25-May-2026 01:20             2946520
+torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp313-..> 25-May-2026 01:04             2946149
+torchvision-0.25.0+rocm7.2.4.git82df5f59-cp310-..> 22-May-2026 03:58             2950918
+torchvision-0.25.0+rocm7.2.4.git82df5f59-cp311-..> 22-May-2026 02:02             2953900
+torchvision-0.25.0+rocm7.2.4.git82df5f59-cp312-..> 22-May-2026 02:01             2954534
+torchvision-0.25.0+rocm7.2.4.git82df5f59-cp313-..> 22-May-2026 01:56             2954242
+torchvision-0.26.0+rocm7.2.4.git3d50b215-cp312-..> 17-Aug-2026 16:02             2938387
+transformer_engine-2.6.0-py3-none-any.whl          25-May-2026 13:46              606630
+transformer_engine_jax-2.6.0.tar.gz                25-May-2026 13:46              404582
+transformer_engine_rocm-2.6.0-py3-none-manylinu..> 25-May-2026 13:45           777770071
+transformer_engine_torch-2.6.0.tar.gz              25-May-2026 13:46              430209
+triton-3.3.1+rocm7.2.4.git28a7371e-cp310-cp310-..> 22-May-2026 04:37           270082005
+triton-3.3.1+rocm7.2.4.git28a7371e-cp311-cp311-..> 22-May-2026 03:17           270179300
+triton-3.3.1+rocm7.2.4.git28a7371e-cp312-cp312-..> 22-May-2026 04:39           270154950
+triton-3.3.1+rocm7.2.4.git28a7371e-cp313-cp313-..> 22-May-2026 02:11           270164456
+triton-3.3.1+rocm7.2.4.git28a7371e-cp39-cp39-li..> 22-May-2026 02:22           270084490
+triton-3.4.0+rocm7.2.4.git0cace8d2-cp310-cp310-..> 21-May-2026 22:01           268668362
+triton-3.4.0+rocm7.2.4.git0cace8d2-cp311-cp311-..> 21-May-2026 19:24           268763459
+triton-3.4.0+rocm7.2.4.git0cace8d2-cp312-cp312-..> 21-May-2026 19:33           268744128
+triton-3.4.0+rocm7.2.4.git0cace8d2-cp313-cp313-..> 21-May-2026 20:58           268746983
+triton-3.5.1+rocm7.2.4.gita272dfa8-cp310-cp310-..> 25-May-2026 01:28           284355039
+triton-3.5.1+rocm7.2.4.gita272dfa8-cp311-cp311-..> 21-May-2026 20:33           284445474
+triton-3.5.1+rocm7.2.4.gita272dfa8-cp312-cp312-..> 25-May-2026 01:20           284425085
+triton-3.5.1+rocm7.2.4.gita272dfa8-cp313-cp313-..> 25-May-2026 01:04           284438750
+triton-3.6.0+rocm7.2.4.git4ed88892-cp310-cp310-..> 22-May-2026 03:58           298359274
+triton-3.6.0+rocm7.2.4.git4ed88892-cp311-cp311-..> 22-May-2026 02:02           298493223
+triton-3.6.0+rocm7.2.4.git4ed88892-cp312-cp312-..> 22-May-2026 02:01           298453497
+triton-3.6.0+rocm7.2.4.git4ed88892-cp313-cp313-..> 22-May-2026 01:56           298465376
+triton-3.7.0+rocm7.2.4.gitb4e20bbe-cp312-cp312-..> 17-Aug-2026 16:02           308035330
+xformers-0.0.32+db55a2f5.d20260521-cp39-abi3-li..> 21-May-2026 22:12             9809687
+xformers-0.0.32+db55a2f5.d20260522-cp39-abi3-li..> 22-May-2026 04:52             9688226
+xformers-0.0.32+db55a2f5.d20260525-cp39-abi3-li..> 25-May-2026 01:32             9996328
+

+ diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit new file mode 100644 index 000000000..897bdc820 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit @@ -0,0 +1 @@ +139 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt new file mode 100644 index 000000000..ea7f8b472 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt @@ -0,0 +1 @@ +/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log new file mode 100644 index 000000000..f543c5988 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log @@ -0,0 +1,5 @@ +Traceback (most recent call last): + File "/home/marcelorm/ds4v-work/source-rocm210-reference/freeze-source-reference.py", line 27, in + assert digest(path) == expected + ^^^^^^^^^^^^^^^^^^^^^^^^ +AssertionError diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log new file mode 100644 index 000000000..e055a7eea --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log @@ -0,0 +1,8 @@ +{ + "canonical_directory": "/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference", + "manifest_sha256": "677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86", + "freeze_manifest": "/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json", + "freeze_manifest_sha256": "8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0", + "status": "SOURCE_REFERENCE_STABILITY_PASS", + "frozen_utc": "2026-09-05T01:40:51.838089+00:00" +} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log new file mode 100644 index 000000000..cd53451da --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log @@ -0,0 +1,68 @@ +{ + "command": [ + "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python", + "-I", + "-B", + "/home/marcelorm/ds4v-work/source-rocm210-reference/source-forward-radeon-name.py", + "--device", + "hip", + "--image", + "carrots", + "--gpu-window-released", + "--rocr-visible-device", + "GPU-93a97448a27aeff3", + "--source", + "/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored", + "--reference", + "/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference", + "--output", + "/home/marcelorm/ds4v-work/source-rocm210-reference/source-carrots-first" + ], + "environment": { + "HOME": "/home/marcelorm", + "PATH": "/usr/bin:/bin", + "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "2", + "MKL_NUM_THREADS": "2", + "OPENBLAS_NUM_THREADS": "2", + "LD_LIBRARY_PATH": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", + "TORCH_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/torch-cache", + "TRITON_CACHE_DIR": "/home/marcelorm/ds4v-work/source-rocm210-reference/triton-cache", + "XDG_CACHE_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/cache" + }, + "timeout_seconds": 300, + "before": { + "operator": { + "MainPID": "0", + "ActiveState": "inactive" + }, + "ports": [], + "kfd_processes": [], + "host_available_bytes": 36105768960, + "discrete_free_vram_bytes": 21430087680, + "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" + }, + "script_sha256": "cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c", + "miopen_sha256": "bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd", + "wrapper_sha256": "b4b2d7f69a7d4c9aa50f18beffb75da5076192f758b7c701ed64fad2bc8bc6cd", + "gpu_release": "parent explicitly released fixed source stability lane", + "prospective_command_equivalence": "same frozen Python argv/environment; os.wait4 supplies timing in place of GNU time", + "python_pid": 3367102, + "exit": 0, + "elapsed_seconds": 5.763090842985548, + "user_seconds": 4.771837, + "system_seconds": 0.783909, + "max_rss_kib": 2517764, + "timed_out": false, + "after": { + "operator": { + "MainPID": "0", + "ActiveState": "inactive" + }, + "ports": [], + "kfd_processes": [], + "host_available_bytes": 36018761728, + "discrete_free_vram_bytes": 21430087680, + "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" + } +} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log new file mode 100644 index 000000000..367eee577 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log @@ -0,0 +1,68 @@ +{ + "command": [ + "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python", + "-I", + "-B", + "/home/marcelorm/ds4v-work/source-rocm210-reference/source-forward-radeon-name.py", + "--device", + "hip", + "--image", + "carrots", + "--gpu-window-released", + "--rocr-visible-device", + "GPU-93a97448a27aeff3", + "--source", + "/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored", + "--reference", + "/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference", + "--output", + "/home/marcelorm/ds4v-work/source-rocm210-reference/source-carrots-repeat" + ], + "environment": { + "HOME": "/home/marcelorm", + "PATH": "/usr/bin:/bin", + "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "2", + "MKL_NUM_THREADS": "2", + "OPENBLAS_NUM_THREADS": "2", + "LD_LIBRARY_PATH": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", + "TORCH_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/torch-cache", + "TRITON_CACHE_DIR": "/home/marcelorm/ds4v-work/source-rocm210-reference/triton-cache", + "XDG_CACHE_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/cache" + }, + "timeout_seconds": 300, + "before": { + "operator": { + "MainPID": "0", + "ActiveState": "inactive" + }, + "ports": [], + "kfd_processes": [], + "host_available_bytes": 36015874048, + "discrete_free_vram_bytes": 21430087680, + "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" + }, + "script_sha256": "cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c", + "miopen_sha256": "bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd", + "wrapper_sha256": "b4b2d7f69a7d4c9aa50f18beffb75da5076192f758b7c701ed64fad2bc8bc6cd", + "gpu_release": "parent explicitly released fixed source stability lane", + "prospective_command_equivalence": "same frozen Python argv/environment; os.wait4 supplies timing in place of GNU time", + "python_pid": 3367383, + "exit": 0, + "elapsed_seconds": 5.7501774380216375, + "user_seconds": 4.690938, + "system_seconds": 0.736495, + "max_rss_kib": 2517508, + "timed_out": false, + "after": { + "operator": { + "MainPID": "0", + "ActiveState": "inactive" + }, + "ports": [], + "kfd_processes": [], + "host_available_bytes": 35733037056, + "discrete_free_vram_bytes": 21430087680, + "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" + } +} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log new file mode 100644 index 000000000..0bb00015e --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log @@ -0,0 +1,68 @@ +{ + "command": [ + "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python", + "-I", + "-B", + "/home/marcelorm/ds4v-work/source-rocm210-reference/source-forward-radeon-name.py", + "--device", + "hip", + "--image", + "corn", + "--gpu-window-released", + "--rocr-visible-device", + "GPU-93a97448a27aeff3", + "--source", + "/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored", + "--reference", + "/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference", + "--output", + "/home/marcelorm/ds4v-work/source-rocm210-reference/source-corn-repeat" + ], + "environment": { + "HOME": "/home/marcelorm", + "PATH": "/usr/bin:/bin", + "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "2", + "MKL_NUM_THREADS": "2", + "OPENBLAS_NUM_THREADS": "2", + "LD_LIBRARY_PATH": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", + "TORCH_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/torch-cache", + "TRITON_CACHE_DIR": "/home/marcelorm/ds4v-work/source-rocm210-reference/triton-cache", + "XDG_CACHE_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/cache" + }, + "timeout_seconds": 300, + "before": { + "operator": { + "MainPID": "0", + "ActiveState": "inactive" + }, + "ports": [], + "kfd_processes": [], + "host_available_bytes": 36105142272, + "discrete_free_vram_bytes": 21430087680, + "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" + }, + "script_sha256": "cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c", + "miopen_sha256": "bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd", + "wrapper_sha256": "b4b2d7f69a7d4c9aa50f18beffb75da5076192f758b7c701ed64fad2bc8bc6cd", + "gpu_release": "parent explicitly released fixed source stability lane", + "prospective_command_equivalence": "same frozen Python argv/environment; os.wait4 supplies timing in place of GNU time", + "python_pid": 3366985, + "exit": 0, + "elapsed_seconds": 5.234832148998976, + "user_seconds": 4.2197189999999996, + "system_seconds": 0.7277429999999999, + "max_rss_kib": 2503140, + "timed_out": false, + "after": { + "operator": { + "MainPID": "0", + "ActiveState": "inactive" + }, + "ports": [], + "kfd_processes": [], + "host_available_bytes": 36105420800, + "discrete_free_vram_bytes": 21430087680, + "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" + } +} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA new file mode 100644 index 000000000..c0957e954 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA @@ -0,0 +1,624 @@ +Metadata-Version: 2.4 +Name: torch +Version: 2.10.0+rocm7.2.4.lw.git3d3aa833 +Summary: Tensors and Dynamic neural networks in Python with strong GPU acceleration +Author-email: PyTorch Team +License: BSD-3-Clause +Project-URL: Homepage, https://pytorch.org +Project-URL: Repository, https://github.com/pytorch/pytorch +Project-URL: Documentation, https://pytorch.org/docs +Project-URL: Issue Tracker, https://github.com/pytorch/pytorch/issues +Project-URL: Forum, https://discuss.pytorch.org +Keywords: pytorch,machine learning +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Education +Classifier: Intended Audience :: Science/Research +Classifier: Topic :: Scientific/Engineering +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence +Classifier: Topic :: Software Development +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Programming Language :: C++ +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +License-File: NOTICE +Requires-Dist: filelock +Requires-Dist: typing-extensions>=4.10.0 +Requires-Dist: setuptools; python_version >= "3.12" +Requires-Dist: sympy>=1.13.3 +Requires-Dist: networkx>=2.5.1 +Requires-Dist: jinja2 +Requires-Dist: fsspec>=0.8.5 +Requires-Dist: triton==3.6.0+rocm7.2.4.git4ed88892; platform_system == "Linux" and platform_machine == "x86_64" +Provides-Extra: optree +Requires-Dist: optree>=0.13.0; extra == "optree" +Provides-Extra: opt-einsum +Requires-Dist: opt-einsum>=3.3; extra == "opt-einsum" +Provides-Extra: pyyaml +Requires-Dist: pyyaml; extra == "pyyaml" +Dynamic: license-file +Dynamic: requires-dist + +![PyTorch Logo](https://github.com/pytorch/pytorch/raw/main/docs/source/_static/img/pytorch-logo-dark.png) + +-------------------------------------------------------------------------------- + +PyTorch is a Python package that provides two high-level features: +- Tensor computation (like NumPy) with strong GPU acceleration +- Deep neural networks built on a tape-based autograd system + +You can reuse your favorite Python packages such as NumPy, SciPy, and Cython to extend PyTorch when needed. + +Our trunk health (Continuous Integration signals) can be found at [hud.pytorch.org](https://hud.pytorch.org/ci/pytorch/pytorch/main). + + + +- [More About PyTorch](#more-about-pytorch) + - [A GPU-Ready Tensor Library](#a-gpu-ready-tensor-library) + - [Dynamic Neural Networks: Tape-Based Autograd](#dynamic-neural-networks-tape-based-autograd) + - [Python First](#python-first) + - [Imperative Experiences](#imperative-experiences) + - [Fast and Lean](#fast-and-lean) + - [Extensions Without Pain](#extensions-without-pain) +- [Installation](#installation) + - [Binaries](#binaries) + - [NVIDIA Jetson Platforms](#nvidia-jetson-platforms) + - [From Source](#from-source) + - [Prerequisites](#prerequisites) + - [NVIDIA CUDA Support](#nvidia-cuda-support) + - [AMD ROCm Support](#amd-rocm-support) + - [Intel GPU Support](#intel-gpu-support) + - [Get the PyTorch Source](#get-the-pytorch-source) + - [Install Dependencies](#install-dependencies) + - [Install PyTorch](#install-pytorch) + - [Adjust Build Options (Optional)](#adjust-build-options-optional) + - [Docker Image](#docker-image) + - [Using pre-built images](#using-pre-built-images) + - [Building the image yourself](#building-the-image-yourself) + - [Building the Documentation](#building-the-documentation) + - [Building a PDF](#building-a-pdf) + - [Previous Versions](#previous-versions) +- [Getting Started](#getting-started) +- [Resources](#resources) +- [Communication](#communication) +- [Releases and Contributing](#releases-and-contributing) +- [The Team](#the-team) +- [License](#license) + + + +## More About PyTorch + +[Learn the basics of PyTorch](https://pytorch.org/tutorials/beginner/basics/intro.html) + +At a granular level, PyTorch is a library that consists of the following components: + +| Component | Description | +| ---- | --- | +| [**torch**](https://pytorch.org/docs/stable/torch.html) | A Tensor library like NumPy, with strong GPU support | +| [**torch.autograd**](https://pytorch.org/docs/stable/autograd.html) | A tape-based automatic differentiation library that supports all differentiable Tensor operations in torch | +| [**torch.jit**](https://pytorch.org/docs/stable/jit.html) | A compilation stack (TorchScript) to create serializable and optimizable models from PyTorch code | +| [**torch.nn**](https://pytorch.org/docs/stable/nn.html) | A neural networks library deeply integrated with autograd designed for maximum flexibility | +| [**torch.multiprocessing**](https://pytorch.org/docs/stable/multiprocessing.html) | Python multiprocessing, but with magical memory sharing of torch Tensors across processes. Useful for data loading and Hogwild training | +| [**torch.utils**](https://pytorch.org/docs/stable/data.html) | DataLoader and other utility functions for convenience | + +Usually, PyTorch is used either as: + +- A replacement for NumPy to use the power of GPUs. +- A deep learning research platform that provides maximum flexibility and speed. + +Elaborating Further: + +### A GPU-Ready Tensor Library + +If you use NumPy, then you have used Tensors (a.k.a. ndarray). + +![Tensor illustration](https://github.com/pytorch/pytorch/raw/main/docs/source/_static/img/tensor_illustration.png) + +PyTorch provides Tensors that can live either on the CPU or the GPU and accelerates the +computation by a huge amount. + +We provide a wide variety of tensor routines to accelerate and fit your scientific computation needs +such as slicing, indexing, mathematical operations, linear algebra, reductions. +And they are fast! + +### Dynamic Neural Networks: Tape-Based Autograd + +PyTorch has a unique way of building neural networks: using and replaying a tape recorder. + +Most frameworks such as TensorFlow, Theano, Caffe, and CNTK have a static view of the world. +One has to build a neural network and reuse the same structure again and again. +Changing the way the network behaves means that one has to start from scratch. + +With PyTorch, we use a technique called reverse-mode auto-differentiation, which allows you to +change the way your network behaves arbitrarily with zero lag or overhead. Our inspiration comes +from several research papers on this topic, as well as current and past work such as +[torch-autograd](https://github.com/twitter/torch-autograd), +[autograd](https://github.com/HIPS/autograd), +[Chainer](https://chainer.org), etc. + +While this technique is not unique to PyTorch, it's one of the fastest implementations of it to date. +You get the best of speed and flexibility for your crazy research. + +![Dynamic graph](https://github.com/pytorch/pytorch/raw/main/docs/source/_static/img/dynamic_graph.gif) + +### Python First + +PyTorch is not a Python binding into a monolithic C++ framework. +It is built to be deeply integrated into Python. +You can use it naturally like you would use [NumPy](https://www.numpy.org/) / [SciPy](https://www.scipy.org/) / [scikit-learn](https://scikit-learn.org) etc. +You can write your new neural network layers in Python itself, using your favorite libraries +and use packages such as [Cython](https://cython.org/) and [Numba](http://numba.pydata.org/). +Our goal is to not reinvent the wheel where appropriate. + +### Imperative Experiences + +PyTorch is designed to be intuitive, linear in thought, and easy to use. +When you execute a line of code, it gets executed. There isn't an asynchronous view of the world. +When you drop into a debugger or receive error messages and stack traces, understanding them is straightforward. +The stack trace points to exactly where your code was defined. +We hope you never spend hours debugging your code because of bad stack traces or asynchronous and opaque execution engines. + +### Fast and Lean + +PyTorch has minimal framework overhead. We integrate acceleration libraries +such as [Intel MKL](https://software.intel.com/mkl) and NVIDIA ([cuDNN](https://developer.nvidia.com/cudnn), [NCCL](https://developer.nvidia.com/nccl)) to maximize speed. +At the core, its CPU and GPU Tensor and neural network backends +are mature and have been tested for years. + +Hence, PyTorch is quite fast — whether you run small or large neural networks. + +The memory usage in PyTorch is extremely efficient compared to Torch or some of the alternatives. +We've written custom memory allocators for the GPU to make sure that +your deep learning models are maximally memory efficient. +This enables you to train bigger deep learning models than before. + +### Extensions Without Pain + +Writing new neural network modules, or interfacing with PyTorch's Tensor API was designed to be straightforward +and with minimal abstractions. + +You can write new neural network layers in Python using the torch API +[or your favorite NumPy-based libraries such as SciPy](https://pytorch.org/tutorials/advanced/numpy_extensions_tutorial.html). + +If you want to write your layers in C/C++, we provide a convenient extension API that is efficient and with minimal boilerplate. +No wrapper code needs to be written. You can see [a tutorial here](https://pytorch.org/tutorials/advanced/cpp_extension.html) and [an example here](https://github.com/pytorch/extension-cpp). + + +## Installation + +### Binaries +Commands to install binaries via Conda or pip wheels are on our website: [https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/) + + +#### NVIDIA Jetson Platforms + +Python wheels for NVIDIA's Jetson Nano, Jetson TX1/TX2, Jetson Xavier NX/AGX, and Jetson AGX Orin are provided [here](https://forums.developer.nvidia.com/t/pytorch-for-jetson-version-1-10-now-available/72048) and the L4T container is published [here](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/l4t-pytorch) + +They require JetPack 4.2 and above, and [@dusty-nv](https://github.com/dusty-nv) and [@ptrblck](https://github.com/ptrblck) are maintaining them. + + +### From Source + +#### Prerequisites +If you are installing from source, you will need: +- Python 3.10 or later +- A compiler that fully supports C++17, such as clang or gcc (gcc 9.4.0 or newer is required, on Linux) +- Visual Studio or Visual Studio Build Tool (Windows only) + +\* PyTorch CI uses Visual C++ BuildTools, which come with Visual Studio Enterprise, +Professional, or Community Editions. You can also install the build tools from +https://visualstudio.microsoft.com/visual-cpp-build-tools/. The build tools *do not* +come with Visual Studio Code by default. + +An example of environment setup is shown below: + +* Linux: + +```bash +$ source /bin/activate +$ conda create -y -n +$ conda activate +``` + +* Windows: + +```bash +$ source \Scripts\activate.bat +$ conda create -y -n +$ conda activate +$ call "C:\Program Files\Microsoft Visual Studio\\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 +``` + +A conda environment is not required. You can also do a PyTorch build in a +standard virtual environment, e.g., created with tools like `uv`, provided +your system has installed all the necessary dependencies unavailable as pip +packages (e.g., CUDA, MKL.) + +##### NVIDIA CUDA Support +If you want to compile with CUDA support, [select a supported version of CUDA from our support matrix](https://pytorch.org/get-started/locally/), then install the following: +- [NVIDIA CUDA](https://developer.nvidia.com/cuda-downloads) +- [NVIDIA cuDNN](https://developer.nvidia.com/cudnn) v8.5 or above +- [Compiler](https://gist.github.com/ax3l/9489132) compatible with CUDA + +Note: You could refer to the [cuDNN Support Matrix](https://docs.nvidia.com/deeplearning/cudnn/backend/latest/reference/support-matrix.html) for cuDNN versions with the various supported CUDA, CUDA driver, and NVIDIA hardware. + +If you want to disable CUDA support, export the environment variable `USE_CUDA=0`. +Other potentially useful environment variables may be found in `setup.py`. If +CUDA is installed in a non-standard location, set PATH so that the nvcc you +want to use can be found (e.g., `export PATH=/usr/local/cuda-12.8/bin:$PATH`). + +If you are building for NVIDIA's Jetson platforms (Jetson Nano, TX1, TX2, AGX Xavier), Instructions to install PyTorch for Jetson Nano are [available here](https://devtalk.nvidia.com/default/topic/1049071/jetson-nano/pytorch-for-jetson-nano/) + +##### AMD ROCm Support +If you want to compile with ROCm support, install +- [AMD ROCm](https://rocm.docs.amd.com/en/latest/deploy/linux/quick_start.html) 4.0 and above installation +- ROCm is currently supported only for Linux systems. + +By default the build system expects ROCm to be installed in `/opt/rocm`. If ROCm is installed in a different directory, the `ROCM_PATH` environment variable must be set to the ROCm installation directory. The build system automatically detects the AMD GPU architecture. Optionally, the AMD GPU architecture can be explicitly set with the `PYTORCH_ROCM_ARCH` environment variable [AMD GPU architecture](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/reference/system-requirements.html#supported-gpus) + +If you want to disable ROCm support, export the environment variable `USE_ROCM=0`. +Other potentially useful environment variables may be found in `setup.py`. + +##### Intel GPU Support +If you want to compile with Intel GPU support, follow these +- [PyTorch Prerequisites for Intel GPUs](https://www.intel.com/content/www/us/en/developer/articles/tool/pytorch-prerequisites-for-intel-gpus.html) instructions. +- Intel GPU is supported for Linux and Windows. + +If you want to disable Intel GPU support, export the environment variable `USE_XPU=0`. +Other potentially useful environment variables may be found in `setup.py`. + +#### Get the PyTorch Source + +```bash +git clone https://github.com/pytorch/pytorch +cd pytorch +# if you are updating an existing checkout +git submodule sync +git submodule update --init --recursive +``` + +#### Install Dependencies + +**Common** + +```bash +# Run this command from the PyTorch directory after cloning the source code using the “Get the PyTorch Source“ section above +pip install --group dev +``` + +**On Linux** + +```bash +pip install mkl-static mkl-include +# CUDA only: Add LAPACK support for the GPU if needed +# magma installation: run with active conda environment. specify CUDA version to install +.ci/docker/common/install_magma_conda.sh 12.4 + +# (optional) If using torch.compile with inductor/triton, install the matching version of triton +# Run from the pytorch directory after cloning +# For Intel GPU support, please explicitly `export USE_XPU=1` before running command. +make triton +``` + +**On MacOS** + +```bash +# Add this package on intel x86 processor machines only +pip install mkl-static mkl-include +# Add these packages if torch.distributed is needed +conda install pkg-config libuv +``` + +**On Windows** + +```bash +pip install mkl-static mkl-include +# Add these packages if torch.distributed is needed. +# Distributed package support on Windows is a prototype feature and is subject to changes. +conda install -c conda-forge libuv=1.51 +``` + +#### Install PyTorch + +**On Linux** + +If you're compiling for AMD ROCm then first run this command: + +```bash +# Only run this if you're compiling for ROCm +python tools/amd_build/build_amd.py +``` + +Install PyTorch + +```bash +# the CMake prefix for conda environment +export CMAKE_PREFIX_PATH="${CONDA_PREFIX:-'$(dirname $(which conda))/../'}:${CMAKE_PREFIX_PATH}" +python -m pip install --no-build-isolation -v -e . + +# the CMake prefix for non-conda environment, e.g. Python venv +# call following after activating the venv +export CMAKE_PREFIX_PATH="${VIRTUAL_ENV}:${CMAKE_PREFIX_PATH}" +``` + +**On macOS** + +```bash +python -m pip install --no-build-isolation -v -e . +``` + +**On Windows** + +If you want to build legacy python code, please refer to [Building on legacy code and CUDA](https://github.com/pytorch/pytorch/blob/main/CONTRIBUTING.md#building-on-legacy-code-and-cuda) + +**CPU-only builds** + +In this mode PyTorch computations will run on your CPU, not your GPU. + +```cmd +python -m pip install --no-build-isolation -v -e . +``` + +Note on OpenMP: The desired OpenMP implementation is Intel OpenMP (iomp). In order to link against iomp, you'll need to manually download the library and set up the building environment by tweaking `CMAKE_INCLUDE_PATH` and `LIB`. The instruction [here](https://github.com/pytorch/pytorch/blob/main/docs/source/notes/windows.rst#building-from-source) is an example for setting up both MKL and Intel OpenMP. Without these configurations for CMake, Microsoft Visual C OpenMP runtime (vcomp) will be used. + +**CUDA based build** + +In this mode PyTorch computations will leverage your GPU via CUDA for faster number crunching + +[NVTX](https://docs.nvidia.com/gameworks/content/gameworkslibrary/nvtx/nvidia_tools_extension_library_nvtx.htm) is needed to build Pytorch with CUDA. +NVTX is a part of CUDA distributive, where it is called "Nsight Compute". To install it onto an already installed CUDA run CUDA installation once again and check the corresponding checkbox. +Make sure that CUDA with Nsight Compute is installed after Visual Studio. + +Currently, VS 2017 / 2019, and Ninja are supported as the generator of CMake. If `ninja.exe` is detected in `PATH`, then Ninja will be used as the default generator, otherwise, it will use VS 2017 / 2019. +
If Ninja is selected as the generator, the latest MSVC will get selected as the underlying toolchain. + +Additional libraries such as +[Magma](https://developer.nvidia.com/magma), [oneDNN, a.k.a. MKLDNN or DNNL](https://github.com/oneapi-src/oneDNN), and [Sccache](https://github.com/mozilla/sccache) are often needed. Please refer to the [installation-helper](https://github.com/pytorch/pytorch/tree/main/.ci/pytorch/win-test-helpers/installation-helpers) to install them. + +You can refer to the [build_pytorch.bat](https://github.com/pytorch/pytorch/blob/main/.ci/pytorch/win-test-helpers/build_pytorch.bat) script for some other environment variables configurations + +```cmd +cmd + +:: Set the environment variables after you have downloaded and unzipped the mkl package, +:: else CMake would throw an error as `Could NOT find OpenMP`. +set CMAKE_INCLUDE_PATH={Your directory}\mkl\include +set LIB={Your directory}\mkl\lib;%LIB% + +:: Read the content in the previous section carefully before you proceed. +:: [Optional] If you want to override the underlying toolset used by Ninja and Visual Studio with CUDA, please run the following script block. +:: "Visual Studio 2019 Developer Command Prompt" will be run automatically. +:: Make sure you have CMake >= 3.12 before you do this when you use the Visual Studio generator. +set CMAKE_GENERATOR_TOOLSET_VERSION=14.27 +set DISTUTILS_USE_SDK=1 +for /f "usebackq tokens=*" %i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -version [15^,17^) -products * -latest -property installationPath`) do call "%i\VC\Auxiliary\Build\vcvarsall.bat" x64 -vcvars_ver=%CMAKE_GENERATOR_TOOLSET_VERSION% + +:: [Optional] If you want to override the CUDA host compiler +set CUDAHOSTCXX=C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\bin\HostX64\x64\cl.exe + +python -m pip install --no-build-isolation -v -e . +``` + +**Intel GPU builds** + +In this mode PyTorch with Intel GPU support will be built. + +Please make sure [the common prerequisites](#prerequisites) as well as [the prerequisites for Intel GPU](#intel-gpu-support) are properly installed and the environment variables are configured prior to starting the build. For build tool support, `Visual Studio 2022` is required. + +Then PyTorch can be built with the command: + +```cmd +:: CMD Commands: +:: Set the CMAKE_PREFIX_PATH to help find corresponding packages +:: %CONDA_PREFIX% only works after `conda activate custom_env` + +if defined CMAKE_PREFIX_PATH ( + set "CMAKE_PREFIX_PATH=%CONDA_PREFIX%\Library;%CMAKE_PREFIX_PATH%" +) else ( + set "CMAKE_PREFIX_PATH=%CONDA_PREFIX%\Library" +) + +python -m pip install --no-build-isolation -v -e . +``` + +##### Adjust Build Options (Optional) + +You can adjust the configuration of cmake variables optionally (without building first), by doing +the following. For example, adjusting the pre-detected directories for CuDNN or BLAS can be done +with such a step. + +On Linux + +```bash +export CMAKE_PREFIX_PATH="${CONDA_PREFIX:-'$(dirname $(which conda))/../'}:${CMAKE_PREFIX_PATH}" +CMAKE_ONLY=1 python setup.py build +ccmake build # or cmake-gui build +``` + +On macOS + +```bash +export CMAKE_PREFIX_PATH="${CONDA_PREFIX:-'$(dirname $(which conda))/../'}:${CMAKE_PREFIX_PATH}" +MACOSX_DEPLOYMENT_TARGET=11.0 CMAKE_ONLY=1 python setup.py build +ccmake build # or cmake-gui build +``` + +### Docker Image + +#### Using pre-built images + +You can also pull a pre-built docker image from Docker Hub and run with docker v19.03+ + +```bash +docker run --gpus all --rm -ti --ipc=host pytorch/pytorch:latest +``` + +Please note that PyTorch uses shared memory to share data between processes, so if torch multiprocessing is used (e.g. +for multithreaded data loaders) the default shared memory segment size that container runs with is not enough, and you +should increase shared memory size either with `--ipc=host` or `--shm-size` command line options to `nvidia-docker run`. + +#### Building the image yourself + +**NOTE:** Must be built with a docker version > 18.06 + +The `Dockerfile` is supplied to build images with CUDA 11.1 support and cuDNN v8. +You can pass `PYTHON_VERSION=x.y` make variable to specify which Python version is to be used by Miniconda, or leave it +unset to use the default. + +```bash +make -f docker.Makefile +# images are tagged as docker.io/${your_docker_username}/pytorch +``` + +You can also pass the `CMAKE_VARS="..."` environment variable to specify additional CMake variables to be passed to CMake during the build. +See [setup.py](./setup.py) for the list of available variables. + +```bash +make -f docker.Makefile +``` + +### Building the Documentation + +To build documentation in various formats, you will need [Sphinx](http://www.sphinx-doc.org) +and the pytorch_sphinx_theme2. + +Before you build the documentation locally, ensure `torch` is +installed in your environment. For small fixes, you can install the +nightly version as described in [Getting Started](https://pytorch.org/get-started/locally/). + +For more complex fixes, such as adding a new module and docstrings for +the new module, you might need to install torch [from source](#from-source). +See [Docstring Guidelines](https://github.com/pytorch/pytorch/wiki/Docstring-Guidelines) +for docstring conventions. + +```bash +cd docs/ +pip install -r requirements.txt +make html +make serve +``` + +Run `make` to get a list of all available output formats. + +If you get a katex error run `npm install katex`. If it persists, try +`npm install -g katex` + +> [!NOTE] +> If you installed `nodejs` with a different package manager (e.g., +> `conda`) then `npm` will probably install a version of `katex` that is not +> compatible with your version of `nodejs` and doc builds will fail. +> A combination of versions that is known to work is `node@6.13.1` and +> `katex@0.13.18`. To install the latter with `npm` you can run +> ```npm install -g katex@0.13.18``` + +> [!NOTE] +> If you see a numpy incompatibility error, run: +> ``` +> pip install 'numpy<2' +> ``` + +When you make changes to the dependencies run by CI, edit the +`.ci/docker/requirements-docs.txt` file. + +#### Building a PDF + +To compile a PDF of all PyTorch documentation, ensure you have +`texlive` and LaTeX installed. On macOS, you can install them using: + +``` +brew install --cask mactex +``` + +To create the PDF: + +1. Run: + + ``` + make latexpdf + ``` + + This will generate the necessary files in the `build/latex` directory. + +2. Navigate to this directory and execute: + + ``` + make LATEXOPTS="-interaction=nonstopmode" + ``` + + This will produce a `pytorch.pdf` with the desired content. Run this + command one more time so that it generates the correct table + of contents and index. + +> [!NOTE] +> To view the Table of Contents, switch to the **Table of Contents** +> view in your PDF viewer. + + +### Previous Versions + +Installation instructions and binaries for previous PyTorch versions may be found +on [our website](https://pytorch.org/get-started/previous-versions). + + +## Getting Started + +Three pointers to get you started: +- [Tutorials: get you started with understanding and using PyTorch](https://pytorch.org/tutorials/) +- [Examples: easy to understand PyTorch code across all domains](https://github.com/pytorch/examples) +- [The API Reference](https://pytorch.org/docs/) +- [Glossary](https://github.com/pytorch/pytorch/blob/main/GLOSSARY.md) + +## Resources + +* [PyTorch.org](https://pytorch.org/) +* [PyTorch Tutorials](https://pytorch.org/tutorials/) +* [PyTorch Examples](https://github.com/pytorch/examples) +* [PyTorch Models](https://pytorch.org/hub/) +* [Intro to Deep Learning with PyTorch from Udacity](https://www.udacity.com/course/deep-learning-pytorch--ud188) +* [Intro to Machine Learning with PyTorch from Udacity](https://www.udacity.com/course/intro-to-machine-learning-nanodegree--nd229) +* [Deep Neural Networks with PyTorch from Coursera](https://www.coursera.org/learn/deep-neural-networks-with-pytorch) +* [PyTorch Twitter](https://twitter.com/PyTorch) +* [PyTorch Blog](https://pytorch.org/blog/) +* [PyTorch YouTube](https://www.youtube.com/channel/UCWXI5YeOsh03QvJ59PMaXFw) + +## Communication +* Forums: Discuss implementations, research, etc. https://discuss.pytorch.org +* GitHub Issues: Bug reports, feature requests, install issues, RFCs, thoughts, etc. +* Slack: The [PyTorch Slack](https://pytorch.slack.com/) hosts a primary audience of moderate to experienced PyTorch users and developers for general chat, online discussions, collaboration, etc. If you are a beginner looking for help, the primary medium is [PyTorch Forums](https://discuss.pytorch.org). If you need a slack invite, please fill this form: https://goo.gl/forms/PP1AGvNHpSaJP8to1 +* Newsletter: No-noise, a one-way email newsletter with important announcements about PyTorch. You can sign-up here: https://eepurl.com/cbG0rv +* Facebook Page: Important announcements about PyTorch. https://www.facebook.com/pytorch +* For brand guidelines, please visit our website at [pytorch.org](https://pytorch.org/) + +## Releases and Contributing + +Typically, PyTorch has three minor releases a year. Please let us know if you encounter a bug by [filing an issue](https://github.com/pytorch/pytorch/issues). + +We appreciate all contributions. If you are planning to contribute back bug-fixes, please do so without any further discussion. + +If you plan to contribute new features, utility functions, or extensions to the core, please first open an issue and discuss the feature with us. +Sending a PR without discussion might end up resulting in a rejected PR because we might be taking the core in a different direction than you might be aware of. + +To learn more about making a contribution to Pytorch, please see our [Contribution page](CONTRIBUTING.md). For more information about PyTorch releases, see [Release page](RELEASE.md). + +## The Team + +PyTorch is a community-driven project with several skillful engineers and researchers contributing to it. + +PyTorch is currently maintained by [Soumith Chintala](http://soumith.ch), [Gregory Chanan](https://github.com/gchanan), [Dmytro Dzhulgakov](https://github.com/dzhulgakov), [Edward Yang](https://github.com/ezyang), [Alban Desmaison](https://github.com/albanD), [Piotr Bialecki](https://github.com/ptrblck) and [Nikita Shulga](https://github.com/malfet) with major contributions coming from hundreds of talented individuals in various forms and means. +A non-exhaustive but growing list needs to mention: [Trevor Killeen](https://github.com/killeent), [Sasank Chilamkurthy](https://github.com/chsasank), [Sergey Zagoruyko](https://github.com/szagoruyko), [Adam Lerer](https://github.com/adamlerer), [Francisco Massa](https://github.com/fmassa), [Alykhan Tejani](https://github.com/alykhantejani), [Luca Antiga](https://github.com/lantiga), [Alban Desmaison](https://github.com/albanD), [Andreas Koepf](https://github.com/andreaskoepf), [James Bradbury](https://github.com/jekbradbury), [Zeming Lin](https://github.com/ebetica), [Yuandong Tian](https://github.com/yuandong-tian), [Guillaume Lample](https://github.com/glample), [Marat Dukhan](https://github.com/Maratyszcza), [Natalia Gimelshein](https://github.com/ngimel), [Christian Sarofeen](https://github.com/csarofeen), [Martin Raison](https://github.com/martinraison), [Edward Yang](https://github.com/ezyang), [Zachary Devito](https://github.com/zdevito). + +Note: This project is unrelated to [hughperkins/pytorch](https://github.com/hughperkins/pytorch) with the same name. Hugh is a valuable contributor to the Torch community and has helped with many things Torch and PyTorch. + +## License + +PyTorch has a BSD-style license, as found in the [LICENSE](LICENSE) file. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL new file mode 100644 index 000000000..fb12b9fcb --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (79.0.1) +Root-Is-Purelib: false +Tag: cp312-cp312-linux_x86_64 + diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt new file mode 100644 index 000000000..2579adb5d --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt @@ -0,0 +1 @@ +https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/torch-2.10.0%2Brocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt new file mode 100644 index 000000000..0f1dbd261 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt @@ -0,0 +1,10 @@ +from typing import Optional + +__all__ = ['__version__', 'debug', 'cuda', 'git_version', 'hip', 'rocm', 'xpu'] +__version__ = '2.10.0+rocm7.2.4.git3d3aa833' +debug = False +cuda: Optional[str] = None +git_version = '3d3aa833db84eed6b7f5595cb5f162c2f78300a4' +hip: Optional[str] = '7.2.53211' +rocm: Optional[str] = '7.2.4' +xpu: Optional[str] = None diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 new file mode 100644 index 000000000..975b944c2 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 @@ -0,0 +1 @@ +e3a4b7f11eacc4037bc405fbf8beacf2ce19cc135ad283bb653b93a127f379d0 torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt new file mode 100644 index 000000000..01912075c --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3 -m venv /home/marcelorm/ds4v-work/source-rocm210-reference/.venv diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py new file mode 100644 index 000000000..41533d0a8 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Freeze first source-HIP outputs after exact repeat validation; CPU-only file work.""" +import datetime +import filecmp +import hashlib +import json +from pathlib import Path +import shutil + +home = Path.home() +root = home/'ds4v-work/source-rocm210-reference' +cpu = home/'lucebox-ds4v-mix-fix/artifacts/vision-reference' +canonical = root/'source-hip-reference' +freeze_path = root/'source-hip-reference-freeze.json' +assert not canonical.exists() and not freeze_path.exists() + +def digest(path): + with path.open('rb') as f: + return hashlib.file_digest(f, 'sha256').hexdigest() + +def read(path): + return json.loads(path.read_text()) + +policy = {'reviewed':(root/'reference-policy-reviewed.md','7edde20ee70b804cc903b827dbea1dbc9b8d43d9d352e22f82672758430f9682'), + 'adopted':(root/'reference-policy-adopted.md','62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f')} +for path, expected in policy.values(): + assert digest(path) == expected +assert [x for x in policy['reviewed'][0].read_text().splitlines() if not x.startswith('Status:')] == \ + [x for x in policy['adopted'][0].read_text().splitlines() if not x.startswith('Status:')] +assert digest(cpu/'manifest.json') == '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f' +original = read(cpu/'manifest.json') +runner = root/'source-forward-radeon-name.py' +assert digest(runner) == 'cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c' +pairs = {'corn':('hip-corn-confirmed','hip-supervision-confirmed','source-corn-repeat','source-corn-repeat-supervision'), + 'carrots':('source-carrots-first','source-carrots-first-supervision','source-carrots-repeat','source-carrots-repeat-supervision')} +gates = {'features':{'max_abs':.25,'rmse':.03,'cosine':.9995}, + 'embeddings':{'max_abs':.75,'rmse':.08,'cosine':.9990}} +freeze = {'status':'SOURCE_REFERENCE_STABILITY_PASS', 'candidate_acceptance':'NOT_EVALUATED', + 'frozen_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(), + 'policy':{k:{'file':str(p),'sha256':h} for k,(p,h) in policy.items()}, + 'original_cpu_manifest':{'file':str(cpu/'manifest.json'),'sha256':digest(cpu/'manifest.json')}, + 'runner':{'file':str(runner),'sha256':digest(runner)}, 'gates':gates, 'images':{}, + 'reference_selection':'FIRST completed source corn and FIRST source carrots outputs; repeats only validate stability', + 'scope':'Original source on this RX7900XT/software configuration; CPU portability remains separate'} +manifest = {'torch':'2.10.0+rocm7.2.4.git3d3aa833', 'reference_kind':'original_source_hip_7900xt', + 'config':original['config'], 'source_hashes':original['source_hashes'], 'images':{}, + 'policy_sha256':policy['adopted'][1], 'original_cpu_manifest_sha256':freeze['original_cpu_manifest']['sha256']} +all_libraries = set() +for image,(first_name,first_supervision,repeat_name,repeat_supervision) in pairs.items(): + first,repeat = root/first_name,root/repeat_name + reports = [read(first/'report.json'),read(repeat/'report.json')] + runs = [read(root/first_supervision/'run.json'),read(root/repeat_supervision/'run.json')] + for report,run in zip(reports,runs): + assert run['exit'] == 0 and not run.get('timed_out') and not run.get('error') + assert report['script_sha256'] == freeze['runner']['sha256'] + assert report['source_hashes'] == original['source_hashes'] and report['image'] == image + assert report['reference_manifest_sha256'] == freeze['original_cpu_manifest']['sha256'] + assert report['weight_inventory_sha256'] == 'b0556c40a8bff3f4c2c262d57137a97123cbdbf7444a7fae495ef17cd28469ee' + assert report['device']['name'] == 'Radeon RX 7900 XT' and report['device']['gcn_arch'].split(':')[0] == 'gfx1100' + assert report['device']['rocr_visible_device'] == 'GPU-93a97448a27aeff3' + assert report['torch_git'] == '3d3aa833db84eed6b7f5595cb5f162c2f78300a4' and report['torch_hip'] == '7.2.53211' + assert report['threads'] == [2,2] and report['default_dtype'] == 'torch.bfloat16' + all_libraries.update(report['loaded_libraries']) + item = {key:original['images'][image][key] for key in ('image_sha256','vit_grid','aligner_grid','patches')} + assert digest(cpu/item['patches']['file']) == item['patches']['sha256'] + evidence = {'first_directory':str(first),'repeat_directory':str(repeat), + 'first_report_sha256':digest(first/'report.json'),'repeat_report_sha256':digest(repeat/'report.json'), + 'first_run':runs[0],'repeat_run':runs[1], 'hardware':reports[0]['device'], + 'outputs':{}, 'cpu_portability':{'first':reports[0]['comparisons'],'repeat':reports[1]['comparisons']}} + for stage in gates: + a,b = reports[0]['outputs'][stage],reports[1]['outputs'][stage] + assert a['shape'] == b['shape'] == original['images'][image][stage]['shape'] + assert digest(first/a['file']) == a['sha256'] and digest(repeat/b['file']) == b['sha256'] + assert a['sha256'] == b['sha256'] and filecmp.cmp(first/a['file'],repeat/b['file'],shallow=False) + for report in reports: + assert report['comparisons'][stage]['gate'] == gates[stage] and report['comparisons'][stage]['finite'] + item[stage] = a.copy() + evidence['outputs'][stage] = {'first_sha256':a['sha256'],'repeat_sha256':b['sha256'],'byte_identical':True} + freeze['images'][image] = evidence + manifest['images'][image] = item + +# These hashes capture the actual loaded shared-library set, not a guessed loader path. +freeze['loaded_libraries'] = [{'file':name,'sha256':digest(Path(name)),'bytes':Path(name).stat().st_size} + for name in sorted(all_libraries)] +freeze['provenance_files'] = {name:{'file':str(root/name),'sha256':digest(root/name)} for name in ( + 'requirements.lock','constraints.txt','evidence/wheels.json','evidence/install-report.json', + 'evidence/cpu-runtime-private.json','evidence/miopen-package.json','evidence/miopen-library.json', + 'evidence/freeze.txt','reference-policy-reviewed.md','reference-policy-adopted.md', + 'run-hip-confirmed-supervised.py','run-source-stability-supervised.py','compare-corn-three-way.py')} +freeze['wheel_inventory'] = read(root/'evidence/wheels.json') +freeze['miopen_package'] = read(root/'evidence/miopen-package.json') +freeze['miopen_library'] = read(root/'evidence/miopen-library.json') +freeze['source_weights'] = {'inventory_sha256':reports[0]['weight_inventory_sha256'], + 'index_sha256':reports[0]['index_sha256'],'source_hashes':original['source_hashes']} +freeze['freeze_script_sha256'] = digest(Path(__file__)) +canonical.mkdir() +for image,item in manifest['images'].items(): + first = Path(freeze['images'][image]['first_directory']) + for stage in ('patches','features','embeddings'): + entry = item[stage] + src = cpu/entry['file'] if stage == 'patches' else first/entry['file'] + dst = canonical/entry['file'] + shutil.copyfile(src,dst) + assert digest(dst) == entry['sha256'] + dst.chmod(0o444) +(canonical/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n') +(canonical/'manifest.json').chmod(0o444) +freeze['canonical_reference'] = {'directory':str(canonical),'manifest_sha256':digest(canonical/'manifest.json')} +freeze_path.write_text(json.dumps(freeze,indent=2)+'\n') +freeze_path.chmod(0o444) +print(json.dumps({'canonical_directory':str(canonical),'manifest_sha256':digest(canonical/'manifest.json'), + 'freeze_manifest':str(freeze_path),'freeze_manifest_sha256':digest(freeze_path), + 'status':freeze['status'],'frozen_utc':freeze['frozen_utc']},indent=2)) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md new file mode 100644 index 000000000..e592d8987 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md @@ -0,0 +1,17 @@ +# Original-source corn HIP control + +**Initial released attempt stopped at the device-name guard before weight loading or a tower forward. No source-HIP tensors were produced.** The frozen script was not changed for this attempt, and no numerical threshold was changed. + +The parent's explicit GPU release authorized the single original-source corn lane. `run-hip-supervised.py` verified source-forward SHA256 `17ba9d66260d25c056ecc56a35192748a662b040605bb0bf8864bd704a66267b` and private MIOpen SHA256 `bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd`, then checked operator inactive/MainPID0, no listeners8016/8217, empty KFD process directory, and at least8 GiB host and discrete VRAM available. Actual preflight:36102742016 host bytes and21430087680 discrete bytes free. + +The Python argv/environment matched the prepared command; direct child supervision with `os.wait4` supplied actual PID/resource timing instead of GNU time. The finite deadline was300 seconds, with termination restricted to the recorded unreaped direct child. No timeout or signal was needed. + +- Actual Python PID3359202, exit1, elapsed2.091728805 seconds. +- User1.455116 seconds, system0.239513 seconds, peak RSS744712 KiB. +- Error:`RuntimeError: actual device is not RX 7900 XT` at the exact `props.name == 'AMD Radeon RX 7900 XT'` guard. +- The frozen script constructs an identity dictionary before that guard but does not print it on this failure path, so the actual Torch name was not captured. No weight loading, forward or output directory creation occurred. +- After exit:operator inactive/PID0, ports free, KFD empty, discrete VRAM unchanged at21430087680 free bytes. + +The completed native lane's existing `device-check.log` reports Device0 as `Radeon RX 7900 XT` without the `AMD` prefix, gfx1100,20464 MiB. This is a plausible display-name mismatch, not proof of the source attempt's actual selected device. No further GPU identity query or retry was performed in this initial attempt. + +Evidence:remote `~/ds4v-work/source-rocm210-reference/hip-supervision/`; local copied `hip-supervision/{run.json,hip-corn.log,memory.jsonl,python.pid,exit}`. The wrapper source is in this report's directory. Source-HIP/CPU and native-HIP/source-HIP comparisons remain unavailable until a source forward completes. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md new file mode 100644 index 000000000..c44f6238d --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md @@ -0,0 +1,51 @@ +# Original-source corn HIP control — completed diagnostic + +**The source HIP forward completed, but source-HIP/CPU feature portability fails the unchanged gate. Native HIP also fails against source HIP for both features and embeddings. Native vision remains NOT QUALIFIED.** These results separate a source backend portability effect from an additional native discrepancy; they do not establish its exact cause or justify widening thresholds. + +## Execution and identity + +The first released attempt used the original frozen runner17ba9d66 unchanged. It exited1 at the exact marketing-name guard before weight loading/forward. That evidence is preserved in `hip-supervision/` and `hip-control-initial-failure.md`. + +The parent then authorized one identity-only query and a narrow name correction if the actual device agreed. Identity-only Python PID3362155 exited0, reporting exactly one visible device: `Radeon RX 7900 XT`, gfx1100,21458059264 bytes (20464 MiB), selected by ROCr UUID `GPU-93a97448a27aeff3`, PCI bus198/device0. The mismatch was the expected string's `AMD ` prefix. + +The reviewed correction changes only that exact expected name and prints actual properties before asserting them. The original source runner and original `vision.py` remain unchanged. `radeon-name-correction.diff` records the two-line correction. New runner `source-forward-radeon-name.py` SHA256: +`cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. + +The parent received this hash before execution and reviewed the diff. Supervision reused the operator/idle/memory/hash guards, created fresh `hip-supervision-confirmed/` and `hip-corn-confirmed/`, and imposed a300-second deadline. The exact Python command/environment are in `hip-supervision-confirmed/run.json`; direct `os.wait4` supervision recorded the actual Python PID/resource usage instead of GNU time. It signals only that unreaped direct child if necessary. + +- Before:operator inactive/MainPID0; no ports8016/8217; KFD empty;36154867712 bytes host available;21430087680 bytes discrete VRAM free. +- Actual corrected Python PID3362755, exit0, elapsed5.758919639 seconds; user4.532378/system0.985126 seconds; peak RSS2504072 KiB. +- Original source forward0.798916269 seconds; GPU peak allocated1176237056 bytes, reserved1186988032 bytes. +- After:operator inactive/PID0, ports free, KFD empty, discrete VRAM back to21430087680 free bytes. GPU ownership was released to the parent immediately after completion. + +There was exactly one actual source corn HIP forward. The initial guard failure and identity-only query did not run a model. No additional image, Torch version, SDPA variant, source math, native code, operator, converter or fixture was changed. + +## Fixed three-way comparison + +All compared tensors are finite and have exact expected shapes:features782×1024, embeddings96×4096. The CPU-only comparison verified the original manifest and every input file hash. Existing fixed gates remain features maxabs≤0.25/RMSE≤0.03/cosine≥0.9995; embeddings maxabs≤0.75/RMSE≤0.08/cosine≥0.9990. All three comparisons retain their own verdicts. + +| Stage | Pair | Max absolute | RMSE | Cosine | Gate | +|---|---|---:|---:|---:|---| +| Features | Source HIP vs original CPU | 2.73828125 | 0.007125659433 | 0.997729789065 | FAIL | +| Features | Native HIP vs source HIP | 1.2039794921875 | 0.014023936578 | 0.991196938632 | FAIL | +| Features | Native HIP vs original CPU | 2.9617919921875 | 0.017743029365 | 0.985938580153 | FAIL | +| Embeddings | Source HIP vs original CPU | 0.09130859375 | 0.003126610779 | 0.999115331698 | PASS | +| Embeddings | Native HIP vs source HIP | 0.16259765625 | 0.007664476777 | 0.994697536836 | FAIL | +| Embeddings | Native HIP vs original CPU | 0.176513671875 | 0.009035238955 | 0.992632567277 | FAIL | + +The new AMD Torch environment's earlier CPU outputs were byte-identical to the immutable original CPU fixtures. The source-HIP/CPU gap therefore appears when executing the original graph on HIP in this controlled environment. This observation is bounded to this image/runtime/default dispatch; it is not a universal backend-error estimate. Native-HIP/source-HIP still fails both stages, so source portability does not explain away the native discrepancy. The old native-HIP outputs are the parent's completed frozen `hip-component-first/native` files, not a new native run. + +## Exact output identities + +| Stage | Producer | SHA256 | +|---|---|---| +| Features | Original CPU | `aa7c43be7182759f83881cf823661bf52c14b73222645d1ec37b14c6502bc982` | +| Features | Source HIP | `5790c492de2618560f81bac3ab8a70282a272be0e1885b95246621aab0bf4bb8` | +| Features | Native HIP | `59bd19a13750d07f7f1018c32c5a43c4ae2cd7a7ff132da3f6201b08c400cc4e` | +| Embeddings | Original CPU | `c96d59ae722ad8ac31299aabb4e758b788a1ee4be30ea94b833c753721229040` | +| Embeddings | Source HIP | `80a30a096a9dd84e91f8d47d63b1cf00b3eded88f6e472b1018bbdabb697ab86` | +| Embeddings | Native HIP | `a398c9c10a7b2bbeb63f4b910bf5f71bb9fefd0aa388b276a3bf06ca3e280a06` | + +Comparison script SHA256:`3741e93cab886c9050e2aa484236a6b15d7aa8b6f3ed55ae1e8acfe37cf4d555`; comparison process exit0 means metrics completed, not numerical acceptance. + +Local evidence is under this report's directory: `hip-identity-supervision/`, `hip-supervision-confirmed/{run.json,hip-corn.log,memory.jsonl,three-way.json,three-way.log}`, and `hip-corn-confirmed/report.json`. Complete output tensor files remain in the corresponding soulf root `~/ds4v-work/source-rocm210-reference/`. Original runner17ba9d66, private library hash, original source/weight/reference hashes and exact pins remain preserved. This report does not qualify another image, source repeat stability, end-to-end image answers or later server integration. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md new file mode 100644 index 000000000..a17d61790 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md @@ -0,0 +1,59 @@ +# First-source HIP reference freeze + +**SOURCE_REFERENCE_STABILITY_PASS.** Corn and carrots each produced byte-identical features and embeddings in two independent fresh processes on the same7900XT. The FIRST completed output for each image is now the frozen reference. No corrected native full-tower output was inspected or used to select it. This establishes reference stability for the adopted target-GPU policy; it does not accept any native candidate or resolve the original CPU feature failure. + +The exact parent policy was independently reviewed at SHA256 `7edde20ee70b804cc903b827dbea1dbc9b8d43d9d352e22f82672758430f9682`. The parent changed only the status sentence after both independent PASS reviews; adopted policy SHA256 is `62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f`. Both text versions and hashes are pinned in the provenance manifest, and their bodies were checked identical apart from the Status line. The initial copy raced the parent's status update; the first freeze correctly rejected the reviewed-hash mismatch before writing a reference directory. The exact reviewed text was restored to a separate file and verified against7edde20e; both versions remain preserved. + +## Canonical reference and provenance + +Remote canonical directory: +`/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference` + +Its `manifest.json` uses the existing comparison schema: each image has vit_grid, aligner_grid and patches/features/embeddings entries with file, shape and SHA256. Patch files are exact copies of the immutable original CPU patches. Feature/embedding files are exact copies of FIRST source-HIP outputs, never repeat-selected samples. All canonical files are read-only. + +- Canonical `manifest.json` SHA256:`677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86`. +- Separate remote `~/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json` SHA256:`8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0`. +- Frozen UTC:`2026-09-05T01:40:51.838089+00:00`. +- Source runner SHA256:`cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. +- Original CPU manifest SHA256:`38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f`. + +The freeze manifest pins first/repeat raw-byte hashes and report hashes; exact source argv/environment/PIDs/resources; first actual hardware identities; all14 wheel hashes/versions, install and private MIOpen provenance;77 actual loaded shared-library paths and SHA256 values; original source/index/weight inventory/patch identities; source supervision and comparison scripts; both policy hashes and all unchanged numerical gates. Repeats were checked with both SHA256 and whole-file byte comparisons. No original CPU reference was changed. + +## Source execution and resources + +All source forwards use the same unchanged original modules, weights, BF16/F32 boundaries, default SDPA, original hashed patches, AMD Torch/runtime and private MIOpen. Actual device identity is `Radeon RX 7900 XT`, gfx1100, UUID selection`GPU-93a97448a27aeff3`,20464 MiB. Source processes use two CPU threads. The earlier exact-name failure and identity-only query remain separate; they ran no model. + +| Lane | Actual Python PID | Exit | Process seconds | Forward seconds | Peak RSS KiB | GPU allocated / reserved bytes | +|---|---:|---:|---:|---:|---:|---| +| FIRST corn, retained | 3362755 | 0 | 5.758920 | 0.798916 | 2504072 | 1176237056 / 1186988032 | +| Corn repeat | 3366985 | 0 | 5.234832 | 0.389909 | 2503140 | 1176237056 / 1186988032 | +| FIRST carrots, retained | 3367102 | 0 | 5.763091 | 0.855355 | 2517764 | 2089592320 / 2134900736 | +| Carrots repeat | 3367383 | 0 | 5.750177 | 0.855419 | 2517508 | 2089592320 / 2134900736 | + +Every lane passed operator inactive/MainPID0, no ports8016/8217, empty KFD and≥8 GiB host/discrete-memory preflight. Each had a300-second deadline; no timeout or signal occurred. Each source child exited before the next launch. After each, KFD was empty and discrete free VRAM returned to21430087680 bytes. GPU ownership was released to the parent immediately after the three new source stability children exited; subsequent manifest work was CPU-only. The timing difference between first/repeat corn was not used to select a reference or make a speed claim. + +## Frozen FIRST output hashes + +| Image | Stage | SHA256, also matched by its repeat | +|---|---|---| +| Corn | Features | `5790c492de2618560f81bac3ab8a70282a272be0e1885b95246621aab0bf4bb8` | +| Corn | Embeddings | `80a30a096a9dd84e91f8d47d63b1cf00b3eded88f6e472b1018bbdabb697ab86` | +| Carrots | Features | `270b09f7b62d47162137613df78c5735284dee5b2d31a42892e12ec9631d57b1` | +| Carrots | Embeddings | `4eaf4a6de24d0b9c6cab3d42ec13a3a74e7a06627c21262106e6b059ac4bbb4f` | + +## Original CPU portability remains separate + +| Image/stage | Max absolute | RMSE | Cosine | Original gate | +|---|---:|---:|---:|---| +| Corn features | 2.73828125 | 0.007125659433 | 0.997729789065 | FAIL | +| Corn embeddings | 0.09130859375 | 0.003126610779 | 0.999115331698 | PASS | +| Carrots features | 0.1484375 | 0.002524693483 | 0.999693162950 | PASS | +| Carrots embeddings | 0.0302734375 | 0.001367435885 | 0.999825389498 | PASS | + +All outputs are finite and shapes match the original manifest. Feature gates remain maxabs≤0.25/RMSE≤0.03/cosine≥0.9995; embedding gates remain maxabs≤0.75/RMSE≤0.08/cosine≥0.9990. The failed corn CPU feature gate remains a failure. The previously measured native-HIP/source-HIP comparison still fails both corn stages; nothing in this freeze relabels it. + +## Review and limits + +The independent recommendation is `target-hip-acceptance-review.md`. Target-GPU fidelity is a justified, explicitly different question from cross-device CPU portability. Matching the original source on this GPU cannot rule out an underlying source-HIP backend defect shared by another implementation; it is not absolute numerical ground truth. That limitation, the unchanged CPU failures, default-SDPA dispatch dependence and the experimental AMD fork/host tuple must remain visible. Two stable fixtures are not universal reproducibility or accuracy coverage. The corrected native GEMM regression, target-tower gates and separate end-to-end image behavior remain required before claiming working vision. + +Local copies: `source-hip-reference-freeze.json`, `evidence/source-hip-reference-manifest.json`, `source-corn-repeat/`, `source-carrots-first/`, `source-carrots-repeat/`, and corresponding `*-supervision/` directories under this report's directory. The local manifest is a review copy; complete canonical tensor files remain on soulf. Copied manifest hashes were rechecked locally. No further GPU execution occurred after release. diff --git a/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md b/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md new file mode 100644 index 000000000..b68515668 --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md @@ -0,0 +1,42 @@ +# Prospective 7900 XT qualification policy review + +**Verdict: PASS as a prospective, target-scoped numerical policy.** Reviewed +`target-hip-qualification-policy.md` at SHA256 +`62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f` +before any corrected full-tower output was produced. The post-review status-line +adoption did not change the reviewed procedure. This review does not qualify the +current or corrected native tower. + +The target reference is scientifically motivated by observed source behavior, +not selected from candidate results: the unchanged original source on the +7900 XT fails the existing CPU feature threshold, while its embedding remains +within the existing embedding threshold. A same-device source reference controls +the backend-dependent reduction schedule that the original CPU fixture cannot. +The policy keeps the already published CPU comparisons and their failures +visible, so a target result cannot be presented as CPU equivalence. + +The freeze is suitably prospective and resists result shopping. It requires the +same original model, weights, patches, BF16 boundaries, default source operations, +pinned software and exact 7900 XT identity; byte-identical source repeats for +corn and carrots; a manifest containing all source outputs and provenance before +the corrected native full tower runs; and no later reference replacement based +on candidate output. The corrected source runner's only semantic change is the +observed marketing-name check and identity logging; its SHA256 is frozen as +`cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. + +Acceptance remains demanding: both images must independently pass every existing +feature and embedding threshold with exact shapes and finite values, failures +must remain machine-visible, and the native corn repeat must be byte-identical. +The dyadic biased-linear regression can run before the reference freeze because +it neither executes the full tower nor supplies a reference output. + +Any PASS is limited to the native tower on the recorded 7900 XT/software tuple. +It does not qualify CPU numerics, gfx1151, other accelerators, decoder behavior, +HTTP image input, image-dependent answers, isolation, resource limits or cleanup. +Those separate gates remain required. The main residual scientific limitation is +fixture breadth: two images establish the selected deployment gate, not general +cross-image or cross-backend equivalence. No tolerance, source implementation or +reference backend may be changed after seeing corrected candidate results. + +This was a read-only policy and evidence review. No build, model execution, GPU +operation, server action or runtime source edit was performed. diff --git a/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md b/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md new file mode 100644 index 000000000..76d63a5ad --- /dev/null +++ b/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md @@ -0,0 +1,33 @@ +# Prospective 7900 XT vision qualification + +Status: adopted prospectively after two independent PASS reviews. No corrected full-tower candidate output has been produced under this policy. The original CPU fixtures and their thresholds remain unchanged and all comparisons to them stay visible. + +## Evidence motivating the target reference + +The unchanged original source, fixed AMD Torch2.10/ROCm7.2.4 environment, original weights and identical corn patches produced a source-HIP feature cosine of0.997729789 and maximum absolute difference2.73828125 against original CPU. These fail the existing feature gate. Source-HIP embeddings pass the original embedding gate. This demonstrates that the existing CPU feature threshold is not portable even to the original implementation on this target GPU; it does not establish native correctness. The existing native HIP implementation also fails against source HIP, with feature cosine0.991196939 and embedding cosine0.994697537. + +Evidence: `source-rocm-reference/hip-supervision-confirmed/three-way.json`. The narrowly corrected original-source runner only logs actual device identity and matches the observed marketing name. Its source SHA256 is `cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. Model code, weights, precision boundaries, default SDPA, image patches and library versions are unchanged. + +## Reference freeze before candidate execution + +Use the same original-source runner on exactly Radeon RX7900XT/gfx1100, selected by UUID `GPU-93a97448a27aeff3`, with its pinned environment and weights. Retain the first completed corn output. Run one corn repeat and two carrots runs in fresh processes/directories. Require each image's features and embeddings to repeat byte-identically; otherwise stop qualification and investigate reference stability without choosing a favorable sample. + +Freeze the first source outputs for both images, their repeat hashes, the existing CPU manifest and patch hashes, runtime/library provenance, hardware identity, exact runner and comparison scripts in one manifest before executing the corrected native full tower. Never replace references based on candidate results. The small exact-dyadic biased-linear regression is independent of this full-tower policy and may execute first. + +## Unchanged numeric gates, explicit scope + +For both images separately, require exact expected dimensions, finite values and every existing gate against frozen same-GPU source output: + +- Final normalized features: maximum absolute difference<=0.25, RMSE<=0.03, cosine>=0.9995. +- Aligner embeddings: maximum absolute difference<=0.75, RMSE<=0.08, cosine>=0.9990. +- Native corn repeat must be byte-identical. Failed metrics remain machine-visible failures with nonzero qualification exit. + +Publish native-vs-original-CPU and source-HIP-vs-original-CPU results beside the target comparison using the unchanged thresholds. A target PASS cannot relabel those CPU portability failures as PASS. CPU functional/regression suites remain required, while numerical CPU tower qualification remains separately unresolved unless its original gate actually passes. + +A successful result qualifies only this native tower on this 7900XT/software configuration. It does not qualify Strix vision execution, other GPU architectures, CPU tower numerics, full decoder parity, other vision model architectures, or image chat. The supported placement keeps the tower on7900XT; Strix continues to own tail language experts. Memory limits, unsupported-path errors, transactional loading and exact projector/preprocessing contracts still apply. + +## Integration and behavior remain separate + +Only a passing target tower may clear the tower dependency for the selected heterogeneous runtime integration. Actual HTTP image input, correct image-dependent answers for both fixtures and equal-layout/different-image isolation, malformed-input behavior, text regression, GPU ownership, memory/latency and cleanup remain required. Sparse decoder prefill remains explicitly approximate. + +Do not sweep tolerances, source versions, precision modes or reference backends to obtain a pass. Any further change to qualification policy must be prospective, separately motivated and independently reviewed, with old failures retained. From 94856ca95034fbc9875dca093916d70e0932e383 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:39:53 +0200 Subject: [PATCH 081/123] build(ds4v): make hipBLASLt optional for the HIP backend The vision tower ops were the only users, yet find_package(hipblaslt REQUIRED) broke every HIP build without the -dev package, including Dockerfile.rocm. The ops now sit behind GGML_HIP_DS4V_VISION, set only when hipBLASLt is found. Co-Authored-By: Claude Fable 5.1 --- .../llama.cpp/ggml/src/ggml-cuda/common.cuh | 4 +++- .../llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu | 22 +++++++++---------- .../deps/llama.cpp/ggml/src/ggml-cuda/norm.cu | 2 +- .../llama.cpp/ggml/src/ggml-cuda/norm.cuh | 2 +- .../llama.cpp/ggml/src/ggml-cuda/vision-av.cu | 2 +- .../ggml/src/ggml-cuda/vision-av.cuh | 2 +- .../ggml/src/ggml-cuda/vision-bias.cu | 2 +- .../ggml/src/ggml-cuda/vision-bias.cuh | 2 +- .../ggml/src/ggml-cuda/vision-rotary.cu | 2 +- .../ggml/src/ggml-cuda/vision-rotary.cuh | 2 +- .../ggml/src/ggml-cuda/vision-softmax.cu | 2 +- .../ggml/src/ggml-cuda/vision-softmax.cuh | 2 +- .../ggml/src/ggml-hip/CMakeLists.txt | 15 +++++++++++-- 13 files changed, 37 insertions(+), 24 deletions(-) diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh index fc29fb642..9c11002f5 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/common.cuh @@ -34,7 +34,9 @@ #if defined(GGML_USE_HIP) #include "vendors/hip.h" +#if defined(GGML_HIP_DS4V_VISION) #include +#endif #elif defined(GGML_USE_MUSA) #include "vendors/musa.h" #else @@ -1449,7 +1451,7 @@ struct ggml_backend_cuda_context { cudaStream_t streams[GGML_CUDA_MAX_DEVICES][GGML_CUDA_MAX_STREAMS] = { { nullptr } }; cublasHandle_t cublas_handles[GGML_CUDA_MAX_DEVICES] = {nullptr}; -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) hipblasLtHandle_t vision_bias_handle = nullptr; void * vision_bias_workspace = nullptr; // shared linear/AV 76 MiB, retained until context destruction cudaEvent_t vision_bias_event = nullptr; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu index b66bc7247..09614c198 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/ggml-cuda.cu @@ -733,7 +733,7 @@ ggml_backend_cuda_context::~ggml_backend_cuda_context() { luce_q8_memo.pop_back(); } -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) if (vision_bias_workspace) { ggml_cuda_set_device(device); // The latest event follows every use of the shared workspace. @@ -3612,28 +3612,28 @@ static bool ggml_cuda_compute_forward(ggml_backend_cuda_context & ctx, struct gg ggml_cuda_flash_attn_sparse(ctx, dst); break; case GGML_OP_MUL_MAT_BIAS_BF16: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) ggml_hip_vision_bias(ctx, dst); break; #else return false; #endif case GGML_OP_RMS_NORM_VISION_F32: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) ggml_hip_vision_norm(ctx, dst); break; #else return false; #endif case GGML_OP_SOFT_MAX_VISION_F32: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) ggml_cuda_op_soft_max_vision_f32(ctx, dst); break; #else return false; #endif case GGML_OP_MUL_MAT_VISION_AV_F32: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) ggml_hip_vision_av_f32(ctx, dst); break; #else @@ -6570,25 +6570,25 @@ static bool ggml_backend_cuda_device_supports_op(ggml_backend_dev_t dev, const g case GGML_OP_FLASH_ATTN_SPARSE: return true; // Always supported on CUDA case GGML_OP_MUL_MAT_BIAS_BF16: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) return ggml_hip_vision_bias_supported(op); #else return false; #endif case GGML_OP_RMS_NORM_VISION_F32: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) return ggml_hip_vision_norm_supported(dev_ctx->device, op); #else return false; #endif case GGML_OP_SOFT_MAX_VISION_F32: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) return ggml_hip_vision_softmax_f32_supported(dev_ctx->device, op); #else return false; #endif case GGML_OP_MUL_MAT_VISION_AV_F32: -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) return ggml_hip_vision_av_f32_supported(dev_ctx->device, op); #else return false; @@ -6770,7 +6770,7 @@ static ggml_backend_feature * ggml_backend_cuda_get_features(ggml_backend_reg_t GGML_UNUSED(reg); } -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) static size_t ggml_backend_hip_vision_bias_bf16_workspace(ggml_backend_t backend) { return backend && ggml_backend_is_cuda(backend) ? 76ULL*1024*1024 : 0; } @@ -6817,7 +6817,7 @@ static size_t ggml_backend_hip_vision_rotary_f32_launches(ggml_backend_t backend #endif static void * ggml_backend_cuda_reg_get_proc_address(ggml_backend_reg_t reg, const char * name) { -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_workspace")==0) return (void *)ggml_backend_hip_vision_bias_bf16_workspace; if (strcmp(name,"ggml_backend_hip_vision_bias_bf16_launches")==0) return (void *)ggml_backend_hip_vision_bias_bf16_launches; if (strcmp(name,"ggml_backend_hip_vision_norm_f32_capable")==0) return (void *)ggml_backend_hip_vision_norm_f32_capable; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu index ae948e935..5e01ae0a0 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cu @@ -3,7 +3,7 @@ #include #include -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) bool ggml_hip_vision_norm_capable(int device) { const auto & info = ggml_cuda_info(); return device >= 0 && device < info.device_count && info.devices[device].warp_size == 32; diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh index c6c91cb7d..b0eb27e2c 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/norm.cuh @@ -6,7 +6,7 @@ void ggml_cuda_op_group_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst) void ggml_cuda_op_rms_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst); -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) bool ggml_hip_vision_norm_capable(int device); bool ggml_hip_vision_norm_supported(int device, const ggml_tensor * op); void ggml_hip_vision_norm(ggml_backend_cuda_context & ctx, ggml_tensor * dst); diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu index 051e87178..12cbfc266 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cu @@ -2,7 +2,7 @@ #include "vision-bias.cuh" #include "norm.cuh" -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) bool ggml_hip_vision_av_f32_supported(int device, const ggml_tensor * op) { if (!ggml_hip_vision_norm_capable(device) || !op || op->op != GGML_OP_MUL_MAT_VISION_AV_F32 || !op->src[0] || !op->src[1] || op->type != GGML_TYPE_F32) { diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh index 70eaa54f2..efc14fb61 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-av.cuh @@ -1,7 +1,7 @@ #pragma once #include "common.cuh" -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) bool ggml_hip_vision_av_f32_supported(int device, const ggml_tensor * op); void ggml_hip_vision_av_f32(ggml_backend_cuda_context & ctx, ggml_tensor * dst); #endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu index 3e422eb17..d0be88183 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cu @@ -1,5 +1,5 @@ #include "vision-bias.cuh" -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) #include bool ggml_hip_vision_bias_supported(const ggml_tensor * d) { diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh index c4a92c33f..ad3a233f7 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-bias.cuh @@ -1,6 +1,6 @@ #pragma once #include "common.cuh" -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) // One workspace/handle/event shared by explicit linear and AV operations. constexpr size_t GGML_HIP_VISION_WORKSPACE_BYTES = 76ULL*1024*1024; void ggml_hip_vision_workspace_acquire(ggml_backend_cuda_context & ctx); diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu index 16dd114bb..b36a705bf 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cu @@ -3,7 +3,7 @@ #include #include -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) // PyTorch 3d3aa833db84eed6b7f5595cb5f162c2f78300a4, aten/src/ATen/native/cuda/: // Pow.cuh uses global ::pow on Linux; UnaryFractionKernels.cu uses float 1/a; // UnaryGeometric{Cos,Sin}Kernel.cu use global ::cos and ::sin. The five kernels diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh index 2f11acbc7..bc3c62f6d 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-rotary.cuh @@ -1,7 +1,7 @@ #pragma once #include "common.cuh" -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) // Caller supplies two distinct host buffers of height*width*32 floats each. // Success fills both buffers synchronously and records one complete table call. bool ggml_hip_vision_rotary(ggml_backend_cuda_context & ctx, int64_t height, int64_t width, diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu index 7e5399a0e..78747b946 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cu @@ -1,6 +1,6 @@ #include "vision-softmax.cuh" -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) #include "vision-softmax-kernels.cuh" #include diff --git a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh index 7bcbffef7..9dcfb5ce9 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh +++ b/server/deps/llama.cpp/ggml/src/ggml-cuda/vision-softmax.cuh @@ -2,7 +2,7 @@ #include "common.cuh" -#if defined(GGML_USE_HIP) +#if defined(GGML_HIP_DS4V_VISION) bool ggml_hip_vision_softmax_f32_supported(int device, const ggml_tensor * op); void ggml_cuda_op_soft_max_vision_f32(ggml_backend_cuda_context & ctx, ggml_tensor * dst); #endif diff --git a/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt b/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt index 33d6d4607..0c20b89f0 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt +++ b/server/deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt @@ -45,7 +45,7 @@ endif() find_package(hip REQUIRED) find_package(hipblas REQUIRED) -find_package(hipblaslt REQUIRED) +find_package(hipblaslt QUIET) find_package(rocblas REQUIRED) if (GGML_HIP_RCCL) @@ -173,4 +173,15 @@ get_filename_component(GGML_HIP_RUNTIME_DIR "${hip_DIR}/../.." ABSOLUTE) target_link_directories(ggml-hip BEFORE PRIVATE "${GGML_HIP_RUNTIME_DIR}") target_link_options(ggml-hip PRIVATE "-L${GGML_HIP_RUNTIME_DIR}") -target_link_libraries(ggml-hip PRIVATE ggml-base hip::host roc::rocblas roc::hipblas roc::hipblaslt) +target_link_libraries(ggml-hip PRIVATE ggml-base hip::host roc::rocblas roc::hipblas) + +# The DS4V vision tower ops use a fused hipBLASLt BF16 bias matmul. Without the +# hipBLASLt development package they are compiled out: the ops report +# unsupported and the server refuses --mmproj at startup. +if (hipblaslt_FOUND) + message(STATUS "hipBLASLt found: building the DS4V vision ops") + target_compile_definitions(ggml-hip PRIVATE GGML_HIP_DS4V_VISION) + target_link_libraries(ggml-hip PRIVATE roc::hipblaslt) +else() + message(STATUS "hipBLASLt not found: DS4V vision ops disabled") +endif() From 6f784d4a7914af6f70f6dc1378a504ba5789fde4 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:40:24 +0200 Subject: [PATCH 082/123] chore(ds4v): drop research logs and evidence receipts Decision logs, the continuation plan and status notes, the quant85 artifact summary and the qualification evidence kit stay on the research branch. Co-Authored-By: Claude Fable 5.1 --- artifacts/ds4v-quant85/README.md | 67 -- decisions-ds4v-1.tsv | 10 - decisions-ds4v-2.tsv | 2 - decisions-ds4v-3.tsv | 5 - decisions-ds4v-continuation.tsv | 24 - docs/ds4v-continuation-status.md | 82 --- docs/ds4v-uncensored-vision-plan.md | 242 ------- harness/qualification/README.md | 2 - .../deepseek4/ds4v-vision/README.md | 97 --- .../ds4v-vision/capture-comparator-runtime.py | 27 - .../ds4v-vision/component-window-review.md | 24 - .../hip-attention-qualification.py | 314 --------- .../hip-linear-qualification-review.md | 40 -- .../ds4v-vision/hip-linear-qualification.sh | 248 ------- .../hip-lt-concurrent-qualification.md | 40 -- .../hip-lt-concurrent-qualification.py | 230 ------- .../ds4v-vision/hip-lt-qualification.sh | 264 -------- .../ds4v-vision/hip-lt-retry-qualification.py | 243 ------- .../ds4v-vision/hip-norm-qualification.py | 297 --------- .../ds4v-vision/hip-qualification-README.md | 38 -- .../ds4v-vision/hip-qualification.sh | 226 ------- .../ds4v-vision/hip-unbiased-qualification.py | 263 -------- .../deepseek4/ds4v-vision/how-backend.md | 19 - .../deepseek4/ds4v-vision/how-source.md | 17 - .../ds4v-vision/mmproj-byte-proof.py | 48 -- .../native-hip-scoped/comparison.exit | 1 - .../ds4v-vision/native-tower-brief.md | 27 - .../ds4v-vision/native-tower-rubric.md | 13 - .../ds4v-vision/patch-bias-diagnostic.py | 58 -- .../ds4v-vision/reference-fixtures.py | 65 -- .../ds4v-vision/runtime-qualification.sh | 51 -- .../compare-corn-three-way.py | 55 -- .../source-rocm-reference/constraints.txt | 4 - .../source-rocm-reference/cpu-runtime-info.py | 41 -- .../evidence/cpu-corn.exit | 1 - .../evidence/cpu-corn.log | 83 --- .../evidence/cpu-corn.time | 23 - .../evidence/cpu-runtime-info.stderr | 7 - .../evidence/cpu-runtime-private.stderr | 0 .../evidence/download.log | 19 - .../source-rocm-reference/evidence/freeze.txt | 15 - .../evidence/frozen-scripts.sha256 | 3 - .../evidence/install.log | 85 --- .../evidence/libtorch-hip-dynamic.txt | 53 -- .../evidence/libtorch-hip-ldd.txt | 45 -- .../evidence/libtorch-hip-private-ldd.txt | 46 -- .../evidence/miopen-apt-metadata.txt | 18 - .../evidence/miopen-apt-policy.txt | 6 - .../evidence/miopen-ldd.txt | 22 - .../evidence/miopen-repair.log | 4 - .../evidence/official-index.html | 140 ---- .../evidence/offloading.exit | 1 - .../evidence/private-library-path.txt | 1 - .../reference-freeze-policy-copy-race.log | 5 - .../evidence/reference-freeze.log | 8 - .../source-carrots-first-controller.log | 68 -- .../source-carrots-repeat-controller.log | 68 -- .../source-corn-repeat-controller.log | 68 -- .../evidence/torch-METADATA | 624 ------------------ .../evidence/torch-WHEEL | 5 - .../evidence/torch-url.txt | 1 - .../evidence/torch-version-static.txt | 10 - .../evidence/torch-wheel.sha256 | 1 - .../evidence/venv-config.txt | 5 - .../freeze-source-reference.py | 113 ---- .../hip-control-initial-failure.md | 17 - .../hip-control-report.md | 51 -- .../reference-stability-report.md | 59 -- .../target-hip-qualification-policy-review.md | 42 -- .../target-hip-qualification-policy.md | 33 - 70 files changed, 4934 deletions(-) delete mode 100644 artifacts/ds4v-quant85/README.md delete mode 100644 decisions-ds4v-1.tsv delete mode 100644 decisions-ds4v-2.tsv delete mode 100644 decisions-ds4v-3.tsv delete mode 100644 decisions-ds4v-continuation.tsv delete mode 100644 docs/ds4v-continuation-status.md delete mode 100644 docs/ds4v-uncensored-vision-plan.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/README.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/component-window-review.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh delete mode 100644 harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/how-backend.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/how-source.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/native-hip-scoped/comparison.exit delete mode 100644 harness/qualification/deepseek4/ds4v-vision/native-tower-brief.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/native-tower-rubric.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/patch-bias-diagnostic.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/reference-fixtures.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/runtime-qualification.sh delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-private.stderr delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md delete mode 100644 harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md diff --git a/artifacts/ds4v-quant85/README.md b/artifacts/ds4v-quant85/README.md deleted file mode 100644 index 36162ff2a..000000000 --- a/artifacts/ds4v-quant85/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# DS4V 80–85 GB candidate - -The requested target is an 80–85 GB model and at least 35 tokens/s for short chat, while retaining image input and the previously qualified 131072-token context capacity. **The 83.62 GB conversion is complete, but the candidate has failed the quality gate and reached only 20.1 tokens/s median. It is not qualified for daily service.** The original c76 service has been restored and verified at `http://127.0.0.1:8016/v1` on soulf; neither reduced model nor experimental runtime was installed. - -Conversion on `soulf` started 2026-09-06 at 05:07:59 UTC and completed successfully in 1 hour 41 minutes, including wrapper verification. The completed file is **83,619,648,416 bytes**, SHA256 `954433dcb2e64ce6082f4ea8c1478428198fd81b6e7ec0729e9eefa3d56f8497`. Full conversion receipts, completed-header checks and raw trial evidence are stored beside this document. - -## Candidate and conversion evidence - -- Verified complete file: **83,619,648,416 bytes / 83.619648416 decimal GB**. Original file: 113,745,874,400 bytes. This saves 30.126225984 GB, approximately 26.5% of the original file size. -- Expert gate/up: IQ2_XXS; expert down: IQ2_XS. Selected dense matrices: Q8_0. Vision, aligner, routing, normalization and related control tensors retain their source representations. -- Conversion reads the original FP4/FP8/BF16 safetensors directly. It does not requantize the older MIX GGUF. -- Importance calibration is explicitly **transferred text calibration**, not DS4V-specific calibration. The smaller candidate needs actual regression and image tests; byte correctness alone does not establish model quality. -- Source commit: `7e851cb81937fa92081d1b2e988af82b07d72575`. Converter SHA256: `afa56b0a60e7f883091ed669f8bad01439f9789fa7835184f736563075f84533`. -- Small conversion pilots were byte-identical across serial/parallel execution. The 17-expert pilot also verified the partial final worker batch with 8 and 16 workers. -- Full job limits: 16 CPU workers, 6 GiB cgroup memory maximum, no cgroup swap, six-hour runtime maximum. No GPU benchmarks run during conversion. -- Completed-file header verification passed: all 11 tokenizer keys and 33 architecture keys are identical; all 1641 tensor names/dimensions match. The 129 expert tensors and 346 selected dense tensors follow the new quantization policy; 1166 tensors preserve their type and encoded byte count. The parser's 23 CPU tests passed on soulf. Header validation and the separate full checksum do not establish model quality. - -Remote final target: `/home/marcelorm/ds4v-work/DeepSeek-V4-Flash-Vision-IQ85-v1.gguf`. -Remote conversion evidence: `/home/marcelorm/ds4v-work/image-integration/quant85-full-v1/`. - -## Frozen comparison and runtime sequence - -The existing model scored **15/16 for answer content and 7/16 for strict JSON formatting** on the fixed small task set. Raw responses are in `quant85-quality-baseline-v1/`; the content-scoring rubric was frozen before candidate inference. The candidate gate requires retaining every baseline-correct answer and at least the baseline strict-format score. This is a narrow regression check, not a broad quality benchmark. - -Three completed trials used the same candidate and unchanged c76 runtime: - -| Decoding | Expert cap | Actual hot experts/layer | Text tokens per sample | Median text decode | Median image decode | Content / strict score | -|---|---:|---:|---|---:|---:|---| -| AR | 1024 MiB | 3 | 243 / 245 / 261 | 14.7 t/s | 14.9 t/s | 12/16 / 5/16 | -| Fused AR | 1024 MiB | 3 | 239 / 251 / 263 | 20.1 t/s | 20.3 t/s | 12/16 / 5/16 | -| Fused AR | 4096 MiB | 14 | 267 / 247 / 253 | 20.1 t/s | 20.4 t/s | 12/16 / 5/16 | - -All 18 original functional cases passed in each trial, including image ordering and follow-ups. The appended quality gate then failed: `python-copy` and `nested-json` answered incorrectly, and `python-slice` gave the correct array inside an unrequested Markdown fence. The existing failing `python-loop` case remained wrong. No rubric was relaxed. Cache-eviction, SSE and long-context stages were **not run**, because the functional quality gate stopped each trial first. - -Each owned model exited cleanly with no safety breach, OOM or global swap-out growth recorded by the supervisor. Each post-stop idle-memory recovery timed out; the separately authorized bounded TTM cleanup and fresh admission were required between trials. These failed overall reports remain intact. No candidate was installed. - -The second candidate restores only the original BF16 embedding and output matrices while retaining the other 1639 IQ85 payloads byte-for-byte. Assembly and independent verification completed: **84,612,519,168 bytes**, SHA256 `99a2260c862e270fa654a1f1e75fad88ec824c58963a2c30135ba91edaf9bb2b`. Its first fused-AR trial passed arithmetic and color-image ordering/follow-ups, then failed the corn/carrot response-format assertion: correct labels appeared inside prose and a Markdown fence. This preserved failure stopped the trial before sustained throughput and the 16-task quality comparison. No sustained speed or quality improvement is established for this variant. The failed trial exited without a safety breach; bounded idle TTM recovery was again needed and completed separately. - -A separately versioned diagnostic probe captured benchmark and quality evidence without converting that failed functional result into a pass. The IOBF16 AR diagnostic completed with **19.9 tokens/s median text decode** (252/240/256 output tokens) and **20.1 tokens/s median image decode** (182/143/139 output tokens). Its content/strict scores remained **12/16 and 5/16**, with the same three baseline regressions. Restoring the two BF16 matrices therefore showed no quality or speed benefit on these checks. Raw evidence is in `quant85-candidate-iobf16-diagnostic-ar-v1/`. This new diagnostic uses identical fixed requests across its forthcoming AR/reference/batched comparisons; it is not paired with the older randomized benchmarks. - -The existing 10.65 GB draft completed its controlled reference diagnostic capture: text median **8.3 tokens/s**, image median **20.1 tokens/s**, and the same **12/16 content, 5/16 strict** scores. All 22 visible answers and their reported completion counts matched the paired AR run on identical requests (`quant85-iobf16-ar-reference-visible-comparison.json`). This is visible-text agreement, not token-ID parity: the HTTP API does not expose token IDs. No speculative runtime qualification is established. The target plus draft would exceed 85 GB of combined model storage; the target model itself remains below 85 GB. Batched verification completed at **14.0 tokens/s text**, **20.0 tokens/s image**, and **13/16 content, 6/16 strict**. Its text answers differ from the paired AR/reference lane, so this is not a verified equivalent speedup. - -A timing-enabled control reproduced all 22 visible answers and completion counts at the same 14.0 tokens/s text median. Increasing graph cache slots from two to four reduced measured graph build time from approximately 23–29 ms to 10–11 ms per verification step, while compute remained approximately 104–107 ms. Text throughput rose to **16.8 tokens/s**, still below AR and the 35 tokens/s target. All 16 quality answers matched the two-slot lane, but all three benchmark text answers changed. See `quant85-iobf16-cache-visible-comparison.json`; numerical or token-ID parity is not established. - -An isolated scoped Q4 MMVQ diagnostic was built on soulf at source commit `7fa6ad3ee6892c9b60faabba8159a252c6aac704`. It reproduced the old release binary and original graph object before compiling the patch; CPU unit tests and nine preparer tests passed. The existing source, build and release were unchanged. Its guarded GPU diagnostic completed at **15.8 tokens/s text** and **20.3 tokens/s image**, with the same **13/16 content and 6/16 strict** scores. This did not improve performance. Policy activation was logged, but no direct kernel-dispatch trace was captured. The model exited cleanly with no safety breach or observed global swap-out growth; idle TTM recovery was performed separately. This build is not qualified or installed. - -A source review found a separate correctness defect in cached speculative attention: preserved ring-row views used construction-time offsets while runtime write indices advanced. The isolated fix replaces those views with a gather driven by refreshed indices. Existing CPU units and ten preparer tests passed. Real guarded cache2/cache4 tests now produced **identical visible text and completion counts for all 22 identical requests**, compared with 19/22 before the fix. This verifies removal of the observed cache-size-dependent divergence on this set, not broad numerical parity. The fixed cache2/cache4 text medians were **14.1/16.2 tokens/s**; both retained the failing **13/16 content, 6/16 strict** scores. Only 16/22 visible replies match the earlier AR-equivalent reference lane. See `fused-preserved-ring-offset.patch`, its evidence notes, and `quant85-iobf16-ring-cache2-cache4-visible-comparison.json`. The fix remains isolated and is not installed in daily service. - -**No tested 80–85 GB candidate met the 35 tokens/s and quality requirements.** Candidate cache-eviction, SSE and long-context qualification were not run after the failed quality gate. The original c76 service is restored and verified; the smaller files and all failure evidence are retained. - -A future candidate must retain quality and meet the short-chat speed target, then complete cache-eviction, SSE, 8K/32K/64K/124K text/image qualification and actual daily-service acceptance. Every model trial retains strict admission and the original service fallback. - -The fixed cache sequence is 2K, 4K, 8K, 16K, 2K in one server process; it is separate from short throughput medians and from 124K qualification. The validation wrapper and pinned configuration generator passed CPU tests on soulf; these tests do not establish GPU or model behavior. - -See [PREPARATION.md](PREPARATION.md) for exact unchanged guard requirements and [runtime-performance-review.md](runtime-performance-review.md) for source-backed trial settings. [draft-compatibility.md](draft-compatibility.md) assesses an optional speculative draft, which was exercised only in unqualified candidate diagnostics and adds approximately 10.65 GB of model storage. Current image requests do not use that speculative path. - -Reducing file size alone does not establish 35 tokens/s. Report sustained generated-token counts, actual decode timings, image and context results, and memory observations before adopting the candidate as the daily service. - -## Final original-service restoration - -Restoration passed on 2026-09-06 after the final diagnostic exited and bounded idle cleanup reached the existing startup threshold. Exact runtime, source/config pins, arguments, environment, both GPU devices, namespace isolation, loopback listener and process identity passed the existing acceptance checker before and after workloads. The service remains active as PID `201518`, invocation `896aae1d96fb4f958323470b9d1e4508`, with zero restarts. - -All **14 text/image functional cases** and **four edge cases** passed. A fresh oversized request was rejected with HTTP 400; real SSE cancellation correlated with `finish=client_disconnect` in the same service invocation, followed by a successful request in **0.83 seconds**. The restored service generated **269 text tokens at 13.8 tokens/s** and **251 image-response tokens at 13.7 tokens/s** in this acceptance run. These are single samples, not paired medians against the smaller-model benchmarks. - -The service advertises 131072 total context tokens and 4096 default output tokens. The prior 124K text/image qualification for this unchanged c76 runtime and original model remains the relevant long-context evidence; it was not repeated during restoration. Final snapshots show zero owned-process VmSwap, OOM kills and kernel taint. The system-wide swap-out counter grew by 26,681,344 bytes during restoration; that is not attributed to this process and is not reported as zero. - -Full restoration evidence and hash-bound summary: `quant85-original-restoration-v1/acceptance.json`. The original model remains 113.75 GB. The size target was achieved experimentally at 83.62 GB, but **35 tokens/s with retained quality was not achieved**. diff --git a/decisions-ds4v-1.tsv b/decisions-ds4v-1.tsv deleted file mode 100644 index 2f8553d7d..000000000 --- a/decisions-ds4v-1.tsv +++ /dev/null @@ -1,10 +0,0 @@ -2026-09-04T06:12Z Created branch ds4v/baseline at origin/main 298031aa4222ec61c971ed834ec8f8829ce37a5c via git branch plus symbolic-ref Plain git checkout -b aborted because the sparse checkout keeps staged deletions; repointing HEAD avoids touching index or worktree -2026-09-04T06:21Z Authored docs/ds4v-baseline.md with PR 604 and blog numbers, no long dashes, no prose colons Task step 3 -2026-09-04T06:21Z Authored scripts/ds4v-baseline.sh with launch, doctor, chat-smoke, spec-flag, cleanup Task step 4 -2026-09-04T06:22Z Fixed spec-flag jq reading, replaced fallback operator with has() test because jq collapses JSON false to the fallback Found by stub server test returning spec_decode_ran false -2026-09-04T06:22Z Validated doctor, chat-smoke, spec-flag, cleanup against local stub servers on ports 8216 and 8217, bash -n, help path, missing-file path, unknown subcommand path Task step 5 -2026-09-04T06:23Z Committing only docs/ds4v-baseline.md and scripts/ds4v-baseline.sh via pathspec commit Sparse checkout index holds staged deletions that must not enter the commit -2026-09-04T06:26Z Committed f27aefc on ds4v/baseline with only the two assigned files, pathspec commit kept sparse checkout staged deletions out Task step 6 -2026-09-04T06:26Z git push to origin denied, 403 for marcelormendes on both SSH and HTTPS, saved format-patch to /tmp/ds4v-program/ds4v-1.patch Task step 6 fallback -2026-09-04T06:28Z Asked supervisor, chose between fork push PR and patch only handoff Supervisor approved fork route -2026-09-04T06:29Z Pushed ds4v/baseline to fork marcelormendes/lucebox and opened ready PR 695 against Luce-Org/lucebox main Supervisor decision, option A diff --git a/decisions-ds4v-2.tsv b/decisions-ds4v-2.tsv deleted file mode 100644 index 33b38885e..000000000 --- a/decisions-ds4v-2.tsv +++ /dev/null @@ -1,2 +0,0 @@ -2026-09-04T06:27:03Z created decisions log for DS4V-2 owner pass, kept uncommitted by design -2026-09-04T06:29:42Z manifest-auth HF token absent and parent repo is gated, supervisor picked optional HF_TOKEN bearer plus public API shard listing and counts pending on operator machine diff --git a/decisions-ds4v-3.tsv b/decisions-ds4v-3.tsv deleted file mode 100644 index b4b6a8cb2..000000000 --- a/decisions-ds4v-3.tsv +++ /dev/null @@ -1,5 +0,0 @@ -2026-09-04T07:05Z branch created ds4v/quant from origin/main 298031aa4222ec61c971ed834ec8f8829ce37a5c via git branch plus symbolic-ref, sparse checkout staged deletions left in index untouched -2026-09-04T07:20Z verified-tool-surface read master llama-quantize usage and src/llama-quant.cpp from github, confirmed --tensor-type takes regex tensor name plus ggml type, so the recipe uses per class overrides instead of multi pass requant -2026-09-04T07:45Z proof bash -n clean, help and plan exit 0 printing pin 9400c894, verify --help exit 0, jq empty clean, python budget recompute shows IQ2_XXS at 104.0 GiB with 48.0 margin against 152 and 92.7 on one 96 GiB card at 1M context, dash grep clean on all three files Task step 6 -2026-09-04T07:52Z stub-tests stub llama.cpp proved b10763 refusal exit 1, pinned 9400c894 run executes the exact quantize command, verify passes at 43 bias_vl and fails at 42 Task step 6 -2026-09-04T07:55Z commit committing only docs/ds4v-quant.md, scripts/ds4v-quant.sh, share/model_cards/ds4v-vision.json via pathspec, sparse checkout staged deletions and DS4V-1 plus DS4V-2 staged files must stay out Task step 7 diff --git a/decisions-ds4v-continuation.tsv b/decisions-ds4v-continuation.tsv deleted file mode 100644 index 257002bdb..000000000 --- a/decisions-ds4v-continuation.tsv +++ /dev/null @@ -1,24 +0,0 @@ -ts phase decision why evidence result -2026-09-04T21:20:31Z fitter Repair duplicate centers on the BF16 grid and stamp affected codebooks Original fitter aborted on a degenerate source expert; preserve the existing Lloyd fit 07e3284; artifacts/ds4v-fitter-fix/red.log; artifacts/ds4v-fitter-fix/green.log; artifacts/ds4v-fitter-fix/replay-42-164.log Unit, smoke, and expert replay pass; full conversion running -2026-09-04T21:20:31Z isolation Use separate local and soulf worktrees; stop recorded baseline PID3263851 Original Mac index has pre-existing deletions; soulf baseline test occupied memory 07e3284; soulf:lucebox-ds4v-mix-fix/artifacts/fitter-fix/cleanup-baseline.txt Original checkout files preserved; test PID exited;8016not touched -2026-09-04T21:32:21Z vision-reference Run the parent image processor, tower, and aligner on both supplied photos Need numerical ground truth for the C++ vision implementation artifacts/ds4v-step2/reference-manifest.json; artifacts/ds4v-step2/reference-run.log CPU reference produced finite embeddings and position-dependent layouts for carrots and corn -2026-09-04T21:45:09Z architecture Keep the small request payload and extend the existing hybrid graph Avoid duplicating the decoder or migrating unrelated backend APIs artifacts/ds4v-step2/design-a.md; artifacts/ds4v-step2/design-b.md; artifacts/ds4v-step2/design.md Selected after independent same-family review; component and image HTTP gates remain open -2026-09-04T21:58:41Z conversion Full calibration passed the prior failing expert BF16 epsilon separation fixed the actual full-run collision without changing fitting weights artifacts/ds4v-fitter-fix/full-run-repair.log PASS calibration; encoding and load proof pending -2026-09-04T22:09:12Z projector Verified lossless standalone projector at ef64f62 Native GGUF parser and independent original-byte comparison qualify the runtime input artifacts/ds4v-step2/mmproj-proof/byte-proof.json PASS 12 tests, 267 tensors and 932786176 payload bytes -2026-09-04T22:55:05Z arena Select A as experimental runtime base; keep numerical ISSUES Independent review scored A20/25 B16/25; both share unchanged corn failure artifacts/ds4v-step2/native-tower-crossjudge.md; ds4v/vision-runtime5bf705e Complete token budget fixed with remote red/green; maximum grid finite and observer invariant; no parity or GPU claim -2026-09-04T23:13:41Z implementation Accept independent image policy and transport units Source fixture policy parity and remote transport red-green; independent transport review resolved depth and placeholder findings ds4v/vision-policy5da9272; ds4v/vision-transport25f6105; artifacts/ds4v-step2/transport-review.md Unintegrated CPU units PASS; no HTTP/GPU/model behavior claim -2026-09-04T23:26:54Z step2 HIP tower probe build passes Reuse server HIP compatibility definitions for standalone gfx1100 and gfx1151 qualification; GPU execution remains after text proof artifacts/ds4v-step2/runtime-proof/build-hip-compat.log PASS build only at 4bf7270; numerical qualification remains ISSUES -2026-09-04T23:35:57Z step2 Isolate pure prompt preparation from tower integration Uses accepted preprocessing core only; final token expansion, checked admission and owning spans can be verified without unqualified tower or codec artifacts/ds4v-step2/prompt-preparation-brief.md Writer ds4v/vision-prompt; tests-first remote CPU red/green required -2026-09-05T00:03:00Z step2 Accept CPU image preparation composition Verified units compose through actual renderer/tokenizer with exact source patches/layouts and interaction negatives; tower remains separate artifacts/ds4v-step2/cpu-composition-review.md PASS ds4v/vision-cpu0065158; no HTTP/backend/GPU integration -2026-09-05T00:03:00Z step1 Prototype byte-preserving parallel expert encoding Single-core full run is substantially slower than handoff estimate; independent review found bounded immutable expert tasks feasible artifacts/ds4v-fitter-fix/parallel-encoding-brief.md Prototype only; current full run preserved; complete17expert byte comparisons and speed evidence before another full run -2026-09-05T00:21:39Z implementation Launch distinct eight-worker full MIX candidate after acceptance Original serial run takes substantially longer than handoff estimate; exact codec and five-lane output identity with 3.9x sample speedup justify bounded concurrency ds4v/mix-parallel 1a38b984; artifacts/ds4v-fitter-fix/mix-parallel-review.md; soulf PID3333730; conversion.started 2026-09-05T00:20:05Z ACTIVE; original PID3289986 untouched; text load and vision runtime still unproven -2026-09-05T00:26:40Z verification Prepare isolated original-source ROCm control without GPU execution CPU native drift remains unresolved; eventual same-device source comparison can distinguish backend arithmetic from graph mismatch artifacts/ds4v-step2/rocm-source-reference-options.md; precommitted AMD torch2.10 rocm7.2.4 investigative build Preparation only; immutable CPU fixtures and system ROCm preserved; no version sweep or gate change -2026-09-05T00:41:37Z verification Accept isolated source ROCm environment and CPU portability control One fixed AMD Torch build plus private matching MIOpen now preserves original corn CPU outputs exactly, enabling later same-GPU diagnostic comparison artifacts/ds4v-step2/source-rocm-reference/report.md; source-rocm-reference-review.md; runner17ba9d66; CPU feature/embedding hashes identical Scoped PASS; source HIP and native acceptance NOT_QUALIFIED; fixed gate unchanged -2026-09-05T01:21:27Z checkpoint Allow standalone vision GPU qualification during CPU quantization User reaffirmed working on vision now; verified projector/source fixtures make the standalone tower independent of the unfinished text artifact. Existing text-first chat milestone stays; idle pair and operator protection are enforced User follow-up; KFD empty; operator inactive PID0; hip-qualification.sh component-only mode with double idle/resource gate Pending harness review before GPU execution; no full image chat before text proof -2026-09-05T01:31:57Z verification Execute independently guarded native HIP component qualification Standalone projector is verified and pair is idle; preserve full text-first chat milestone component-window-review.md; native-hip-first/summary.json; harness152af330 Execution PASS; fixed features and embeddings ISSUES/exit3; corn repeat byte-identical; targeted biased-linear rounding hypothesis under investigation -2026-09-05T01:37:12Z qualification-policy Adopt prospective same7900XT original-source reference with unchanged numeric thresholds Original source HIP itself fails CPU feature threshold; native also fails against source HIP; controlled target fidelity requires target reference target-hip-qualification-policy.md 62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f; two independent reviews of substantive policy7edde20e Policy accepted; reference repeat/freeze and corrected candidate qualification still pending; all CPU failures retained -2026-09-05T01:44:59Z reference-freeze Freeze first original-source HIP outputs after exact repeat stability Same-target baseline must precede corrected native full-tower output canonical677b5ef0; freeze8ab35a8a; source-rocm-reference/reference-stability-report.md PASS source stability both images; native candidate NOT_EVALUATED; CPU corn feature portability ISSUES retained -2026-09-05T02:02:01Z conversion Accept completed parallel MIX artifact for private load proof All129 tensors and internal structural/sidecar/raw-byte verification pass; complete2filemanifestpublishedbeforeexit0 parallel-run/conversion.sha256; GGUF58086fcd; GUMIX954110a5 Conversion PASS; private text load active; original serial preserved until text verdict -2026-09-05T02:02:01Z vision Keep scoped rounding fix but preserve full tower ISSUES Tiny source-grounded regression passes and CPU outputs staybyteidentical; both target embeddings nowpass but featuresstillfail be8b0f1; biased-linear-rounding.md; native-hip-scoped/summary.json Implementation scopedPASS; targettowerexit3; runtime integration remains blocked -2026-09-05T02:08:07Z text-load Preserve failed first chat proof and fix BF16 RMS affine compatibility Model loads acrossbothGPUs but firstchat asserts GPUbinarybroadcast src1 type; actualnormvectorsBF16 load-first/server.log; load-first/harness.exit52; PID3380377gone/KFDempty Load/topologyPASS; chatFAIL; isolated narrowgraphfix authorized; operator remainsdown untilsuccessfulproof -2026-09-05T02:42:20Z text-load Accept full MIX text proof with narrow BF16 norm fix All six requests succeed; exact math and triplicate longer145-token replies pass with speculative decoding true 7071946; binaryc32e5ae3; load-bf16-pass/verdict.json; harness.exit0; PID3396669gone/KFDempty Text PASS; short decode15.9/16.4/17.4tps; old operator as-is restoration next; vision still unqualified diff --git a/docs/ds4v-continuation-status.md b/docs/ds4v-continuation-status.md deleted file mode 100644 index 5f1dba410..000000000 --- a/docs/ds4v-continuation-status.md +++ /dev/null @@ -1,82 +0,0 @@ -# DS4V continuation status - -Current authority is the September 4 operator handoff. Earlier quant recipes, gating blockers, and throughput targets in ds4v-uncensored-vision-plan.md are historical. - -## Completed units - -The adaptive-codebook failure is fixed on fork branch `ds4v/mix-converter` at `07e32844cc32602bab8167072e9b301eb832bfe2`. Tests reproduced the failure before the fix. The repaired fitter passes degenerate-input tests, preserves distinct BF16 levels, and passed the actual layer 42 expert 164 replay. The one-layer, one-expert converter smoke passed. The full run crossed the former failure and stamped one `bf16-epsilon-v1` repair. Plain MSE fitting and the uniform imatrix remain in use. - -The standalone projector exporter is verified on branch `ds4v/vision` at `ef64f62fdc9aca1202ef58600d020c42d8e4c1b0`. Twelve tests pass on soulf without skips. The native gguf.cpp reader accepted the file, and an independent reader compared all 267 tensor names, shapes, BF16 types, and 932786176 payload bytes against the parent. The artifact is `~/ds4v-work/ds4v-mmproj.gguf` on soulf, SHA256 `58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c`. Independent source review passed. - -## Current work - -Current checkpoint: full MIX conversion and new-model text chat PASS. Existing operator8016 remains PID3401443, active/running with NRestarts0 and its owned listener; no test request was sent to it. Direct original-source patch/QKV GPU calculations match exactly. Native HIP-only operation6137f430 passes clean CPU/HIP builds and preserves all four previous CPU image outputs. The first reviewed concurrent tiny RED attempt failed before HIP initialization completed: a monitor allocation-field parse error triggered owned-child cleanup, and the kernel logged an AMD XDNA NPU driver NULL dereference during that interval. PID3414758/start50217527 remains in uninterruptible `amdxdna_drm_close` with KILL pending; cleanup is incomplete. GREEN/full-image lanes did not launch. All further GPU work is held. A separate monitor repair handles standard MiB counters, preserves parse/error diagnostics, and bounds post-KILL waiting; all115 CPU tests pass. Restart approval was requested because restarting soulf interrupts the protected server. Evidence: `artifacts/ds4v-step2/native-lt-concurrent-first/` and `radeon-numerical-guard-recovery/`. Vision serving remains unfinished. - -Full conversion and the corrected full-model text proof pass. Vision features remain unqualified. The original serial converter PID3289986 was terminated only after text PASS, using its exact command, executable and start ticks47893724 with a PID file descriptor. Its partial output is preserved. The successful parallel artifact is authoritative for subsequent testing. - -The first supervised as-is operator restoration failed safely. Original PID3398634 reached its91.1GiB managed-memory allocation, then sustained memory pressure without log/process-I/O progress: PSI approximately50–74%, swap1.2→13.1GiB, and available memory approximately1.1GiB after61seconds. No OOM or automatic restart was observed. The guard stopped the one service it started and confirmed inactive/MainPID0; evidence is `artifacts/ds4v-fitter-fix/operator-restoration-first/`. After cleanup KFD was empty and available memory recovered to42GiB. The original unit/profile/binary are unchanged. Read-only diagnosis precedes any further restoration attempt; no reset, cache drop or configuration workaround is authorized by this failure. - -Observed single-core encoding is substantially slower than the handoff's three-hour estimate. The bounded optional encode-threads1..8 prototype on separate `ds4v/mix-parallel` is frozen at `1a38b984cfdc51f6bc83acc366e3543ab4192498`. Calibration, repair, row quantizer arithmetic, recipes and metadata are unchanged. All three suites and actual42/164 fitter replay pass. Seventeen actual experts produce byte-identical complete GGUF+GUMIX files with old/default/1/8/repeat8. The eight-worker sample takes10.8seconds versus42.2seconds serially, about3.9x end-to-end and5.15x during encoding, with153888KiB peak RSS. This shares the machine with the original conversion and is not an isolated benchmark. Independent source and corrected launch-wrapper reviews pass. - -A separate full eight-worker candidate started at2026-09-05T00:20:05Z: wrapperPID3333717, converterPID3333730, output `~/ds4v-work/DeepSeek-V4-Flash-Vision-Uncensored-ROCmFPX-MIX-parallel.gguf`, evidence `~/lucebox-ds4v-mix-parallel/artifacts/fitter-fix`. The wrapper pins source and binary, verifies the qualification manifest's config/tokenizer/index and uniform-imatrix hashes, records both PIDs, and publishes conversion.exit only after the complete two-entry output checksum manifest. Calibration remained serial. This candidate has completed; both recorded processes have exited. - -The parallel candidate completed calibration in1999.04seconds, with exactly the expected layer42/expert164 down repair stamp, and has now finished all129 expert tensors. Its internal verifier passed qtypes/bounds, exact sidecars and raw pass-through bytes. Conversion wall time was1:34:34, peak RSS168996KiB. The wrapper subsequently finished the complete two-file checksum manifest and published conversion.exit0. Its first64MiB matches the original candidate byte for byte, SHA256 `f13168d5f45282b39451b6756184ab7bcd8dd20f91733d83969b2d90c27cf88c`. This is a bounded prefix comparison, not whole-artifact verification. Initial full expert tensors take about25–34seconds each. Evidence: `artifacts/ds4v-fitter-fix/parallel-run/prefix-comparison.json`. - -Both isolated native tower candidates completed CPU qualification. Independent review selected A as the experimental base,20/25 versus16/25, while keeping the numerical verdict ISSUES. Selected branch `ds4v/vision-runtime` at `4bf7270` fixes the complete N-layout token budget and retains a machine-visible failing numerical gate. Functional loader, arithmetic, original-image shape/finite checks, and maximum permitted3366-patch execution pass. Peak measured scratch is891424512bytes with diagnostic snapshots; snapshots do not change outputs. Corn feature cosine remains0.99822935 against the fixed0.9995 minimum. - -The first projection diagnostic shows adjacent-BF16 accumulation sensitivity without detecting a layout/formula bug, but does not explain every source-kernel outcome or prove harmless end-to-end error. The completed sensitive-block diagnostic found no semantic or BF16-boundary discrepancy in corn blocks12/31. A single original-source corn one-thread control reproduced the two-thread reference bitwise; native drift remains unresolved and the gate is unchanged. - -The native HIP qualification harness now supports an independently released `--component-only` window. Source review passed; it checks operator inactivity, free ports, no KFD compute processes and at least 8GiB available host/discrete memory before execution. Current harness SHA256 is `152af330bb37f77f8e6f9c795ae46c12f787e8587c42fc210aeb671b205296d5`. This changes component scheduling only; full image chat still follows text proof. - -The first native HIP qualification executed on the 7900 XT at unchanged runtime `4bf7270`. All three image probe processes completed, produced finite outputs and repeated corn byte-identically. Both unchanged numerical comparisons failed with exit3: corn feature cosine0.985939 and carrots0.993884, below0.9995; embeddings also failed their cosine gate. Evidence is `artifacts/ds4v-step2/native-hip-first/`. Standalone encode times were0.279s corn and0.716s carrots, including the probe's transfers; these are not full chat or warmed throughput measurements. - -The exact dyadic micro-regression confirmed a biased-linear rounding defect: GGML's nonbatched BF16 HIP GEMM returns BF16 even when F32 precision was requested, before the tower adds bias. The scoped fix at `be8b0f1b07f1a3a034ce1d7333fd0d3402754c60` promotes biased weight operands only on GPU backends. Tiny HIP biased errors fall from528 to0. The unchanged unbiased output exactly matches original Torch HIP, including256 negative tie cases that differ from an abstract nearest-even oracle. The test now checks the actual unmodified product-rounding contract and retains those diagnostic differences. Direct fused source biased output still differs in88 tiny tie cases; no bitwise fused-linear claim is made. - -The first broader F32-weight candidate8bec967 regressed CPU numerics; the scoped fix avoids that regression. All four CPU corn/carrots outputs now match pre-fix4bf7270 byte for byte. CPU tiny, geometry and16 loader cases pass; original CPU corn feature ISSUES remains. The scoped implementation and harness passed independent review. See `artifacts/ds4v-step2/biased-linear-rounding.md` and `vision-linear-rounding-review.md`. - -The full scoped target-HIP run completed at `soulf:~/lucebox-ds4v-linear-rounding/artifacts/hip-target-scoped`, with metadata/logs copied to `artifacts/ds4v-step2/native-hip-scoped/`. Both embeddings now pass against frozen same-GPU source (corn cosine0.999525047, carrots0.999721786). Features still fail: corn maxabs0.947265625 and cosine0.999063593; carrots maxabs0.26416015625 (cosine0.999538399 passes). All outputs are finite and corn repeat is byte-identical. Harness exit3 is preserved. The tower dependency remains blocking; no production HTTP/backend vision wiring or image-chat acceptance is claimed. - -The independent original-source ROCm environment at `~/ds4v-work/source-rocm210-reference/.venv` uses one preselected AMD Torch2.10/ROCm7.2.4 build and privately extracted matching MIOpen, without system installation. Its CPU corn control reproduces frozen features and embeddings byte for byte. The first source HIP attempt stopped before weights because of an exact marketing-name guard. An identity-only query confirmed the same7900XT/gfx1100/20464MiB and justified a two-line runner correction: print actual identity and match `Radeon RX 7900 XT`. Corrected runner SHA256 is `cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`; original runner/evidence remain intact. - -The corrected original-source corn HIP forward completed successfully, but features fail the unchanged CPU gate (cosine0.997729789, maxabs2.73828125); embeddings pass (cosine0.999115332). NativeHIP also fails against sourceHIP (features0.991196939, embeddings0.994697537), so CPU/GPU portability does not explain away the native discrepancy. All three comparisons are retained in `source-rocm-reference/hip-supervision-confirmed/three-way.json`. Source GPU peak allocation was1176237056bytes; the lane released its GPU allocations after exit. - -A prospective GPU-only policy passed two independent reviews and was adopted in `artifacts/ds4v-step2/target-hip-qualification-policy.md`: freeze stable original-source corn+carrots outputs on the same7900XT before corrected native full-tower execution, apply every existing numerical threshold unchanged, and retain CPU portability failures separately. The immutable CPU reference, system ROCm, source graph and precision remain unchanged. No alternative-version sweep or tolerance change occurred. - -Native preprocessing is accepted at `ds4v/vision-preprocess` `edb3b0e15d3e73b5408d8fbb13532fe40fc9fefb`. Source RGB/resize/BF16/layout fixtures pass. Decoder regressions first failed at a28abfd; 6d28845 fixes bounded PNG IDAT inflation, Pillow-compatible grayscale16 and explicit unsupported CMYK/YCCK, and edb3b0e corrects the pinned license notice. Original ten fixtures and three reviewer fixtures pass, with independent review PASS. UBSan covers the C++ wrapper and LodePNG, not the external libjpeg C build. The earlier RGB core combined implementation and tests; no tests-first RED evidence is claimed for that earlier commit. Decoder total memory includes inflate/raw/interlace buffers beyond its two RGB buffers. - -Pure prompt preparation at `ds4v/vision-prompt`859f2f7 passes recorded RED/GREEN, exact source comparisons for ten single-image cases and both image orders, and independent review. The accepted components are composed at `ds4v/vision-cpu`0065158, with recorded composition RED/GREEN and independent review PASS. The probe joins actual data-URL extraction, renderer/tokenizer, codecs, preprocessing and expansion, with real-image source comparisons, Jinja/cardinality controls, context/errors/redaction and same-layout pixel isolation. It uses an explicit probe text adapter, not HttpServer normalization. HTTP image input, decoder image visibility, and image expert routing are not yet integrated into the server. - -The selected design is in `artifacts/ds4v-step2/design.md`. It keeps monolithic asymmetric expert parallelism and extends the existing sparse layer-major graph. Sparse prefill remains approximate. No complete Torch decoder-logit parity is claimed. - -`artifacts/ds4v-step2/integration-touchpoints.md` maps the actual request, HTTP, lifecycle, cache, routing, raw-mask and chunking seams at4bf7270, including unsupported-path guards and ordered validation. This is read-only preparation; it does not clear the tower gate or implement runtime integration. - -Independent image policy is verified at `ds4v/vision-policy`5da9272:120 image expert IDs and433562 raw visibility pairs match source exactly; maximum weight error5.96e-8. Bounded image data-URL transport is verified at `ds4v/vision-transport`25f6105, with remote red/green and independent review. Neither unit is wired into the server. The optional HIP tower probe built successfully on soulf from `ds4v/vision-runtime`4bf7270 for gfx1100 and gfx1151, using the existing server HIP compatibility definitions. The first standalone GPU execution completed with numerical ISSUES, as recorded above. - -## Load and operator restoration - -The private text load harness is `artifacts/ds4v-fitter-fix/load-proof.sh`, copied to `/tmp/ds4v-load-proof.sh` with `/tmp/ds4v-load-validate.py` on soulf. Conversion is complete. The first private parallel text proof, `load-proof-20260905T015809Z-IYNiSF`, verified both full checksums and loaded the model across both GPUs. The first chat request aborted at `ggml-cuda/binbcast.cu:414`, which rejects a BF16 second operand. Harness exit52 and own-PID cleanup are recorded in `artifacts/ds4v-fitter-fix/load-first/`; PID3380377 is gone and KFD is empty. No chat verdict passed. Its optional `serial|parallel` selector couples the candidate model and evidence root, while retaining the same original launch profile; default is serial. Both the original harness and this narrow selector pass independent review. It requires and verifies exact target/sidecar checksum records, uses a fresh evidence directory indexed by `artifacts/fitter-fix/load-proof.latest`, pins private8217 and the intended child topology/workaround environment, checks actual HIP device order and expert ownership, and validates triplicate exact math and longer deterministic speculative replies. It records valid usage/timings, binary/draft hashes and final exit status, then cleans up its own PID. Six validator CPU tests pass. Timing results will be short load observations, not a warmed benchmark. - -Current harness SHA256 is `da2a424317885376017c866674df7f77642427ec6d843db2a8aa07432838f1f6`. Its added load-slot guard checks that the operator is inactive/failed with PID0 and both8016/8217 are free, before evidence creation and again immediately before launch. Listener-query failure rejects the run. Independent review and9/9 CPU mock cases pass; the first full execution failed on first chat and cleaned up private8217. A clean isolated text compatibility fix is being prepared from298031aa. - -Operator8016 is restored. Its unchanged user unit is `deepseek-dflash.service`, with ExecStart `~/lucebox-0731-main/run/serve-ds4-0731-mix-merged.sh`; it serves the older Strix-only model at128K. The first attempt stopped under memory pressure. A richer second observation demonstrated real allocation/upload progress and completed main-model initialization, but reached its300-second deadline before listener readiness. Cleanup naturally reclaimed retained memory, leaving111GiB available. The final attempt required at least110GiB available in all three pre-start snapshots and retained every runtime/cleanup guard. It passed: PID3401443, active/running, NRestarts0, correct gfx1151-only initialization and owned8016 listener. Evidence is `artifacts/ds4v-fitter-fix/operator-restored/`; both unsuccessful attempts remain preserved. No reset, dropped cache, model/profile change, or operator chat request occurred. - -The user was asked whether to restore 8016 after text proof or keep the pair available through vision qualification. Until a reply changes the instruction, the handoff requires restoration after text proof. Full-model image lanes need a separate GPU window once the operator service is restored. - -## Evidence - -- `artifacts/ds4v-fitter-fix/` contains fitter red/green logs, actual-expert replay, converter smoke, full-run repair stamp, memory diagnosis, and the private load harness. -- `artifacts/ds4v-step2/mmproj-proof/` contains the projector manifest, output hash, native inventory, and tests. -- `artifacts/ds4v-step2/reference-manifest.json` and `routing-mask-manifest.json` index parent CPU fixtures stored only on soulf. -- `decisions-ds4v-continuation.tsv` records completed decisions and their evidence. - -No GPU execution, model download, or native build has run on this Mac. The original worktree index remains untouched. All fork pushes use origin; no upstream push occurred. - -The original-source target references are now frozen at canonical manifest677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86 and freeze/provenance8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0. Both features and embeddings repeat byte-identically for both images. FIRST outputs are retained. Source corn CPU feature portability remains ISSUES; source carrots features/embeddings and corn embeddings pass their CPU gates. See `source-rocm-reference/reference-stability-report.md`. - -Completed parallel artifact:113745874400bytes (GGUF), SHA256 `58086fcd38a57338d0f6ac50466ce4bc9cae1a2e0faebe975c22fee3061b97be`; GUMIX376696bytes, SHA256 `954110a5169fedf06a6973604a20242e9473a8be7661c1bfe2e3195680f50a2a`. Final manifest and exit0 are copied in `artifacts/ds4v-fitter-fix/parallel-run/`. The live text load measured12.88GiB dense/core on7900XT, leaving room for6.07GiB hot experts; future vision-enabled placement must reserve projector weights plus scratch before choosing its hot budget. - -Text compatibility fix: actual model normalization vectors are BF16, while HC parameters, router biases and attention sinks are F32. The DS4 RMS graph passed the BF16 norm vector directly to a GPU binary multiplication that accepts only F32/F16. Isolated branch `ds4v/text-bf16-affine` at `707194695703c023a8bf026684102d7c597d15b6` casts only BF16 affine vectors to F32 in the graph, preserving stored model bytes and dense/expert matrices. Real HIP RED reproduced the assertion; GREEN executes both tested token shapes with maximum error below 2.2e-7. CPU execution and F16/F32 graph-preservation checks pass. Independent review passes. - -The clean full server build completed on soulf at `/tmp/ds4-text-bf16-server-build/dflash_server`, SHA256 `c32e5ae32da82cde1c8aab61e26c693dbc5b3679181b55b7483ae9312d64fca0`. The pinned retry harness `load-bf16-proof.sh`, SHA256 `336bc3816553fbf000c40a67997cea757c93c9f13c9f7386e760c5d200da9d2e`, passes independent review, shell syntax, and all 11 load-slot guard mocks. Private full text retry `load-proof-20260905T023749Z-Vc1jid` completed with exit0 and all acceptance checks PASS. Three math replies returned exactly4; three longer145-token replies were byte-identical with speculative decoding true and acceptance0.6610. Their short decode observations were15.9/16.4/17.4tokens/s, not a warmed benchmark. The private PID3396669 exited and KFD was empty after cleanup. Complete evidence is copied to `artifacts/ds4v-fitter-fix/load-bf16-pass/`. Main checkout/binary and operator configuration remain untouched; supervised as-is operator restoration is next. - -The direct hipBLASLt synthetic biased projection matches the original GPU source output exactly (1024/1024 values), using the source's fixed first-heuristic, 76 MiB workspace configuration. Two preselected real corn projections are prepared for separate guarded execution: patch projection and block-0 QKV. Their capture must reproduce the frozen original full-tower outputs before either fixture is accepted. No production operation or image-chat support is implied by this small test; see `artifacts/ds4v-step2/hipblaslt-real-projections/README.md`. diff --git a/docs/ds4v-uncensored-vision-plan.md b/docs/ds4v-uncensored-vision-plan.md deleted file mode 100644 index acba93152..000000000 --- a/docs/ds4v-uncensored-vision-plan.md +++ /dev/null @@ -1,242 +0,0 @@ -# DS4V uncensored vision GGUF for Strix Halo plus 7900XT plan - -Build an uncensored DeepSeek V4 Flash Vision GGUF that runs on the operator pair. -Start from the OrcaRouter abliterated parent with vision intact. -Quant with the prometheusAIR imatrix recipe for a 128 GiB plus 24 GiB budget. -Serve with asymmetric expert parallelism per the Lucebox report. -PR ids in order. DS4V-1 then DS4V-2 then DS4V-3. -Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. -Style rules. i dont want any abstract metaphors. Write like hemingway. - -## How to read this - -One box is one unit of work. Every box names the evidence that checks it. A nested box is a substep of the box above it. Check a box only when its evidence exists. A file. A log line. A test run. Or a SHA. -The program runs `playbooks/autopilot-stack.md`. The root builds the chain. The operator lands it with her own clicks. -Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. -Style rules. i dont want any abstract metaphors. Write like hemingway. - -## Program checklist - -### Arm the program - -- [ ] State the protocol and this plan to the operator, then stop. Start execution only on her explicit go. -- [ ] On her go, adopt the run objective with this exact text. "`docs/ds4v-uncensored-vision-plan.md`, DS4V-1 then DS4V-2 then DS4V-3, Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked., the root builds the chain and the operator lands it, done when every box is checked with its evidence." -- [ ] Read these from trunk at program start. Re-read them at every tick. - - [ ] `git show origin/main:skills/poteto-mode/playbooks/autopilot-stack.md` - - [ ] `git show origin/main:skills/swarm/SKILL.md` - - [ ] `git show origin/main:.pi/skills/verify-lucebox/SKILL.md` - - [ ] `git show origin/main:skills/poteto-mode/playbooks/opening-a-pr.md` - - [ ] `git show origin/main:skills/how/SKILL.md` -- [ ] Arm the 30-minute audit tick as a bash polling loop with an explicit iteration cap. Never leave the cadence to memory. -- [ ] Use this tick prompt, verbatim. "Re-read the execution playbook from trunk and the run objective. Audit the operation against both and fix drift in this tick. Probe every active lane and judge progress by side effects only. Stand down a stuck lane and dispatch its replacement now. Then send the operator a status message, whether or not anything changed, with the queue table of PR, owner, state, and head SHA, the verdicts since the last tick, what merged, open operator gates, and blockers." -- [ ] On the operator hold or stand down order, send every owner a zero writes order at once. - -### Run owner passes - -- [ ] Run one owner pass per PR with the full lifecycle the execution playbook names. -- [ ] Follow this dependency graph. Start dependent work only after its parent merges. - - [ ] DS4V-1 and DS4V-2 are independent and first. Both branch from `main`. - - [ ] DS4V-3 after DS4V-1 and DS4V-2. -- [ ] Hold the file boundaries. DS4V-1 touches only `docs/ds4v-baseline.md` and `scripts/ds4v-baseline.sh`. DS4V-2 touches only `docs/ds4v-source.md` and `scripts/ds4v-fetch-source.sh`. DS4V-3 touches only `scripts/ds4v-quant.sh` and `docs/ds4v-quant.md` and `share/model_cards/ds4v-vision.json`. -- [ ] Hold the review gate. No PR changes an interaction. All three stop at merge ready without an operator media review. - -### PR mechanics, for every PR - -- [ ] Resolve the forge once. Default to `gh`. If `command -v origin` succeeds and Origin can resolve the repository, use `origin pr` for every PR operation. Record any fallback to `gh`. Never require `gt`. -- [ ] Open the PR ready, never draft, with `origin pr create --status open --base main` or `gh pr create --base main` according to the resolved forge. A stack child targets its parent branch. -- [ ] Run the repo lint and typecheck once before the PR facing push. Push with hooks on. -- [ ] Run `/unslop` over the diff before each commit and `/no-comments` before review. -- [ ] Triage every Bugbot and security reviewer comment per `../references/bugbot-triage.md`. -- [ ] Rebase onto current trunk before babysit and again before the merge ready report. - -### Verdict and merge, for every PR - -- [ ] At the merge ready head SHA, run the swarm per `skills/swarm/SKILL.md`. One gates lane. The ten live lanes from the PR Verify live block. The perf lane from its Verify perf block. One audit lane that reads the diff and the receipts and distrusts the PR body. -- [ ] Clean only when every lane is `PASS`. Findings go back to the owner. A new head gets a fresh swarm and a fresh verdict. -- [ ] The root appends each clean PR to the one linear base branch stack and the operator lands it bottom up. A rebase that changes a patch id sends that PR back through verification. - -### Boot recipe, for every live lane - -- [ ] Fetch the PR head with `git fetch origin` and check out the exact head SHA. -- [ ] Start the backend on the Strix Halo plus 7900XT pair and wait for `/props.build` to answer. -- [ ] Deliver input only through the bash driven harness. Name the read only diagnostics. -- [ ] Save every proof file under `/tmp/swarm-DS4V/worker-1` and return the paths with the report. - -## Reproduce the asymmetric baseline on the operator pair (DS4V-1) - -**Depends on.** None. - -**Files.** - -- [ ] Create `docs/ds4v-baseline.md` with the measured setup and commands. -- [ ] Create `scripts/ds4v-baseline.sh` with the launch and curl proof steps. - -**Build.** - -- [ ] Record the server SHA and both model SHAs in `docs/ds4v-baseline.md`. - -**You see.** - -- [ ] A `curl` call to `/props.build` answers with the expected image tag. - -**Verify, unit.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. - -- [ ] Run `ctest --output-on-failure -R deepseek4_unit` in `server/build` and keep the log. - -**Verify, live.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on `fast mechanical model (setup default)` at the PR head, per the boot recipe. - -- [ ] Lane 1. Regression lane against trunk. Run the same text prompt at trunk and head. Save `artifacts/ds4v-1/lane-1-compare.json`. Pass when both sides return HTTP 200 with non empty text. -- [ ] Lane 2. Chat smoke over the heterogeneous path. Send the LRU prompt from the verify skill. Save `artifacts/ds4v-1/lane-2-chat.json`. Pass when the response holds generated text. -- [ ] Lane 3. Build identity. Read `/props.build` from the running server. Save `artifacts/ds4v-1/lane-3-props.json`. Pass when the file names the expected image tag. -- [ ] Lane 4. Prefill probe. Send the 2k prompt used in the Lucebox report. Save `artifacts/ds4v-1/lane-4-prefill.json`. Pass when prompt processing exceeds 300 tok/s. -- [ ] Lane 5. Decode probe. Generate 128 tokens from the same prompt. Save `artifacts/ds4v-1/lane-5-decode.json`. Pass when decode exceeds 40 tok/s. -- [ ] Lane 6. DSpark acceptance. Read the served response header for the spec flag. Save `artifacts/ds4v-1/lane-6-spec.json`. Pass when the flag reports true. -- [ ] Lane 7. Placement proof. Read the server log for the owner lines. Save `artifacts/ds4v-1/lane-7-placement.log`. Pass when both devices appear as owners. -- [ ] Lane 8. Determinism. Send the same prompt twice. Save `artifacts/ds4v-1/lane-8-repeat.json`. Pass when both answers match byte for byte. -- [ ] Lane 9. Model list. Read `/v1/models` from the same server. Save `artifacts/ds4v-1/lane-9-models.txt`. Pass when the call returns 200 or 404 with a body. -- [ ] Lane 10. Cleanup. Run the verify skill cleanup. Save `artifacts/ds4v-1/lane-10-cleanup.log`. Pass when the instance is gone and the proof files remain. - -**Verify, perf.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. - -- [ ] Metric. Decode tok/s on the 2k prompt with 128 output tokens. -- [ ] Probe. Run `scripts/ds4v-baseline.sh` at trunk and at the head, interleaved. -- [ ] Baseline. Record the trunk value first. -- [ ] Rule. Head ties or beats trunk. Fail when head trails by more than 5 percent. - -**Review gate.** None. DS4V-1 is not review-gated. - -**Merge.** - -- [ ] Root records a clean verdict at the exact head SHA. -- [ ] The owner rebases onto current trunk after the verdict with patch id unchanged. - -## Lock the abliterated vision source with provenance (DS4V-2) - -**Depends on.** None. - -**Files.** - -- [ ] Create `docs/ds4v-source.md` with the parent repo and the tensor manifest. -- [ ] Create `scripts/ds4v-fetch-source.sh` with the exact download commands. - -**Build.** - -- [ ] Verify all 48 shards and 72633 tensors match the manifest by name and shape. - -**You see.** - -- [ ] The manifest lists the vision tower and the aligner as present. - -**Verify, unit.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. - -- [ ] Run `python3 scripts/ds4v-fetch-source.sh --check-only` and keep the checksum log. - -**Verify, live.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on `fast mechanical model (setup default)` at the PR head, per the boot recipe. - -- [ ] Lane 1. Regression lane against trunk. Run the same text prompt against the stock parent and the abliterated source. Save `artifacts/ds4v-2/lane-1-text.json`. Pass when both answers match in shape and the ablated one refuses less. -- [ ] Lane 2. Vision presence. List `vision.*` tensors in the manifest. Save `artifacts/ds4v-2/lane-2-vision.txt`. Pass when the count equals 259. -- [ ] Lane 3. Aligner presence. List `aligner.*` tensors in the manifest. Save `artifacts/ds4v-2/lane-3-aligner.txt`. Pass when the count equals 4. -- [ ] Lane 4. Router bias. List `bias_vl` tensors in the manifest. Save `artifacts/ds4v-2/lane-4-bias.txt`. Pass when the count equals 43. -- [ ] Lane 5. Image smoke. Describe the Earth image with the reference implementation. Save `artifacts/ds4v-2/lane-5-earth.json`. Pass when the answer names Earth. -- [ ] Lane 6. Text capability. Score the MMLU sample with both checkpoints. Save `artifacts/ds4v-2/lane-6-mmlu.json`. Pass when the delta stays within 1 point. -- [ ] Lane 7. Tokenizer. Encode the OpenAI style image message with the reference encoder. Save `artifacts/ds4v-2/lane-7-encode.json`. Pass when token ids match the reference. -- [ ] Lane 8. Draft head. List `mtp.*` blocks in the manifest. Save `artifacts/ds4v-2/lane-8-mtp.txt`. Pass when the count equals 3. -- [ ] Lane 9. License. Read the model `LICENSE` from the source repo. Save `artifacts/ds4v-2/lane-9-license.txt`. Pass when the text names MIT. -- [ ] Lane 10. Cleanup. Remove the scratch download cache. Save `artifacts/ds4v-2/lane-10-cleanup.log`. Pass when the manifest and proof files remain. - -**Verify, perf.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. - -- [ ] Metric. Minutes to verify all shard checksums on the build machine. -- [ ] Probe. Run `scripts/ds4v-fetch-source.sh --check-only` twice on the same host. -- [ ] Baseline. Record the first run value first. -- [ ] Rule. Second run ties or beats the first. Fail when it trails by more than 20 percent. - -**Review gate.** None. DS4V-2 is not review-gated. - -**Merge.** - -- [ ] Root records a clean verdict at the exact head SHA. -- [ ] The owner rebases onto current trunk after the verdict with patch id unchanged. - -## Ship a vision GGUF tuned for the operator pair (DS4V-3) - -**Depends on.** DS4V-1 and DS4V-2. - -**Files.** - -- [ ] Create `scripts/ds4v-quant.sh` with the imatrix quant recipe. -- [ ] Create `docs/ds4v-quant.md` with the rung table and the budget math. -- [ ] Create `share/model_cards/ds4v-vision.json` with the placement and budget. - -**Build.** - -- [ ] Run the quant recipe and record the GGUF SHAs in `docs/ds4v-quant.md`. - -**You see.** - -- [ ] A `curl` image prompt returns a correct scene description. - -**Verify, unit.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. - -- [ ] Assert the GGUF keeps `bias_vl` on all 43 layers and the mmproj loads. - -**Verify, live.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. Ten lanes on `fast mechanical model (setup default)` at the PR head, per the boot recipe. - -- [ ] Lane 1. Regression lane against trunk. Run the same text prompt at the DS4V-1 baseline and at this head. Save `artifacts/ds4v-3/lane-1-compare.json`. Pass when both return HTTP 200 with non empty text. -- [ ] Lane 2. Image description. Describe the carrots image through `/v1/chat/completions`. Save `artifacts/ds4v-3/lane-2-carrots.json`. Pass when the answer names carrots. -- [ ] Lane 3. Second image. Describe the corn image through the same endpoint. Save `artifacts/ds4v-3/lane-3-corn.json`. Pass when the answer names corn. -- [ ] Lane 4. Missing projector. Send an image prompt without mmproj loaded. Save `artifacts/ds4v-3/lane-4-nommproj.json`. Pass when the server answers 400 cleanly. -- [ ] Lane 5. Text still works. Send the LRU prompt with mmproj loaded. Save `artifacts/ds4v-3/lane-5-text.json`. Pass when the response holds generated text. -- [ ] Lane 6. Placement proof. Read the server log for the owner lines. Save `artifacts/ds4v-3/lane-6-placement.log`. Pass when both devices appear as owners. -- [ ] Lane 7. Decode probe. Generate 128 tokens from the 2k prompt. Save `artifacts/ds4v-3/lane-7-decode.json`. Pass when decode exceeds 35 tok/s. -- [ ] Lane 8. Determinism. Send the same image prompt twice. Save `artifacts/ds4v-3/lane-8-repeat.json`. Pass when both answers match byte for byte. -- [ ] Lane 9. Build identity. Read `/props.build` from the running server. Save `artifacts/ds4v-3/lane-9-props.json`. Pass when the file names the expected image tag. -- [ ] Lane 10. Cleanup. Run the verify skill cleanup. Save `artifacts/ds4v-3/lane-10-cleanup.log`. Pass when the instance is gone and the proof files remain. - -**Verify, perf.** Tests alone are not sufficient verification. A PR is verified only when its unit, live, and perf boxes are all checked. - -- [ ] Metric. Decode tok/s on the 2k prompt with 128 output tokens, plus prefill tok/s on the same prompt. -- [ ] Probe. Run the DS4V-1 baseline script and the DS4V-3 vision script interleaved on the same pair. -- [ ] Baseline. Record the DS4V-1 value first. -- [ ] Rule. Vision head stays within budget. Fail when decode trails the text baseline by more than 20 percent. - -**Review gate.** None. DS4V-3 is not review-gated. - -**Merge.** - -- [ ] Root records a clean verdict at the exact head SHA. -- [ ] The owner rebases onto current trunk after the verdict with patch id unchanged. - -## Close the program - -- [ ] Every box above is checked with its evidence. -- [ ] Reply to the operator with the report the execution playbook names. - -## Appendix A. Prototype evidence - -The Lucebox report at `https://www.lucebox.com/blog/deepseek-v4-asymmetric-parallelism` measures 51 tok/s median decode with asymmetric expert parallelism. -PR 604 is merged. It adds the RX 7900 XT plus Strix Halo dual GPU profile with 45 to 47 tok/s decode. -HF API lists 4 prometheusAIR rungs from 66 GiB to 108 GiB plus a sub GiB mmproj file. -OrcaRouter parent keeps vision. Its GGUF is text only. -Unproven. No GPU run happened on this Mac. All throughput numbers above are cited, not measured here. - -## Appendix B. Alternatives rejected - -Re-abliterate from scratch. Rejected. The OrcaRouter parent already bakes the edit with measured evals. -OrcaRouter GGUF directly. Rejected. It drops the vision tower. -Unsloth quants directly. Rejected for now. First shards read empty at check time. -Qwen mmproj path in Lucebox. Rejected for DS4. Lucebox vision gates on Qwen35 only. - -## Appendix C. Risks - -VRAM budget. The 95 GiB rung plus KV at long context nears the pair budget. Watch the 1M context setting. -Upstream llama dot cpp drift. Vision support merged days ago. Pin the commit in the quant script. -Safety. Abliterated weights comply with harmful requests. Keep them local and never serve them publicly. -This checkout lacks `server/` sources. All GPU work runs on the Strix Halo machine with submodules present. - -## Appendix D. Links and reading list - -Read `skills/how/SKILL.md` before the placement review in DS4V-1. -Read `skills/interrogate/SKILL.md` before the quant recipe review in DS4V-3. -Keep the trail per `skills/show-me-your-work/SKILL.md`. -The verify surface is `.pi/skills/verify-lucebox/SKILL.md`. diff --git a/harness/qualification/README.md b/harness/qualification/README.md index 4c5d8ed8c..34a378e4b 100644 --- a/harness/qualification/README.md +++ b/harness/qualification/README.md @@ -7,5 +7,3 @@ device settings and require machine-specific inputs. - `deepseek4/qualify_ds4_q5_amd.sh`: R9700 plus Strix Halo q=5 qualification - `deepseek4/rocprof_server_wrapper.sh`: delayed ROCm profiler launcher - `deepseek4/analyze_rocprof_overlap.py`: profiler overlap summary -- `deepseek4/ds4v-vision/`: DS4V vision numerical gate reproduction kit (harness, - reference environment receipts, frozen same-GPU reference tooling and run summaries) diff --git a/harness/qualification/deepseek4/ds4v-vision/README.md b/harness/qualification/deepseek4/ds4v-vision/README.md deleted file mode 100644 index 8406ca778..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# DS4V vision qualification kit - -Reproduction material for the DS4V vision numerical gate, shipped so the work can be -continued without rebuilding the measurement setup. Context and the full narrative are in -`docs/ds4v-continuation-status.md` and `docs/ds4v-image-serving.md`. - -The scripts are reproduced verbatim from the operator host (Strix Halo + 7900 XT, Linux -ROCm). They carry hard-coded host paths and their own idle/resource guards; read -`hip-qualification-README.md` before running anything. - -## Where the gate stands - -Against the frozen same-GPU original-source reference, thresholds unchanged: - -| Check | Result | Gate | -|---|---|---| -| Embeddings corn / carrots | 0.999525047 / 0.999721786 | pass | -| Corn features (cosine) | 0.999063593 | fail (0.9995) | -| Corn features (maxabs) | 0.947265625 | fail | -| Carrots features | cosine 0.999538399 pass, maxabs 0.26416015625 | fail (maxabs) | - -Earlier and retained results: - -- first native HIP run, runtime `4bf7270`: corn 0.985939, carrots 0.993884, embeddings also - below gate, harness exit 3 (`native-hip-first/`). -- scoped biased-linear-rounding fix `be8b0f1` (biases promoted to F32 operands on GPU - backends only) produced the passing embeddings and the corn cosine above - (`native-hip-scoped/`). -- CPU portability is a separate retained failure: original CPU corn feature cosine - 0.99822935. -- three-way comparison (`source-rocm-reference/hip-supervision-confirmed/three-way.json`): - the original-source corn HIP forward also fails the unchanged CPU gate (0.997729789, - maxabs 2.73828125), and native HIP fails against source HIP (features 0.991196939, - embeddings 0.994697537). CPU/GPU portability therefore does not explain the native - discrepancy. - -## Contents - -- `hip-qualification.sh`, `runtime-qualification.sh` - component-only and runtime windows - with the idle, port, KFD and memory guard. -- `hip-linear-qualification.sh`, `hip-attention-qualification.py`, - `hip-norm-qualification.py`, `hip-unbiased-qualification.py`, `hip-lt-qualification.sh`, - `hip-lt-concurrent-qualification.py`, `hip-lt-retry-qualification.py` - per-component - qualification, each with its review note where one exists. -- `target-hip-qualification-policy.md` (+ review), `component-window-review.md` - the - adopted same-GPU reference policy and the window review. -- `how-source.md`, `how-backend.md`, `native-tower-brief.md`, `native-tower-rubric.md` - - the source/backend contracts and the tower acceptance rubric. -- `source-rocm-reference/` - reference environment receipts (`constraints.txt`, - `cpu-runtime-info.py`, `libtorch-hip-*ldd.txt`, MIOpen receipts, script hashes), - the freeze and comparison tooling (`freeze-source-reference.py`, - `compare-corn-three-way.py`) and the control reports. -- `native-hip-first/`, `native-hip-scoped/` - the run summaries and comparisons. -- `mmproj-byte-proof.py`, `reference-fixtures.py`, `capture-comparator-runtime.py`, - `patch-bias-diagnostic.py` - supporting checks. - -## Reproducing - -1. Pin the reference environment from `source-rocm-reference/constraints.txt` and rebuild - the original-source Torch/ROCm control (see `source-rocm-reference/evidence/` for the - exact library and package receipts). -2. Freeze original-source corn and carrots outputs on the same device - (`freeze-source-reference.py`) and confirm repeat stability. -3. Run the native component window (`hip-qualification.sh --component-only`) under the - guard, then the corrected full-tower run. -4. Compare with `compare-corn-three-way.py`; no threshold is adjusted between runs. - -## Open decision: what counts as done - -The adopted policy keeps every numeric threshold unchanged, but the original-source corn -HIP forward itself fails the CPU feature gate. "Match the frozen same-GPU source" and -"pass 0.9995" are therefore different targets, and the residual corn deviation sits -between them. This needs an owner decision before the tower can be declared qualified. - -## Next experiments - -- Capture the two preselected real corn projections (patch projection, block-0 QKV) and - compare them against the frozen source outputs. The synthetic biased projection already - matches the original GPU source exactly at 1024/1024 values with the source's - first-heuristic and 76 MiB workspace configuration - (`artifacts/ds4v-step2/hipblaslt-real-projections/`, not in this kit). -- The residual is sparse rather than structural: corn cosine is close to the gate while - maxabs is 0.947265625, which points at per-element tie and product-rounding behaviour. - The direct fused source biased output still differs in 88 tiny tie cases, so a fused - versus unfused rounding contract is the leading candidate. -- The sensitive-block diagnostic found no semantic or BF16-boundary discrepancy for corn - blocks 12 and 31, and a single-thread original-source control reproduced the two-thread - reference bitwise. - -## Known gaps - -- The two source images (`corn.jpeg`, `carrots.jpeg`) are not in this repository and the - frozen reference outputs are tied to them. They were deleted from the reference host; - they can be recovered from the base64 payloads in the trial captures or provided on - request. -- Guard journals, per-run `guard.json` snapshots and HTTP captures are not included here. -- No production image-chat acceptance has been run against this tower state. diff --git a/harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py b/harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py deleted file mode 100644 index 1c27f3891..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/capture-comparator-runtime.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Read-only CPU comparator runtime inventory; run only on soulf.""" -import hashlib -import json -import pathlib -import subprocess -import sys -import numpy - -assert sys.flags.isolated -root = pathlib.Path(numpy.__file__).parent -files = {pathlib.Path(sys.executable).resolve()} -files.update(root.rglob('*.py')) -files.update(root.rglob('*.so')) -files.update((root.parent / 'numpy.libs').glob('*')) -for path in list(files): - if path.suffix == '.so' or path.name.startswith('python'): - result = subprocess.run(['/usr/bin/ldd', str(path)], text=True, capture_output=True, check=True) - assert 'not found' not in result.stdout - for line in result.stdout.splitlines(): - parts = line.split() - name = parts[2] if len(parts) > 2 and parts[1] == '=>' else parts[0] if parts else '' - if name.startswith('/'): - files.add(pathlib.Path(name).resolve()) -print(json.dumps({'python': sys.version, 'numpy': numpy.__version__, 'files': { - str(path): hashlib.file_digest(path.open('rb'), 'sha256').hexdigest() - for path in sorted(files) if path.is_file() -}}, indent=2)) diff --git a/harness/qualification/deepseek4/ds4v-vision/component-window-review.md b/harness/qualification/deepseek4/ds4v-vision/component-window-review.md deleted file mode 100644 index 364f96b1a..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/component-window-review.md +++ /dev/null @@ -1,24 +0,0 @@ -# Standalone component GPU-window review - -**PASS** for the narrow `--component-only` mode at local/deployed harness SHA256 `152af330bb37f77f8e6f9c795ae46c12f787e8587c42fc210aeb671b205296d5`. Reviewed `/Users/marcelorm/workspace/lucebox/artifacts/ds4v-step2/hip-qualification.sh` and the matching `/tmp/ds4v-hip-qualification.sh` on soulf. No harness/probe/GPU execution, source edit, service action or converter action occurred. - -The mode skips only the completed-text-proof prerequisite for an explicitly released standalone component window. It still requires the explicit release argument, fresh evidence, pinned runtime/binary/libraries/mmproj/fixtures and actual HIP device identity. Its summary records `component_only=true`, no text proof, and `standalone component; no chat/server acceptance`. The normal text-proof mode still validates its completed proof. Fixed comparisons, exit3 preservation, sequential hip0 probes and owned-child cleanup remain intact. - -`idle_window()` fails closed on an active operator/nonzero MainPID, either TCP listener8016/8217, any KFD compute process, unavailable sysfs data, less than8GiB host available memory, missing/ambiguous discrete card, or less than8GiB free discrete VRAM. It executes once before evidence creation and again immediately before the first GPU probe, after provenance hashing. These are readiness snapshots within the parent's explicitly released window, not an interprocess GPU reservation. - -CPU-only validation used the existing immutable reference venv with `python -I` under the exact clean HOME/PATH/locale/two-thread environment. Bash syntax and complete embedded Python AST parsing passed. Only AST-extracted `check` and `idle_window` function definitions were executed; no other harness statements or imports of model/GPU libraries ran. - -Both independent idle-window invocations returned: - -```text -ActiveState=inactive -MainPID=0 -8016/8217 TCP listeners absent -KFD process directory empty -host_available_bytes=36079882240 -discrete_free_vram_bytes=21430087680 -``` - -The user-service query works in that clean environment with only `XDG_RUNTIME_DIR=/run/user/` added; no inherited DBus variable was needed. The `card[0-9]*/device` glob also visits DRM connector entries, but their nested `device` is a directory, so the `is_file()` predicate correctly excludes them. Exactly `/sys/class/drm/card0/device` has PCI ID `0x744c`; card1 is `0x1586`. The discrete card resolves to PCI `0000:c6:00.0`. No duplicate match or DBus issue was found. - -This approves the prepared component-window gate under the parent's revised ordering. It claims no numerical/HIP result or full-chat acceptance, and does not authorize driving the operator service. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py deleted file mode 100644 index 61b118b20..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-attention-qualification.py +++ /dev/null @@ -1,314 +0,0 @@ -"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. - -This supervises three sequential Radeon-only lanes through the separate live -operator guard. It makes no isolated performance or HTTP acceptance claim. -""" -import argparse -import fcntl -import hashlib -import json -import os -from pathlib import Path -import re -import shutil -import signal -import stat -import subprocess -import sys - -HOME = Path('/home/marcelorm') -ROOT = HOME / 'lucebox-ds4v-vision-attention' -BUILD = Path('/tmp/ds4v-attention-hip-build') -SOURCE = '686285f092c961423747a8c961a509767aec1017' -REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' -CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' -PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' -SUPERVISOR = HOME / 'ds4v-work/hipblaslt-stage-diagnostic/log-snapshot-guard.py' -SUPERVISOR_SHA = '8591d28b0b11a1531fe2a657080958cbf401fda9a19dd18d747c09e5edbd06cb' -LANES = ['tiny', 'patch', 'qkv', 'mlp_w1', 'mlp_w2', 'norm782', 'norm2562', 'rotary', 'softmax', 'attention'] -FIXED = { - SUPERVISOR: SUPERVISOR_SHA, - BUILD / 'ds4v_vision_probe': 'd9ad23010e8dae30623442b94697587ca7c160824b2fe5c48ddaa6f6a380949d', - COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', - CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', -} -LIBRARIES = { - 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', 'e14f36e6e2ad059e404c658d307b03a5c20cfbf57ea3ded00049ee872e080f86'), - 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '91d2d975096a3a4597f033ee2250d0df092be1493e212d9bf84ce8099c443590'), - 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '67debd0e1638230a6f2c94690483b660bff9e071e41eeef61f35d386cdfcc817'), - 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', '60cc08a311f0ad9121a8760adac132c46cd028bb8070acd55632e50c89dee397'), - 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), - 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), -} - -def require(ok, why): - if not ok: - raise RuntimeError(why) - -def digest(path): - with Path(path).open('rb') as stream: - return hashlib.file_digest(stream, 'sha256').hexdigest() - -def verify_pins(pins): - for path, sha in pins.items(): - require(re.fullmatch('[0-9a-f]{64}', sha) is not None, 'unbound or invalid pin: ' + str(path)) - require(digest(path) == sha, 'pin changed: ' + str(path)) - -def guarded_run(command, log): - child = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT) - previous = {} - def interrupted(signum, frame): - raise InterruptedError(f'qualification interrupted: {signum}') - try: - for signum in (signal.SIGTERM, signal.SIGINT): - previous[signum] = signal.signal(signum, interrupted) - require(child.wait() == 0, 'lane supervision failed') - finally: - # The guard handles SIGTERM by stopping/reaping its direct GPU child. - # Never kill the guard while it might still own a live GPU process. - if child.poll() is None: - child.terminate() - child.wait(timeout=30) - for signum, handler in previous.items(): - signal.signal(signum, handler) - -def attention_counts(name): - softmax = 6 if name == 'softmax' else 1 if name == 'attention' else 0 - av = 1 if name == 'attention' else 0 - return {'explicit_softmax_ops': softmax, 'actual_softmax_launches': softmax, - 'explicit_av_ops': av, 'actual_av_launches': av, - 'actual_rotary_launches': 1 if name in ('rotary', 'attention') else 0} - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--config', type=Path, required=True) - parser.add_argument('--config-sha', required=True) - parser.add_argument('--parent-radeon-window-released', action='store_true') - args = parser.parse_args() - require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') - require(re.fullmatch('[0-9a-f]{40}', SOURCE) is not None, 'source commit is not bound') - require(digest(args.config) == args.config_sha, 'config changed') - cfg = json.loads(args.config.read_text()) - require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') - production_path = Path(cfg['production_pins']['path']) - production_sha = cfg['production_pins']['sha256'] - require(re.fullmatch('[0-9a-f]{64}', production_sha) is not None - and digest(production_path) == production_sha, 'production pin manifest changed') - production = json.loads(production_path.read_text()) - require(production.get('schema') == 'ds4v-attention-production-runtime-v1' - and production.get('source_root') == str(ROOT) - and production.get('source_commit') == SOURCE and isinstance(production.get('files'), dict) - and production['files'], 'production source/file pins missing') - linear_path = Path(cfg['production_receipt']['path']) - require(digest(linear_path) == cfg['production_receipt']['sha256'], 'linear acceptance receipt changed') - linear = json.loads(linear_path.read_text()) - require(linear.get('schema') == 'ds4v-attention-native-production-proof-v1' and linear.get('pass') is True - and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') - require(linear['source_commit'] == SOURCE, 'linear source mismatch') - require(linear['pins_sha256'] == production_sha, 'linear production pins mismatch') - require([x['name'] for x in linear['lanes']] == LANES, 'ten production prerequisite lanes required') - require(linear['supervisor'] == production['supervisor'] == {'path': str(SUPERVISOR), 'sha256': SUPERVISOR_SHA}, - 'logging supervisor differs from accepted prerequisites') - require(linear['source_norm'] == production['source_norm'], 'normalization source provenance mismatch') - require(linear['source_attention'] == production['source_attention'], 'attention source provenance mismatch') - require(linear['source_acceptance'] == production['source_acceptance'] == { - 'path': str(SUPERVISOR.parent / 'unbiased-source-acceptance-v1.json'), - 'sha256': '1d0d59f2b3ffc21a366df50ef7257fe6ac2a1eca78cbea10988df3f2398bdbfa'}, 'source acceptance changed') - for lane in linear['lanes']: - require(lane['guard_exit'] == 0 and lane['child_exit'] == 0 - and lane['component_numeric_pass'] is True, 'linear lane not accepted') - require(lane['source_bitwise_mismatches'] == 0, 'production linear differs from frozen source') - norm = lane['name'].startswith('norm') - expected_dispatch = 0 if norm or lane['name'] in ('rotary', 'softmax', 'attention') else 1 if lane['name'].startswith('mlp_') else 2 - require(lane['explicit_ops'] == lane['actual_lt_launches'] == expected_dispatch, - 'wrong production linear dispatch count') - require(lane['explicit_norm_ops'] == lane['actual_norm_launches'] == (1 if norm else 0), - 'wrong production normalization dispatch count') - for field, expected in attention_counts(lane['name']).items(): - require(lane[field] == expected, 'wrong production attention dispatch count: ' + field) - if norm: - require(lane['reference_layout'] == ('original' if lane['name'] == 'norm782' else 'rowwise-tiled-original'), - 'normalization reference layout changed') - require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') - pins = dict(FIXED) - pins[args.config] = args.config_sha - pins[linear_path] = cfg['production_receipt']['sha256'] - pins[production_path] = production_sha - pins.update({path: sha for path, sha in LIBRARIES.values()}) - for path, sha in production['files'].items(): - require(Path(path).is_absolute(), 'absolute production file pin required') - require(Path(path) not in pins or pins[Path(path)] == sha, 'production pin conflicts with qualification pin: ' + path) - pins[Path(path)] = sha - for path, sha in linear['input_artifact_hashes'].items(): - require(Path(path).is_absolute() and (Path(path) not in pins or pins[Path(path)] == sha), - 'prerequisite provenance conflicts with qualification pins') - pins[Path(path)] = sha - for lane in linear['lanes']: - pins[Path(lane['guard_report_path'])] = lane['guard_report_sha256'] - runtime = Path(cfg['comparator_runtime']) - policy = Path(cfg['qualification_policy']) - pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', - policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) - verify_pins(pins) - for path, sha in json.loads(runtime.read_text())['files'].items(): - require(Path(path) not in pins or pins[Path(path)] == sha, 'comparator runtime pin conflict') - pins[Path(path)] = sha - for directory in (REFERENCE, CPU_REFERENCE): - manifest = json.loads((directory / 'manifest.json').read_text()) - require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') - for label, entry in manifest['images'].items(): - require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') - for stage in ('patches', 'features', 'embeddings'): - path = directory / entry[stage]['file'] - require(path.parent == directory, 'fixture path escapes reference') - pins[path] = entry[stage]['sha256'] - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) - require('not found' not in ldd, 'unresolved dependency') - resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) - for soname, (path, _) in LIBRARIES.items(): - # The standalone probe links the backend libraries directly; the umbrella - # libggml is built/pinned but omitted by the linker's --as-needed rule. - if soname == 'libggml.so.0' and soname not in resolved: - continue - require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) - verify_pins(pins) - # Guard code and lane policies are frozen before importing or invoking them. - guard_dir = Path(cfg['guard_dir']) - require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') - pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) - verify_pins(pins) - require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') - sys.path.insert(0, str(guard_dir)) - from run import load_policy, launch_command - from host import Host - from guard import prepare_preflight - require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') - policies = [] - for lane in cfg['lanes']: - p = load_policy(Path(lane['policy']), lane['sha256']) - pins[Path(lane['policy'])] = lane['sha256'] - isolation_pins = p.get('isolation_pins', {}) - require('/usr/bin/python3.12' in isolation_pins and - all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), - 'namespace runtime is not included in guarded component pins') - for path, sha in p['component_pins'].items(): - require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) - pins[Path(path)] = sha - label = lane['name'].split('-')[0] - h, w = (42, 61) if label == 'carrots' else (23, 34) - out = guard_dir / p['run_name'] - expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), - str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] - require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') - require('hip_vision_linear_launches=131 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') - require('hip_vision_norm_launches=65' in p['required_log_lines'], 'normalization dispatch contract missing') - for name, count in (('rotary', 1), ('softmax', 32), ('av', 32)): - require('hip_vision_' + name + '_launches=' + str(count) in p['required_log_lines'], - 'attention dispatch contract missing: ' + name) - require(p['component_pins'].get(str(SUPERVISOR)) == SUPERVISOR_SHA, 'logging supervisor is not pinned') - sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, - str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} - require(set(p['required_outputs']) == set(sizes), 'wrong output contract') - require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') - policies.append((p, out, label)) - require(len({str(out) for _, out, _ in policies}) == 3, 'full lane evidence directories must be distinct') - evidence = Path(cfg['evidence']) - require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') - evidence.mkdir() - report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', - 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), - 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'production_receipt': cfg['production_receipt'], - 'production_pins': cfg['production_pins'], 'supervisor': production['supervisor'], 'lanes': []} - try: - for lane, (p, out, label) in zip(cfg['lanes'], policies): - verify_pins(pins) - supervisor_log = evidence / (lane['name'] + '-supervisor.log') - with supervisor_log.open('x') as log: - guarded_run([str(PYTHON), '-I', '-B', str(SUPERVISOR), '--policy', lane['policy'], - '--policy-sha', lane['sha256'], '--parent-radeon-window-released'], log) - pins[supervisor_log] = digest(supervisor_log) - result = json.loads((out / 'guard.json').read_text()) - require(result['pass'] and result['device_proof_verified'], 'guard/result failed') - require(result.get('namespace_verified') is True, 'private NPU namespace not verified') - require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') - require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] - and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', - 'guard report belongs to a different command/policy/outcome') - require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') - for file, meta in result['output_evidence'].items(): - require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') - require(digest(file) == meta['sha256'], 'guard output changed before copying') - pins[Path(file)] = meta['sha256'] - pins[out / 'guard.json'] = digest(out / 'guard.json') - log = (out / 'child.log').read_text() - require(re.findall(r'^hip_vision_linear_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('131', '79691776')], 'ambiguous/incomplete full-tower dispatch') - require(re.findall(r'^hip_vision_norm_launches=(\d+)$', log, re.M) == ['65'], - 'ambiguous/incomplete full-tower normalization dispatch') - for name, count in (('rotary', 1), ('softmax', 32), ('av', 32)): - require(re.findall(r'^hip_vision_' + name + r'_launches=(\d+)$', log, re.M) == [str(count)], - 'ambiguous/incomplete full-tower attention dispatch: ' + name) - pins[out / 'child.log'] = digest(out / 'child.log') - report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], - 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], - 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], - 'command': p['command'], 'launch_command': result['launch_command'], - 'namespace_verified': result['namespace_verified'], - 'actual_lt_launches': 131, 'actual_norm_launches': 65, - 'actual_rotary_launches': 1, 'actual_softmax_launches': 32, 'actual_av_launches': 32, - 'supervisor_log_path': str(supervisor_log), 'supervisor_log_sha256': pins[supervisor_log], - 'child_exit': result['exit'], 'outputs': result['output_evidence']}) - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for index, (_, out, label) in enumerate(policies): - for stage in ('features', 'embeddings'): - original = out / f'{label}-{stage}.f32' - copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied output differs from guarded output') - pins[copied] = pins[original] - for stage in ('features', 'embeddings'): - original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied repeat carrots differ') - pins[copied] = pins[original] - codes = {} - for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), - ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: - verify_pins(pins) - with (evidence / f'{name}.log').open('w') as log: - result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], - stdout=log, stderr=subprocess.STDOUT, timeout=120) - require(result.returncode in (0, 3), 'comparator execution failed: ' + name) - codes[name] = result.returncode - report['comparisons'] = codes - report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) - verify_pins(pins) - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - p = policies[-1][0] - host = Host(p) - fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) - try: - st = os.fstat(fd) - require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) - finally: - os.close(fd) - (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') - report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] - except Exception as error: - report['error'] = f'{type(error).__name__}: {error}' - finally: - (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') - print(json.dumps(report, indent=2)) - return 0 if report['pass'] else 3 - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md b/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md deleted file mode 100644 index 34a845407..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification-review.md +++ /dev/null @@ -1,40 +0,0 @@ -# Scoped HIP linear qualification harness review - -**Verdict: PASS for execution only after the scoped tiny regression and CPU -identity gates pass and the parent explicitly releases the GPU lane.** Reviewed -`hip-linear-qualification.sh` at SHA256 -`8d3e34a5df237da0e4bb3098ff03e1d6694e91d547700879e927ddb2ff5c6806`. -This is a read-only harness review, not a tower qualification result. - -The harness pins candidate source `be8b0f1b07f1a3a034ce1d7333fd0d3402754c60`, -probe `79f4928a5172001eef205b100d7578f77560a566d4179fe6755951a15438b483`, -the three reused GGML libraries, projector, comparator, original CPU manifest, -canonical 7900 XT source manifest and its freeze. Source policy SHA256 -`62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f` -matches the adopted prospective policy. The frozen target manifest and freeze -are `677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86` -and `8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0`. -They record first-output selection, byte-identical source repeats for both images, -the exact runner and device, and unchanged thresholds before candidate execution. - -The revised integrity boundary is complete. It hashes all target and original CPU -patch, feature and embedding bytes before the first GPU operation, includes both -sets in provenance, and rehashes both after all comparisons. It also rechecks all -pinned files. Candidate execution uses only the frozen target patches. The two -original-CPU comparisons are reported separately and their numerical exit 3 does -not control target acceptance. - -Target acceptance requires both the first and repeat comparisons to complete -normally and apply every unchanged feature and embedding gate to both images. -Corn output must also repeat byte for byte. Either target comparator exit 3 or a -repeat mismatch produces final exit 3. Execution, shape, hash, load, device or -timeout failures remain unqualified. Once a target comparator records numerical -exit 3, the finalizer cannot turn it into success or hide it behind a later error. - -The idle-window, exact device, no-fallback, owned-child cleanup, timeout and fresh -evidence-directory controls are retained from the previously reviewed harness. -The run remains scoped to the 7900 XT tower and does not qualify CPU portability, -gfx1151, HTTP image behavior, the decoder, text regression or full serving. - -No build, model execution, GPU operation, server action or source edit was made -during this review. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh b/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh deleted file mode 100644 index ebd5f7d88..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-linear-qualification.sh +++ /dev/null @@ -1,248 +0,0 @@ -#!/usr/bin/env bash -# Scoped GPU candidate; execute only after tiny-regression PASS and explicit lane release. -# Standalone component checks may precede text proof in an idle, explicitly released GPU window. -set -euo pipefail -[[ $(hostname) = soulf && $# = 3 && $3 = --gpu-window-released ]] || { - echo 'Usage on soulf: hip-qualification.sh COMPLETED_TEXT_PROOF_OR_--component-only NEW_EVIDENCE_DIR --gpu-window-released' >&2; exit 2; -} -python="$HOME/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python" -# No inherited device masks, overrides, GGML/DS4/DFLASH controls, LD_PRELOAD, or Python settings. -exec env -i HOME="$HOME" PATH=/usr/bin:/bin LANG=C LC_ALL=C \ - OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MKL_NUM_THREADS=2 \ - "$python" -I - "$1" "$2" "$(realpath "$0")" <<'PY' -import hashlib, json, os, re, signal, subprocess, sys, time -from pathlib import Path - -home = Path.home() -root = home / 'lucebox-ds4v-linear-rounding' -build = Path('/tmp/ds4v-linear-rounding-scoped-build') -ggml_build = Path('/tmp/ds4v-runtime-hip-build') -binary = build / 'ds4v_vision_probe' -cpu_reference = home / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -reference = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference') -freeze_manifest = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json') -policy_sha = '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f' -mmproj = home / 'ds4v-work/ds4v-mmproj.gguf' -compare = root / 'server/tools/ds4v_vision/compare.py' -component_only = sys.argv[1] == '--component-only' -text_proof = None if component_only else Path(sys.argv[1]) -evidence, harness = map(Path, sys.argv[2:]) -source_sha = 'be8b0f1b07f1a3a034ce1d7333fd0d3402754c60' -pinned = { - binary: '79f4928a5172001eef205b100d7578f77560a566d4179fe6755951a15438b483', - compare: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - mmproj: '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - cpu_reference / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - reference / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', - freeze_manifest: '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', - ggml_build / 'ggml/src/libggml-base.so.0': '378b6c81052532d19bd86b32de236553cc1e57f824ce35fb733569470786ed29', - ggml_build / 'ggml/src/libggml-cpu.so.0': 'b33faf3a600eeff2bea8b692360cff6de397aaf3082ea0c73a9f3a1af9ee70d2', - ggml_build / 'ggml/src/ggml-hip/libggml-hip.so.0': 'b8991450ee422983b91cfbfcdf8d6b612e92f62f1128c6cce0c6b3e37ff8ef7e', -} - -def check(ok, message): - if not ok: raise RuntimeError(message) - -def digest(path): - with open(path, 'rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() - -def dump(name, value): - (evidence / name).write_text(json.dumps(value, indent=2) + '\n') - -def idle_window(): - bus_env = dict(os.environ, XDG_RUNTIME_DIR=f'/run/user/{os.getuid()}') - state = subprocess.check_output(['systemctl', '--user', 'show', 'deepseek-dflash.service', - '--property=ActiveState', '--property=MainPID'], env=bus_env, text=True) - properties = dict(line.split('=', 1) for line in state.splitlines()) - check(properties.get('ActiveState') in ('inactive', 'failed') and properties.get('MainPID') == '0', - 'operator service is not down') - listeners = subprocess.check_output(['ss', '-ltn'], text=True) - check(not any(len(row.split()) > 3 and row.split()[3].endswith((':8016', ':8217')) - for row in listeners.splitlines()), 'operator or private text port is occupied') - processes = list(Path('/sys/class/kfd/kfd/proc').iterdir()) - check(not processes, 'GPU compute processes already exist') - available = int(next(row.split()[1] for row in Path('/proc/meminfo').read_text().splitlines() - if row.startswith('MemAvailable:'))) * 1024 - check(available >= 8 * 1024**3, 'less than 8 GiB host memory available for component check') - cards = [p for p in Path('/sys/class/drm').glob('card[0-9]*/device') - if (p / 'device').is_file() and (p / 'device').read_text().strip() == '0x744c'] - check(len(cards) == 1, 'expected exactly one RX 7900 XT device') - free_vram = int((cards[0] / 'mem_info_vram_total').read_text()) - int((cards[0] / 'mem_info_vram_used').read_text()) - check(free_vram >= 8 * 1024**3, 'less than 8 GiB discrete VRAM available') - return {'operator': properties, 'kfd_processes': [], 'host_available_bytes': available, - 'discrete_free_vram_bytes': free_vram} - -# No GPU call or evidence mutation before the explicit mode and idle-window gates. -if not component_only: - text_proof = text_proof.resolve(strict=True) - allowed = [home / f'{tree}/artifacts/fitter-fix' for tree in - ('lucebox-ds4v-mix-fix', 'lucebox-ds4v-mix-parallel')] - check(text_proof.parent in allowed and text_proof.name.startswith('load-proof-'), 'unexpected text-proof directory') - check((text_proof / 'harness.exit').read_text().strip() == '0', 'private text proof did not complete successfully') - check(bool((text_proof / 'cleanup.txt').read_text().strip()), 'private text proof cleanup missing') - verdict = json.loads((text_proof / 'verdict.json').read_text()) - check(all(verdict.get(k) == 'PASS' for k in ('text_load_smoke', 'math_answer', 'longer_decode_speculation')), - 'private text verdict is not PASS') - server_pid = int((text_proof / 'server.pid').read_text()) - check(server_pid > 1 and not Path(f'/proc/{server_pid}').exists(), 'private text server PID remains present') -initial_window = idle_window() -check(evidence.is_absolute() and not evidence.exists(), 'provide a fresh absolute evidence directory') -evidence.mkdir() # Parent must already exist; never remove or reuse evidence. -print(f'Evidence: {evidence}', flush=True) -active = None -summary = {'status': 'ISSUES', 'features': 'ISSUES', 'embeddings': 'NOT_QUALIFIED', - 'source_sha': source_sha, 'device': 'hip:0', 'text_proof': str(text_proof) if text_proof else None, - 'scope': '7900XT target-source fidelity only; CPU portability separate; no chat/server acceptance', 'policy_sha256': policy_sha, 'reference': str(reference), 'component_only': component_only, - 'initial_window': initial_window, 'lanes': []} -exit_code = 1 - -def interrupted(signum, frame): - raise InterruptedError(signum) - -signal.signal(signal.SIGINT, interrupted) -signal.signal(signal.SIGTERM, interrupted) - -def memory(): - result = {'monotonic_seconds': time.monotonic(), 'host': Path('/proc/meminfo').read_text()} - result['drm'] = {str(p): p.read_text().strip() for p in Path('/sys/class/drm').glob('card*/device/mem_info_*') - if p.name in ('mem_info_vram_total', 'mem_info_vram_used', 'mem_info_gtt_used')} - return result - -def stop_owned(): - global active - if active is not None: - # This unreaped direct child cannot have its PID reused. Never pkill or signal other processes. - active.terminate() - try: active.wait(timeout=5) - except subprocess.TimeoutExpired: active.kill(); active.wait() - lane = summary['lanes'][-1] - lane.update(exit=active.returncode, stopped_by_harness=True) - (evidence / f"{lane['name']}.exit").write_text(str(active.returncode) + '\n') - dump(f"{lane['name']}.time.json", lane) - active = None - -def run(name, command, timeout=900): - global active - lane = {'name': name, 'command': list(map(str, command))} - summary['lanes'].append(lane) - started = time.monotonic() - with open(evidence / f'{name}.log', 'w') as log, open(evidence / f'{name}.memory.jsonl', 'w') as samples: - active = subprocess.Popen(lane['command'], stdout=log, stderr=subprocess.STDOUT) - lane['pid'] = active.pid - (evidence / f'{name}.pid').write_text(str(active.pid) + '\n') - dump('summary.json', summary) - while True: - pid, status, usage = os.wait4(active.pid, os.WNOHANG) - if pid: - code = os.waitstatus_to_exitcode(status) - active.returncode = code - active = None - lane.update(exit=code, elapsed_seconds=time.monotonic()-started, - user_seconds=usage.ru_utime, system_seconds=usage.ru_stime, max_rss_kib=usage.ru_maxrss) - (evidence / f'{name}.exit').write_text(str(code) + '\n') - dump(f'{name}.time.json', lane) - dump('summary.json', summary) - return code - sample = memory() - try: sample['process_status'] = Path(f'/proc/{active.pid}/status').read_text() - except FileNotFoundError: pass - samples.write(json.dumps(sample) + '\n'); samples.flush() - if time.monotonic()-started > timeout: raise TimeoutError(f'{name} exceeded {timeout}s') - time.sleep(0.5) - -def verify_device(name): - log = (evidence / f'{name}.log').read_text() - check(re.search(r'^backend=ROCm0 requested=hip:0$', log, re.M), f'{name}: not the requested HIP backend') - check(re.search(r'Device 0: [^\n]*7900 XT[^\n]*gfx1100', log), f'{name}: device0 is not 7900 XT/gfx1100') - check(re.search(r'Device 1: [^\n]*gfx1151', log), f'{name}: device1 order changed') - -try: - check(subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() == source_sha, - 'runtime source commit changed') - subprocess.run(['git', '-C', str(root), 'diff', '--quiet', 'HEAD', '--'], check=True) - for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed: {path}') - manifest = json.loads((reference / 'manifest.json').read_text()) - check(set(manifest['images']) == {'carrots', 'corn'}, 'unexpected fixture set') - fixtures = {} - for label, grid in [('carrots', [42, 61]), ('corn', [23, 34])]: - entry = manifest['images'][label] - check(entry['vit_grid'] == grid, f'{label}: grid changed') - for stage in ('patches', 'features', 'embeddings'): - meta = entry[stage]; path = reference / meta['file'] - check(path.parent == reference and digest(path) == meta['sha256'], f'{label}/{stage}: fixture changed') - fixtures[str(path)] = meta - cpu_fixtures = {} - cpu_manifest = json.loads((cpu_reference / 'manifest.json').read_text()) - check(set(cpu_manifest['images']) == {'carrots', 'corn'}, 'unexpected CPU fixture set') - for label, entry in cpu_manifest['images'].items(): - for stage in ('patches', 'features', 'embeddings'): - meta = entry[stage]; path = cpu_reference / meta['file'] - check(path.parent == cpu_reference and digest(path) == meta['sha256'], f'CPU fixture changed: {label}/{stage}') - cpu_fixtures[str(path)] = meta - ldd = subprocess.check_output(['/usr/bin/ldd', str(binary)], text=True) - check('not found' not in ldd and 'libggml-hip.so' in ldd, 'HIP dependency resolution failed') - (evidence / 'ldd.txt').write_text(ldd) - libraries = sorted(set(re.findall(r'(?:=>\s+|^\s*)(/[^\s]+)\s+\(', ldd, re.M))) - software = {path: digest(path) for path in libraries} - software[str(Path(sys.executable).resolve())] = digest(Path(sys.executable).resolve()) - dump('provenance.json', {'source_sha': source_sha, 'pinned': {str(p): s for p, s in pinned.items()}, - 'shared_libraries_and_python': software, 'fixtures': fixtures, 'cpu_fixtures': cpu_fixtures, - 'environment': dict(os.environ), 'python': sys.version, 'harness_sha256': digest(harness), - 'text_proof_files': {p.name: digest(p) for p in text_proof.iterdir() if p.is_file()} if text_proof else {}}) - dump('memory-before.json', memory()) - dump('window-before-gpu.json', idle_window()) - check(run('device-check', [binary, mmproj, '--load-only', '4096', '129280', 'hip:0']) == 0, - 'HIP load-only/device check failed') - verify_device('device-check') - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for label, h, w, output, lane in [('carrots', 42, 61, native, 'carrots'), - ('corn', 23, 34, native, 'corn'), ('corn', 23, 34, repeat, 'corn-repeat')]: - check(run(lane, [binary, mmproj, reference / f'{label}-patches.f32', str(h), str(w), output, label, '0', 'hip:0']) == 0, - f'{lane}: HIP encode failed') - verify_device(lane) - # compare.py requires both fixture names: explicitly reuse the first carrots result in repeat comparison. - for stage in ('features', 'embeddings'): - (repeat / f'carrots-{stage}.f32').symlink_to(native / f'carrots-{stage}.f32') - cpu_statuses = [run(name, [sys.executable, '-I', compare, cpu_reference, output, '--output', evidence / f'{name}.json']) - for name, output in [('native-vs-cpu', native), ('source-hip-vs-cpu', reference)]] - check(all(code in (0, 3) for code in cpu_statuses), 'CPU portability comparison execution/shape/hash failure') - summary['cpu_portability'] = dict(zip(('native_vs_cpu', 'source_hip_vs_cpu'), - ('PASS' if code == 0 else 'ISSUES' for code in cpu_statuses))) - statuses = [run(name, [sys.executable, '-I', compare, reference, output, '--output', evidence / f'{name}.json']) - for name, output in [('comparison', native), ('repeat-comparison', repeat)]] - check(all(code in (0, 3) for code in statuses), 'comparison execution/shape/hash failure') - comparisons = [json.loads((evidence / f'{name}.json').read_text()) for name in ('comparison', 'repeat-comparison')] - summary['features'] = 'PASS' if all(c[label]['features']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' - summary['embeddings'] = 'PASS' if all(c[label]['embeddings']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' - summary['corn_repeat_byte_identical'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') - for s in ('features', 'embeddings')) - dump('outputs.sha256.json', {str(p.relative_to(evidence)): digest(p) for directory in (native, repeat) for p in directory.glob('*.f32')}) - for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed during qualification: {path}') - for path, meta in fixtures.items(): check(digest(path) == meta['sha256'], f'reference changed during qualification: {path}') - for path, meta in cpu_fixtures.items(): check(digest(path) == meta['sha256'], f'CPU reference changed during qualification: {path}') - exit_code = 3 if 3 in statuses or not summary['corn_repeat_byte_identical'] else 0 - summary['status'] = 'PASS' if exit_code == 0 else 'ISSUES' -except InterruptedError as error: - exit_code = 128 + int(error.args[0]); summary['error'] = f'interrupted by signal {error.args[0]}' - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -except TimeoutError as error: - exit_code = 124; summary['error'] = str(error) - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -except Exception as error: - summary['error'] = f'{type(error).__name__}: {error}' - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -finally: - signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGTERM, signal.SIG_IGN) - stop_owned() - # Once the fixed comparator returned 3, no later command may turn that into success or mask it. - if any(lane['name'] in ('comparison', 'repeat-comparison') and lane.get('exit') == 3 for lane in summary['lanes']): - exit_code = 3 - summary['exit'] = exit_code - dump('summary.json', summary) - dump('memory-after.json', memory()) - (evidence / 'harness.exit').write_text(str(exit_code) + '\n') - print(json.dumps(summary, indent=2), flush=True) -raise SystemExit(exit_code) -PY diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md b/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md deleted file mode 100644 index da8ad7db2..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.md +++ /dev/null @@ -1,40 +0,0 @@ -# Native HIP image qualification - -Prepared, not executed. `hip-lt-concurrent-qualification.py` supersedes the -idle-only draft for this prospective numerical window. It does not start a -server or authorize a paired GPU HTTP test. - -The candidate is `6137f4305400247fed98d2144634c184e2bc6b13`. Its clean HIP -probe is `635a49b45d116a62d405898230389f41e77aa51d47437c02ffa647919fd458d7`. -CPU execution already preserves all four prior full-image outputs exactly; -the older CPU-versus-source feature failures remain visible. - -Release requires the reviewed concurrent guard, concrete immutable workload -policies, and the accepted six-lane linear receipt for this same candidate. -The three full-image lanes are carrots, corn, and a second corn execution. -Each is a separate direct child of the live operator guard, sees one Radeon, -and must execute all 67 fused biased projections with the fixed 76 MiB -workspace. The guard checks the existing Strix operator and its resources -before, during, and after every lane. Unrelated opaque non-KFD processes are -a recorded visibility limitation; this is not exclusive device ownership or -a performance benchmark. - -The original target policy is unchanged, SHA256 -`62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f`: - -| Output | Maximum error | RMSE | Minimum cosine | -| --- | ---: | ---: | ---: | -| Features | 0.25 | 0.03 | 0.9995 | -| Embeddings | 0.75 | 0.08 | 0.9990 | - -All stages and both images must pass against the frozen first original-source -Radeon outputs. Both corn outputs must repeat byte-for-byte. Separate CPU -portability reports cannot replace the target comparison or hide its failure. -The comparator, references, complete Python/numpy runtime inventory, candidate -binary and actual linked HIP libraries are pinned. The umbrella `libggml.so` -is a pinned build artifact but is not linked into this standalone probe. - -After comparisons, input/source checks repeat and a final locked read-only -operator preflight must pass before the report can indicate success. A -successful numerical report would permit the planned production integration; -it would not establish that HTTP image input or image-based answers work. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py deleted file mode 100644 index 2014028a5..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-lt-concurrent-qualification.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. - -This supervises three sequential Radeon-only lanes through the separate live -operator guard. It makes no isolated performance or HTTP acceptance claim. -""" -import argparse -import fcntl -import hashlib -import json -import os -from pathlib import Path -import re -import shutil -import signal -import stat -import subprocess -import sys - -HOME = Path('/home/marcelorm') -ROOT = HOME / 'lucebox-ds4v-vision-hipblaslt' -BUILD = Path('/tmp/ds4v-lt-hip-build') -SOURCE = '6137f4305400247fed98d2144634c184e2bc6b13' -REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' -CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' -PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' -FIXED = { - BUILD / 'ds4v_vision_probe': '635a49b45d116a62d405898230389f41e77aa51d47437c02ffa647919fd458d7', - COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', - CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', -} -LIBRARIES = { - 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', '1bdc3462382f5a208272badc459aee4f1c8c46536241f6e40fdbaee6c7ebeef1'), - 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '09c3721cb3e4c3689752fed9163c62dc4e20e5e3136a806a72ef7b544b7f89b3'), - 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '6c7f4bf85b8fc98c6b1109de5d2de4b5e9beb09680d47f7fd01414e70c556955'), - 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', 'd71378079c9ea008269964b34e9aa48a06db703e6c69db92b1aa7c803e5eee72'), - 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), - 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), -} - -def require(ok, why): - if not ok: - raise RuntimeError(why) - -def digest(path): - with Path(path).open('rb') as stream: - return hashlib.file_digest(stream, 'sha256').hexdigest() - -def verify_pins(pins): - for path, sha in pins.items(): - require(digest(path) == sha, 'pin changed: ' + str(path)) - -def guarded_run(command): - child = subprocess.Popen(command) - previous = {} - def interrupted(signum, frame): - raise InterruptedError(f'qualification interrupted: {signum}') - try: - for signum in (signal.SIGTERM, signal.SIGINT): - previous[signum] = signal.signal(signum, interrupted) - require(child.wait() == 0, 'lane supervision failed') - finally: - # The guard handles SIGTERM by stopping/reaping its direct GPU child. - # Never kill the guard while it might still own a live GPU process. - if child.poll() is None: - child.terminate() - child.wait(timeout=30) - for signum, handler in previous.items(): - signal.signal(signum, handler) - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--config', type=Path, required=True) - parser.add_argument('--config-sha', required=True) - parser.add_argument('--parent-radeon-window-released', action='store_true') - args = parser.parse_args() - require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') - require(digest(args.config) == args.config_sha, 'config changed') - cfg = json.loads(args.config.read_text()) - linear_path = Path(cfg['linear_receipt']['path']) - require(digest(linear_path) == cfg['linear_receipt']['sha256'], 'linear acceptance receipt changed') - linear = json.loads(linear_path.read_text()) - require(linear.get('schema') == 'ds4v-lt-concurrent-linear-proof-v1' and linear.get('pass') is True - and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') - require(linear['source_commit'] == SOURCE and linear['red_commit'] == '983be861d878681a26f9f8c9e8cd4f804ab0f14d', 'linear source mismatch') - require(linear['pins_sha256'] == '0b2fb11d1b8d2b39716d9e19a3c61f4340ec21c7e15b3242e11f7d4ccfe660e5', 'linear pins mismatch') - require([x['name'] for x in linear['lanes']] == ['redtiny', 'redpatch', 'redqkv', 'greentiny', 'greenpatch', 'greenqkv'], 'six linear lanes required') - for lane in linear['lanes']: - red = lane['name'].startswith('red') - require(lane['guard_exit'] == 0 and lane['child_exit'] == (3 if red else 0) - and lane['component_numeric_pass'] is True, 'linear lane not accepted') - require((lane['source_bitwise_mismatches'] > 0) if red else (lane['source_bitwise_mismatches'] == 0), 'wrong linear numerical outcome') - require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') - pins = dict(FIXED) - pins[args.config] = args.config_sha - pins[linear_path] = cfg['linear_receipt']['sha256'] - pins.update({path: sha for path, sha in LIBRARIES.values()}) - runtime = Path(cfg['comparator_runtime']) - policy = Path(cfg['qualification_policy']) - pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', - policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) - verify_pins(pins) - pins.update(json.loads(runtime.read_text())['files']) - for directory in (REFERENCE, CPU_REFERENCE): - manifest = json.loads((directory / 'manifest.json').read_text()) - require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') - for label, entry in manifest['images'].items(): - require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') - for stage in ('patches', 'features', 'embeddings'): - path = directory / entry[stage]['file'] - require(path.parent == directory, 'fixture path escapes reference') - pins[path] = entry[stage]['sha256'] - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) - require('not found' not in ldd, 'unresolved dependency') - resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) - for soname, (path, _) in LIBRARIES.items(): - # The standalone probe links the backend libraries directly; the umbrella - # libggml is built/pinned but omitted by the linker's --as-needed rule. - if soname == 'libggml.so.0' and soname not in resolved: - continue - require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) - verify_pins(pins) - # Guard code and lane policies are frozen before importing or invoking them. - guard_dir = Path(cfg['guard_dir']) - pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) - verify_pins(pins) - require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py'}, 'guard pin set incomplete') - sys.path.insert(0, str(guard_dir)) - from run import load_policy - from host import Host - from guard import prepare_preflight - require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') - policies = [] - for lane in cfg['lanes']: - p = load_policy(Path(lane['policy']), lane['sha256']) - pins[Path(lane['policy'])] = lane['sha256'] - label = lane['name'].split('-')[0] - h, w = (42, 61) if label == 'carrots' else (23, 34) - out = HOME / 'ds4v-work/radeon-numerical-guard' / p['run_name'] - expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), - str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] - require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') - require('hip_fused_bias_launches=67 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') - sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, - str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} - require(set(p['required_outputs']) == set(sizes), 'wrong output contract') - require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') - policies.append((p, out, label)) - evidence = Path(cfg['evidence']) - require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') - evidence.mkdir() - report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', - 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), - 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'linear_receipt': cfg['linear_receipt'], 'lanes': []} - try: - for lane, (p, out, label) in zip(cfg['lanes'], policies): - guarded_run([str(PYTHON), '-I', str(guard_dir / 'run.py'), '--policy', lane['policy'], - '--policy-sha', lane['sha256'], '--parent-radeon-window-released']) - result = json.loads((out / 'guard.json').read_text()) - require(result['pass'] and result['device_proof_verified'], 'guard/result failed') - require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] - and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', - 'guard report belongs to a different command/policy/outcome') - require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') - for file, meta in result['output_evidence'].items(): - require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') - require(digest(file) == meta['sha256'], 'guard output changed before copying') - pins[Path(file)] = meta['sha256'] - pins[out / 'guard.json'] = digest(out / 'guard.json') - log = (out / 'child.log').read_text() - require(re.findall(r'^hip_fused_bias_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('67', '79691776')], 'ambiguous/incomplete full-tower dispatch') - pins[out / 'child.log'] = digest(out / 'child.log') - report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], - 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], - 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], - 'command': p['command'], 'child_exit': result['exit'], 'outputs': result['output_evidence']}) - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for index, (_, out, label) in enumerate(policies): - for stage in ('features', 'embeddings'): - original = out / f'{label}-{stage}.f32' - copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied output differs from guarded output') - pins[copied] = pins[original] - for stage in ('features', 'embeddings'): - original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied repeat carrots differ') - pins[copied] = pins[original] - codes = {} - for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), - ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: - verify_pins(pins) - with (evidence / f'{name}.log').open('w') as log: - result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], - stdout=log, stderr=subprocess.STDOUT, timeout=120) - require(result.returncode in (0, 3), 'comparator execution failed: ' + name) - codes[name] = result.returncode - report['comparisons'] = codes - report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) - verify_pins(pins) - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - p = policies[-1][0] - host = Host(p) - fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) - try: - st = os.fstat(fd) - require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) - finally: - os.close(fd) - (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') - report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] - except Exception as error: - report['error'] = f'{type(error).__name__}: {error}' - finally: - (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') - print(json.dumps(report, indent=2)) - return 0 if report['pass'] else 3 - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh b/harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh deleted file mode 100644 index daacaa895..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-lt-qualification.sh +++ /dev/null @@ -1,264 +0,0 @@ -#!/usr/bin/env bash -# Pinned clean HIP build; execute only after source-linear regression acceptance. -# Execute only after the three source-linear regressions PASS and an explicitly released idle GPU window. -set -euo pipefail -[[ $(hostname) = soulf && $# = 3 && $3 = --gpu-window-released ]] || { - echo 'Usage on soulf: hip-qualification.sh COMPLETED_TEXT_PROOF_OR_--component-only NEW_EVIDENCE_DIR --gpu-window-released' >&2; exit 2; -} -python="$HOME/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python" -# No inherited device masks, overrides, GGML/DS4/DFLASH controls, LD_PRELOAD, or Python settings. -exec env -i HOME="$HOME" PATH=/usr/bin:/bin LANG=C LC_ALL=C \ - OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MKL_NUM_THREADS=2 \ - "$python" -I - "$1" "$2" "$(realpath "$0")" <<'PY' -import hashlib, json, os, re, signal, subprocess, sys, time -from pathlib import Path - -home = Path.home() -root = home / 'lucebox-ds4v-vision-hipblaslt' -build = Path('/tmp/ds4v-lt-hip-build') -ggml_build = build -binary = build / 'ds4v_vision_probe' -cpu_reference = home / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -reference = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference') -freeze_manifest = Path('/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json') -policy_sha = '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f' -mmproj = home / 'ds4v-work/ds4v-mmproj.gguf' -compare = root / 'server/tools/ds4v_vision/compare.py' -component_only = sys.argv[1] == '--component-only' -text_proof = None if component_only else Path(sys.argv[1]) -evidence, harness = map(Path, sys.argv[2:]) -source_sha = '6137f4305400247fed98d2144634c184e2bc6b13' -pinned = { - binary: '635a49b45d116a62d405898230389f41e77aa51d47437c02ffa647919fd458d7', - compare: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - mmproj: '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - cpu_reference / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - reference / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', - freeze_manifest: '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', - ggml_build / 'ggml/src/libggml-base.so.0': '1bdc3462382f5a208272badc459aee4f1c8c46536241f6e40fdbaee6c7ebeef1', - ggml_build / 'ggml/src/libggml-cpu.so.0': '09c3721cb3e4c3689752fed9163c62dc4e20e5e3136a806a72ef7b544b7f89b3', - ggml_build / 'ggml/src/ggml-hip/libggml-hip.so.0': '6c7f4bf85b8fc98c6b1109de5d2de4b5e9beb09680d47f7fd01414e70c556955', - ggml_build / 'ggml/src/libggml.so.0': 'd71378079c9ea008269964b34e9aa48a06db703e6c69db92b1aa7c803e5eee72', - Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'): '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950', - Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'): 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac', -} - -def check(ok, message): - if not ok: raise RuntimeError(message) - -def digest(path): - with open(path, 'rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() - -def dump(name, value): - (evidence / name).write_text(json.dumps(value, indent=2) + '\n') - -def idle_window(): - bus_env = dict(os.environ, XDG_RUNTIME_DIR=f'/run/user/{os.getuid()}') - state = subprocess.check_output(['systemctl', '--user', 'show', 'deepseek-dflash.service', - '--property=ActiveState', '--property=MainPID'], env=bus_env, text=True) - properties = dict(line.split('=', 1) for line in state.splitlines()) - check(properties.get('ActiveState') in ('inactive', 'failed') and properties.get('MainPID') == '0', - 'operator service is not down') - listeners = subprocess.check_output(['ss', '-ltn'], text=True) - check(not any(len(row.split()) > 3 and row.split()[3].endswith((':8016', ':8217')) - for row in listeners.splitlines()), 'operator or private text port is occupied') - processes = list(Path('/sys/class/kfd/kfd/proc').iterdir()) - check(not processes, 'GPU compute processes already exist') - available = int(next(row.split()[1] for row in Path('/proc/meminfo').read_text().splitlines() - if row.startswith('MemAvailable:'))) * 1024 - check(available >= 8 * 1024**3, 'less than 8 GiB host memory available for component check') - cards = [p for p in Path('/sys/class/drm').glob('card[0-9]*/device') - if (p / 'device').is_file() and (p / 'device').read_text().strip() == '0x744c'] - check(len(cards) == 1, 'expected exactly one RX 7900 XT device') - free_vram = int((cards[0] / 'mem_info_vram_total').read_text()) - int((cards[0] / 'mem_info_vram_used').read_text()) - check(free_vram >= 8 * 1024**3, 'less than 8 GiB discrete VRAM available') - return {'operator': properties, 'kfd_processes': [], 'host_available_bytes': available, - 'discrete_free_vram_bytes': free_vram} - -# No GPU call or evidence mutation before the explicit mode and idle-window gates. -check(source_sha != 'SOURCE_PIN_PENDING' and all(re.fullmatch(r'[0-9a-f]{64}', sha) for sha in pinned.values()), - 'candidate source/binary/library pins are unfinished') -check(not component_only, 'this candidate requires the completed text proof') -if not component_only: - text_proof = text_proof.resolve(strict=True) - allowed = [home / f'{tree}/artifacts/fitter-fix' for tree in - ('lucebox-ds4v-mix-fix', 'lucebox-ds4v-mix-parallel')] - check(text_proof.parent in allowed and text_proof.name.startswith('load-proof-'), 'unexpected text-proof directory') - check((text_proof / 'harness.exit').read_text().strip() == '0', 'private text proof did not complete successfully') - check(bool((text_proof / 'cleanup.txt').read_text().strip()), 'private text proof cleanup missing') - verdict = json.loads((text_proof / 'verdict.json').read_text()) - check(digest(text_proof / 'verdict.json') == 'f4f53d102e3386c45ac619d6c66e1b1dd50b6fd227b092474e310926919a1d5f', 'accepted text verdict changed') - check((text_proof / 'source.sha').read_text().strip() == '707194695703c023a8bf026684102d7c597d15b6', 'accepted text source changed') - check(all(verdict.get(k) == 'PASS' for k in ('text_load_smoke', 'math_answer', 'longer_decode_speculation')), - 'private text verdict is not PASS') - server_pid = int((text_proof / 'server.pid').read_text()) - check(server_pid > 1 and not Path(f'/proc/{server_pid}').exists(), 'private text server PID remains present') -initial_window = idle_window() -check(evidence.is_absolute() and not evidence.exists(), 'provide a fresh absolute evidence directory') -evidence.mkdir() # Parent must already exist; never remove or reuse evidence. -print(f'Evidence: {evidence}', flush=True) -active = None -summary = {'status': 'ISSUES', 'features': 'ISSUES', 'embeddings': 'NOT_QUALIFIED', - 'source_sha': source_sha, 'device': 'hip:0', 'text_proof': str(text_proof) if text_proof else None, - 'scope': '7900XT target-source fidelity only; CPU portability separate; no chat/server acceptance', 'policy_sha256': policy_sha, 'reference': str(reference), 'component_only': component_only, - 'initial_window': initial_window, 'lanes': []} -exit_code = 1 - -def interrupted(signum, frame): - raise InterruptedError(signum) - -signal.signal(signal.SIGINT, interrupted) -signal.signal(signal.SIGTERM, interrupted) - -def memory(): - result = {'monotonic_seconds': time.monotonic(), 'host': Path('/proc/meminfo').read_text()} - result['drm'] = {str(p): p.read_text().strip() for p in Path('/sys/class/drm').glob('card*/device/mem_info_*') - if p.name in ('mem_info_vram_total', 'mem_info_vram_used', 'mem_info_gtt_used')} - return result - -def stop_owned(): - global active - if active is not None: - # This unreaped direct child cannot have its PID reused. Never pkill or signal other processes. - active.terminate() - try: active.wait(timeout=5) - except subprocess.TimeoutExpired: active.kill(); active.wait() - lane = summary['lanes'][-1] - lane.update(exit=active.returncode, stopped_by_harness=True) - (evidence / f"{lane['name']}.exit").write_text(str(active.returncode) + '\n') - dump(f"{lane['name']}.time.json", lane) - active = None - -def run(name, command, timeout=900): - global active - lane = {'name': name, 'command': list(map(str, command))} - summary['lanes'].append(lane) - started = time.monotonic() - with open(evidence / f'{name}.log', 'w') as log, open(evidence / f'{name}.memory.jsonl', 'w') as samples: - active = subprocess.Popen(lane['command'], stdout=log, stderr=subprocess.STDOUT) - lane['pid'] = active.pid - (evidence / f'{name}.pid').write_text(str(active.pid) + '\n') - dump('summary.json', summary) - while True: - pid, status, usage = os.wait4(active.pid, os.WNOHANG) - if pid: - code = os.waitstatus_to_exitcode(status) - active.returncode = code - active = None - lane.update(exit=code, elapsed_seconds=time.monotonic()-started, - user_seconds=usage.ru_utime, system_seconds=usage.ru_stime, max_rss_kib=usage.ru_maxrss) - (evidence / f'{name}.exit').write_text(str(code) + '\n') - dump(f'{name}.time.json', lane) - dump('summary.json', summary) - return code - sample = memory() - try: sample['process_status'] = Path(f'/proc/{active.pid}/status').read_text() - except FileNotFoundError: pass - samples.write(json.dumps(sample) + '\n'); samples.flush() - if time.monotonic()-started > timeout: raise TimeoutError(f'{name} exceeded {timeout}s') - time.sleep(0.5) - -def verify_device(name): - log = (evidence / f'{name}.log').read_text() - check(re.search(r'^backend=ROCm0 requested=hip:0$', log, re.M), f'{name}: not the requested HIP backend') - check(re.search(r'Device 0: [^\n]*7900 XT[^\n]*gfx1100', log), f'{name}: device0 is not 7900 XT/gfx1100') - check(re.search(r'Device 1: [^\n]*gfx1151', log), f'{name}: device1 order changed') - -def verify_lt_dispatch(name): - log = (evidence / f'{name}.log').read_text() - check(re.search(r'^hip_fused_bias_launches=67 retained_workspace_bytes=79691776$', log, re.M), - f'{name}: expected actual source-style HIP operations and retained workspace are missing') - -try: - check(subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() == source_sha, - 'runtime source commit changed') - subprocess.run(['git', '-C', str(root), 'diff', '--quiet', 'HEAD', '--'], check=True) - for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed: {path}') - manifest = json.loads((reference / 'manifest.json').read_text()) - check(set(manifest['images']) == {'carrots', 'corn'}, 'unexpected fixture set') - fixtures = {} - for label, grid in [('carrots', [42, 61]), ('corn', [23, 34])]: - entry = manifest['images'][label] - check(entry['vit_grid'] == grid, f'{label}: grid changed') - for stage in ('patches', 'features', 'embeddings'): - meta = entry[stage]; path = reference / meta['file'] - check(path.parent == reference and digest(path) == meta['sha256'], f'{label}/{stage}: fixture changed') - fixtures[str(path)] = meta - cpu_fixtures = {} - cpu_manifest = json.loads((cpu_reference / 'manifest.json').read_text()) - check(set(cpu_manifest['images']) == {'carrots', 'corn'}, 'unexpected CPU fixture set') - for label, entry in cpu_manifest['images'].items(): - for stage in ('patches', 'features', 'embeddings'): - meta = entry[stage]; path = cpu_reference / meta['file'] - check(path.parent == cpu_reference and digest(path) == meta['sha256'], f'CPU fixture changed: {label}/{stage}') - cpu_fixtures[str(path)] = meta - ldd = subprocess.check_output(['/usr/bin/ldd', str(binary)], text=True) - check('not found' not in ldd and 'libggml-hip.so' in ldd, 'HIP dependency resolution failed') - (evidence / 'ldd.txt').write_text(ldd) - libraries = sorted(set(re.findall(r'(?:=>\s+|^\s*)(/[^\s]+)\s+\(', ldd, re.M))) - software = {path: digest(path) for path in libraries} - software[str(Path(sys.executable).resolve())] = digest(Path(sys.executable).resolve()) - dump('provenance.json', {'source_sha': source_sha, 'pinned': {str(p): s for p, s in pinned.items()}, - 'shared_libraries_and_python': software, 'fixtures': fixtures, 'cpu_fixtures': cpu_fixtures, - 'environment': dict(os.environ), 'python': sys.version, 'harness_sha256': digest(harness), - 'text_proof_files': {p.name: digest(p) for p in text_proof.iterdir() if p.is_file()} if text_proof else {}}) - dump('memory-before.json', memory()) - dump('window-before-gpu.json', idle_window()) - check(run('device-check', [binary, mmproj, '--load-only', '4096', '129280', 'hip:0']) == 0, - 'HIP load-only/device check failed') - verify_device('device-check') - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for label, h, w, output, lane in [('carrots', 42, 61, native, 'carrots'), - ('corn', 23, 34, native, 'corn'), ('corn', 23, 34, repeat, 'corn-repeat')]: - dump(f'{lane}.window-before.json', idle_window()) - check(run(lane, [binary, mmproj, reference / f'{label}-patches.f32', str(h), str(w), output, label, '0', 'hip:0']) == 0, - f'{lane}: HIP encode failed') - verify_device(lane) - verify_lt_dispatch(lane) - dump(f'{lane}.window-after.json', idle_window()) - # compare.py requires both fixture names: explicitly reuse the first carrots result in repeat comparison. - for stage in ('features', 'embeddings'): - (repeat / f'carrots-{stage}.f32').symlink_to(native / f'carrots-{stage}.f32') - cpu_statuses = [run(name, [sys.executable, '-I', compare, cpu_reference, output, '--output', evidence / f'{name}.json']) - for name, output in [('native-vs-cpu', native), ('source-hip-vs-cpu', reference)]] - check(all(code in (0, 3) for code in cpu_statuses), 'CPU portability comparison execution/shape/hash failure') - summary['cpu_portability'] = dict(zip(('native_vs_cpu', 'source_hip_vs_cpu'), - ('PASS' if code == 0 else 'ISSUES' for code in cpu_statuses))) - statuses = [run(name, [sys.executable, '-I', compare, reference, output, '--output', evidence / f'{name}.json']) - for name, output in [('comparison', native), ('repeat-comparison', repeat)]] - check(all(code in (0, 3) for code in statuses), 'comparison execution/shape/hash failure') - comparisons = [json.loads((evidence / f'{name}.json').read_text()) for name in ('comparison', 'repeat-comparison')] - summary['features'] = 'PASS' if all(c[label]['features']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' - summary['embeddings'] = 'PASS' if all(c[label]['embeddings']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' - summary['corn_repeat_byte_identical'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') - for s in ('features', 'embeddings')) - dump('outputs.sha256.json', {str(p.relative_to(evidence)): digest(p) for directory in (native, repeat) for p in directory.glob('*.f32')}) - for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed during qualification: {path}') - for path, meta in fixtures.items(): check(digest(path) == meta['sha256'], f'reference changed during qualification: {path}') - for path, meta in cpu_fixtures.items(): check(digest(path) == meta['sha256'], f'CPU reference changed during qualification: {path}') - exit_code = 3 if 3 in statuses or not summary['corn_repeat_byte_identical'] else 0 - summary['status'] = 'PASS' if exit_code == 0 else 'ISSUES' -except InterruptedError as error: - exit_code = 128 + int(error.args[0]); summary['error'] = f'interrupted by signal {error.args[0]}' - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -except TimeoutError as error: - exit_code = 124; summary['error'] = str(error) - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -except Exception as error: - summary['error'] = f'{type(error).__name__}: {error}' - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -finally: - signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGTERM, signal.SIG_IGN) - stop_owned() - # Once the fixed comparator returned 3, no later command may turn that into success or mask it. - if any(lane['name'] in ('comparison', 'repeat-comparison') and lane.get('exit') == 3 for lane in summary['lanes']): - exit_code = 3 - summary['exit'] = exit_code - dump('summary.json', summary) - dump('memory-after.json', memory()) - (evidence / 'harness.exit').write_text(str(exit_code) + '\n') - print(json.dumps(summary, indent=2), flush=True) -raise SystemExit(exit_code) -PY diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py deleted file mode 100644 index fb328182f..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-lt-retry-qualification.py +++ /dev/null @@ -1,243 +0,0 @@ -"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. - -This supervises three sequential Radeon-only lanes through the separate live -operator guard. It makes no isolated performance or HTTP acceptance claim. -""" -import argparse -import fcntl -import hashlib -import json -import os -from pathlib import Path -import re -import shutil -import signal -import stat -import subprocess -import sys - -HOME = Path('/home/marcelorm') -ROOT = HOME / 'lucebox-ds4v-vision-hipblaslt' -BUILD = Path('/tmp/ds4v-lt-hip-build') -SOURCE = '3191e7eaee3f5b4d0caa8e8b228c09a9d52b5f3f' -REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' -CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' -PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' -FIXED = { - BUILD / 'ds4v_vision_probe': '71515dbb84a48a764ed109ae55bfe94ebc40f1327bac0ddd9c49ab2268e32be3', - COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', - CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', -} -LIBRARIES = { - 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', '1bdc3462382f5a208272badc459aee4f1c8c46536241f6e40fdbaee6c7ebeef1'), - 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '09c3721cb3e4c3689752fed9163c62dc4e20e5e3136a806a72ef7b544b7f89b3'), - 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '6c7f4bf85b8fc98c6b1109de5d2de4b5e9beb09680d47f7fd01414e70c556955'), - 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', '0c7a845a117b3f57b27b10b7a5a8e8821631e1e7be17374d1e758c6ab407f719'), - 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), - 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), -} - -def require(ok, why): - if not ok: - raise RuntimeError(why) - -def digest(path): - with Path(path).open('rb') as stream: - return hashlib.file_digest(stream, 'sha256').hexdigest() - -def verify_pins(pins): - for path, sha in pins.items(): - require(digest(path) == sha, 'pin changed: ' + str(path)) - -def guarded_run(command): - child = subprocess.Popen(command) - previous = {} - def interrupted(signum, frame): - raise InterruptedError(f'qualification interrupted: {signum}') - try: - for signum in (signal.SIGTERM, signal.SIGINT): - previous[signum] = signal.signal(signum, interrupted) - require(child.wait() == 0, 'lane supervision failed') - finally: - # The guard handles SIGTERM by stopping/reaping its direct GPU child. - # Never kill the guard while it might still own a live GPU process. - if child.poll() is None: - child.terminate() - child.wait(timeout=30) - for signum, handler in previous.items(): - signal.signal(signum, handler) - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--config', type=Path, required=True) - parser.add_argument('--config-sha', required=True) - parser.add_argument('--parent-radeon-window-released', action='store_true') - args = parser.parse_args() - require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') - require(digest(args.config) == args.config_sha, 'config changed') - cfg = json.loads(args.config.read_text()) - require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') - linear_path = Path(cfg['linear_receipt']['path']) - require(digest(linear_path) == cfg['linear_receipt']['sha256'], 'linear acceptance receipt changed') - linear = json.loads(linear_path.read_text()) - require(linear.get('schema') == 'ds4v-lt-concurrent-linear-proof-v1' and linear.get('pass') is True - and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') - require(linear['source_commit'] == SOURCE and linear['red_commit'] == 'cce69498d6b01541d06fcf77364f68fdbee4627d', 'linear source mismatch') - require(linear['pins_sha256'] == '832017215ddea57d145e95e31564c1ef7444020a75ba4c9092cefced7b23241b', 'linear pins mismatch') - require([x['name'] for x in linear['lanes']] == ['redtiny', 'redpatch', 'redqkv', 'greentiny', 'greenpatch', 'greenqkv'], 'six linear lanes required') - for lane in linear['lanes']: - red = lane['name'].startswith('red') - require(lane['guard_exit'] == 0 and lane['child_exit'] == (3 if red else 0) - and lane['component_numeric_pass'] is True, 'linear lane not accepted') - require((lane['source_bitwise_mismatches'] > 0) if red else (lane['source_bitwise_mismatches'] == 0), 'wrong linear numerical outcome') - require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') - pins = dict(FIXED) - pins[args.config] = args.config_sha - pins[linear_path] = cfg['linear_receipt']['sha256'] - pins.update({path: sha for path, sha in LIBRARIES.values()}) - runtime = Path(cfg['comparator_runtime']) - policy = Path(cfg['qualification_policy']) - pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', - policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) - verify_pins(pins) - pins.update(json.loads(runtime.read_text())['files']) - for directory in (REFERENCE, CPU_REFERENCE): - manifest = json.loads((directory / 'manifest.json').read_text()) - require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') - for label, entry in manifest['images'].items(): - require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') - for stage in ('patches', 'features', 'embeddings'): - path = directory / entry[stage]['file'] - require(path.parent == directory, 'fixture path escapes reference') - pins[path] = entry[stage]['sha256'] - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) - require('not found' not in ldd, 'unresolved dependency') - resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) - for soname, (path, _) in LIBRARIES.items(): - # The standalone probe links the backend libraries directly; the umbrella - # libggml is built/pinned but omitted by the linker's --as-needed rule. - if soname == 'libggml.so.0' and soname not in resolved: - continue - require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) - verify_pins(pins) - # Guard code and lane policies are frozen before importing or invoking them. - guard_dir = Path(cfg['guard_dir']) - require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') - pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) - verify_pins(pins) - require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') - sys.path.insert(0, str(guard_dir)) - from run import load_policy, launch_command - from host import Host - from guard import prepare_preflight - require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') - policies = [] - for lane in cfg['lanes']: - p = load_policy(Path(lane['policy']), lane['sha256']) - pins[Path(lane['policy'])] = lane['sha256'] - isolation_pins = p.get('isolation_pins', {}) - require('/usr/bin/python3.12' in isolation_pins and - all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), - 'namespace runtime is not included in guarded component pins') - for path, sha in p['component_pins'].items(): - require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) - pins[Path(path)] = sha - label = lane['name'].split('-')[0] - h, w = (42, 61) if label == 'carrots' else (23, 34) - out = guard_dir / p['run_name'] - expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), - str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] - require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') - require('hip_fused_bias_launches=67 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') - sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, - str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} - require(set(p['required_outputs']) == set(sizes), 'wrong output contract') - require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') - policies.append((p, out, label)) - evidence = Path(cfg['evidence']) - require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') - evidence.mkdir() - report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', - 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), - 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'linear_receipt': cfg['linear_receipt'], 'lanes': []} - try: - for lane, (p, out, label) in zip(cfg['lanes'], policies): - guarded_run([str(PYTHON), '-I', str(guard_dir / 'run.py'), '--policy', lane['policy'], - '--policy-sha', lane['sha256'], '--parent-radeon-window-released']) - result = json.loads((out / 'guard.json').read_text()) - require(result['pass'] and result['device_proof_verified'], 'guard/result failed') - require(result.get('namespace_verified') is True, 'private NPU namespace not verified') - require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') - require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] - and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', - 'guard report belongs to a different command/policy/outcome') - require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') - for file, meta in result['output_evidence'].items(): - require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') - require(digest(file) == meta['sha256'], 'guard output changed before copying') - pins[Path(file)] = meta['sha256'] - pins[out / 'guard.json'] = digest(out / 'guard.json') - log = (out / 'child.log').read_text() - require(re.findall(r'^hip_fused_bias_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('67', '79691776')], 'ambiguous/incomplete full-tower dispatch') - pins[out / 'child.log'] = digest(out / 'child.log') - report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], - 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], - 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], - 'command': p['command'], 'launch_command': result['launch_command'], - 'namespace_verified': result['namespace_verified'], - 'child_exit': result['exit'], 'outputs': result['output_evidence']}) - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for index, (_, out, label) in enumerate(policies): - for stage in ('features', 'embeddings'): - original = out / f'{label}-{stage}.f32' - copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied output differs from guarded output') - pins[copied] = pins[original] - for stage in ('features', 'embeddings'): - original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied repeat carrots differ') - pins[copied] = pins[original] - codes = {} - for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), - ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: - verify_pins(pins) - with (evidence / f'{name}.log').open('w') as log: - result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], - stdout=log, stderr=subprocess.STDOUT, timeout=120) - require(result.returncode in (0, 3), 'comparator execution failed: ' + name) - codes[name] = result.returncode - report['comparisons'] = codes - report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) - verify_pins(pins) - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - p = policies[-1][0] - host = Host(p) - fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) - try: - st = os.fstat(fd) - require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) - finally: - os.close(fd) - (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') - report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] - except Exception as error: - report['error'] = f'{type(error).__name__}: {error}' - finally: - (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') - print(json.dumps(report, indent=2)) - return 0 if report['pass'] else 3 - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py deleted file mode 100644 index c72835924..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-norm-qualification.py +++ /dev/null @@ -1,297 +0,0 @@ -"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. - -This supervises three sequential Radeon-only lanes through the separate live -operator guard. It makes no isolated performance or HTTP acceptance claim. -""" -import argparse -import fcntl -import hashlib -import json -import os -from pathlib import Path -import re -import shutil -import signal -import stat -import subprocess -import sys - -HOME = Path('/home/marcelorm') -ROOT = HOME / 'lucebox-ds4v-vision-norm' -BUILD = Path('/tmp/ds4v-norm-hip-build') -SOURCE = 'ed661d01a5dfebb02009f23816aec18f8cc87178' -REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' -CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' -PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' -SUPERVISOR = HOME / 'ds4v-work/hipblaslt-stage-diagnostic/log-snapshot-guard.py' -SUPERVISOR_SHA = '8591d28b0b11a1531fe2a657080958cbf401fda9a19dd18d747c09e5edbd06cb' -LANES = ['tiny', 'patch', 'qkv', 'mlp_w1', 'mlp_w2', 'norm782', 'norm2562'] -FIXED = { - SUPERVISOR: SUPERVISOR_SHA, - BUILD / 'ds4v_vision_probe': '75215a951ca640e250c040e31010bb202723da89774f992566525e8c56f9c332', - COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', - CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', -} -LIBRARIES = { - 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', 'bdaf7f896e931898241e2f77775511d1a959af9293e0eb8c2c3d2c323095a253'), - 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '1622da3087c042dcd5bcf27487890a8301a9baaa4f57fe11c98c40b1898e6e10'), - 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', 'bb296f9dd83d5ea5a9dbf844e02e8ac426358d7035fb9db1642d6a8f63eae462'), - 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', 'e0e2a256cdeac6139f779a417ae11fed414e92b43781c16a0835c003054d680c'), - 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), - 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), -} - -def require(ok, why): - if not ok: - raise RuntimeError(why) - -def digest(path): - with Path(path).open('rb') as stream: - return hashlib.file_digest(stream, 'sha256').hexdigest() - -def verify_pins(pins): - for path, sha in pins.items(): - require(re.fullmatch('[0-9a-f]{64}', sha) is not None, 'unbound or invalid pin: ' + str(path)) - require(digest(path) == sha, 'pin changed: ' + str(path)) - -def guarded_run(command, log): - child = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT) - previous = {} - def interrupted(signum, frame): - raise InterruptedError(f'qualification interrupted: {signum}') - try: - for signum in (signal.SIGTERM, signal.SIGINT): - previous[signum] = signal.signal(signum, interrupted) - require(child.wait() == 0, 'lane supervision failed') - finally: - # The guard handles SIGTERM by stopping/reaping its direct GPU child. - # Never kill the guard while it might still own a live GPU process. - if child.poll() is None: - child.terminate() - child.wait(timeout=30) - for signum, handler in previous.items(): - signal.signal(signum, handler) - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--config', type=Path, required=True) - parser.add_argument('--config-sha', required=True) - parser.add_argument('--parent-radeon-window-released', action='store_true') - args = parser.parse_args() - require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') - require(re.fullmatch('[0-9a-f]{40}', SOURCE) is not None, 'source commit is not bound') - require(digest(args.config) == args.config_sha, 'config changed') - cfg = json.loads(args.config.read_text()) - require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') - production_path = Path(cfg['production_pins']['path']) - production_sha = cfg['production_pins']['sha256'] - require(re.fullmatch('[0-9a-f]{64}', production_sha) is not None - and digest(production_path) == production_sha, 'production pin manifest changed') - production = json.loads(production_path.read_text()) - require(production.get('schema') == 'ds4v-norm-production-runtime-v1' - and production.get('source_root') == str(ROOT) - and production.get('source_commit') == SOURCE and isinstance(production.get('files'), dict) - and production['files'], 'production source/file pins missing') - linear_path = Path(cfg['production_receipt']['path']) - require(digest(linear_path) == cfg['production_receipt']['sha256'], 'linear acceptance receipt changed') - linear = json.loads(linear_path.read_text()) - require(linear.get('schema') == 'ds4v-norm-native-production-proof-v1' and linear.get('pass') is True - and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') - require(linear['source_commit'] == SOURCE, 'linear source mismatch') - require(linear['pins_sha256'] == production_sha, 'linear production pins mismatch') - require([x['name'] for x in linear['lanes']] == LANES, 'seven production prerequisite lanes required') - require(linear['supervisor'] == production['supervisor'] == {'path': str(SUPERVISOR), 'sha256': SUPERVISOR_SHA}, - 'logging supervisor differs from accepted prerequisites') - require(linear['source_norm'] == production['source_norm'], 'normalization source provenance mismatch') - require(linear['source_acceptance'] == production['source_acceptance'] == { - 'path': str(SUPERVISOR.parent / 'unbiased-source-acceptance-v1.json'), - 'sha256': '1d0d59f2b3ffc21a366df50ef7257fe6ac2a1eca78cbea10988df3f2398bdbfa'}, 'source acceptance changed') - for lane in linear['lanes']: - require(lane['guard_exit'] == 0 and lane['child_exit'] == 0 - and lane['component_numeric_pass'] is True, 'linear lane not accepted') - require(lane['source_bitwise_mismatches'] == 0, 'production linear differs from frozen source') - norm = lane['name'].startswith('norm') - expected_dispatch = 0 if norm else 1 if lane['name'].startswith('mlp_') else 2 - require(lane['explicit_ops'] == lane['actual_lt_launches'] == expected_dispatch, - 'wrong production linear dispatch count') - require(lane['explicit_norm_ops'] == lane['actual_norm_launches'] == (1 if norm else 0), - 'wrong production normalization dispatch count') - if norm: - require(lane['reference_layout'] == ('original' if lane['name'] == 'norm782' else 'rowwise-tiled-original'), - 'normalization reference layout changed') - require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') - pins = dict(FIXED) - pins[args.config] = args.config_sha - pins[linear_path] = cfg['production_receipt']['sha256'] - pins[production_path] = production_sha - pins.update({path: sha for path, sha in LIBRARIES.values()}) - for path, sha in production['files'].items(): - require(Path(path).is_absolute(), 'absolute production file pin required') - require(Path(path) not in pins or pins[Path(path)] == sha, 'production pin conflicts with qualification pin: ' + path) - pins[Path(path)] = sha - for path, sha in linear['input_artifact_hashes'].items(): - require(Path(path).is_absolute() and (Path(path) not in pins or pins[Path(path)] == sha), - 'prerequisite provenance conflicts with qualification pins') - pins[Path(path)] = sha - for lane in linear['lanes']: - pins[Path(lane['guard_report_path'])] = lane['guard_report_sha256'] - runtime = Path(cfg['comparator_runtime']) - policy = Path(cfg['qualification_policy']) - pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', - policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) - verify_pins(pins) - for path, sha in json.loads(runtime.read_text())['files'].items(): - require(Path(path) not in pins or pins[Path(path)] == sha, 'comparator runtime pin conflict') - pins[Path(path)] = sha - for directory in (REFERENCE, CPU_REFERENCE): - manifest = json.loads((directory / 'manifest.json').read_text()) - require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') - for label, entry in manifest['images'].items(): - require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') - for stage in ('patches', 'features', 'embeddings'): - path = directory / entry[stage]['file'] - require(path.parent == directory, 'fixture path escapes reference') - pins[path] = entry[stage]['sha256'] - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) - require('not found' not in ldd, 'unresolved dependency') - resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) - for soname, (path, _) in LIBRARIES.items(): - # The standalone probe links the backend libraries directly; the umbrella - # libggml is built/pinned but omitted by the linker's --as-needed rule. - if soname == 'libggml.so.0' and soname not in resolved: - continue - require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) - verify_pins(pins) - # Guard code and lane policies are frozen before importing or invoking them. - guard_dir = Path(cfg['guard_dir']) - require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') - pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) - verify_pins(pins) - require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') - sys.path.insert(0, str(guard_dir)) - from run import load_policy, launch_command - from host import Host - from guard import prepare_preflight - require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') - policies = [] - for lane in cfg['lanes']: - p = load_policy(Path(lane['policy']), lane['sha256']) - pins[Path(lane['policy'])] = lane['sha256'] - isolation_pins = p.get('isolation_pins', {}) - require('/usr/bin/python3.12' in isolation_pins and - all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), - 'namespace runtime is not included in guarded component pins') - for path, sha in p['component_pins'].items(): - require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) - pins[Path(path)] = sha - label = lane['name'].split('-')[0] - h, w = (42, 61) if label == 'carrots' else (23, 34) - out = guard_dir / p['run_name'] - expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), - str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] - require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') - require('hip_vision_linear_launches=131 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') - require('hip_vision_norm_launches=65' in p['required_log_lines'], 'normalization dispatch contract missing') - require(p['component_pins'].get(str(SUPERVISOR)) == SUPERVISOR_SHA, 'logging supervisor is not pinned') - sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, - str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} - require(set(p['required_outputs']) == set(sizes), 'wrong output contract') - require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') - policies.append((p, out, label)) - require(len({str(out) for _, out, _ in policies}) == 3, 'full lane evidence directories must be distinct') - evidence = Path(cfg['evidence']) - require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') - evidence.mkdir() - report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', - 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), - 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'production_receipt': cfg['production_receipt'], - 'production_pins': cfg['production_pins'], 'supervisor': production['supervisor'], 'lanes': []} - try: - for lane, (p, out, label) in zip(cfg['lanes'], policies): - verify_pins(pins) - supervisor_log = evidence / (lane['name'] + '-supervisor.log') - with supervisor_log.open('x') as log: - guarded_run([str(PYTHON), '-I', '-B', str(SUPERVISOR), '--policy', lane['policy'], - '--policy-sha', lane['sha256'], '--parent-radeon-window-released'], log) - pins[supervisor_log] = digest(supervisor_log) - result = json.loads((out / 'guard.json').read_text()) - require(result['pass'] and result['device_proof_verified'], 'guard/result failed') - require(result.get('namespace_verified') is True, 'private NPU namespace not verified') - require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') - require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] - and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', - 'guard report belongs to a different command/policy/outcome') - require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') - for file, meta in result['output_evidence'].items(): - require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') - require(digest(file) == meta['sha256'], 'guard output changed before copying') - pins[Path(file)] = meta['sha256'] - pins[out / 'guard.json'] = digest(out / 'guard.json') - log = (out / 'child.log').read_text() - require(re.findall(r'^hip_vision_linear_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('131', '79691776')], 'ambiguous/incomplete full-tower dispatch') - require(re.findall(r'^hip_vision_norm_launches=(\d+)$', log, re.M) == ['65'], - 'ambiguous/incomplete full-tower normalization dispatch') - pins[out / 'child.log'] = digest(out / 'child.log') - report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], - 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], - 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], - 'command': p['command'], 'launch_command': result['launch_command'], - 'namespace_verified': result['namespace_verified'], - 'actual_lt_launches': 131, 'actual_norm_launches': 65, - 'supervisor_log_path': str(supervisor_log), 'supervisor_log_sha256': pins[supervisor_log], - 'child_exit': result['exit'], 'outputs': result['output_evidence']}) - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for index, (_, out, label) in enumerate(policies): - for stage in ('features', 'embeddings'): - original = out / f'{label}-{stage}.f32' - copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied output differs from guarded output') - pins[copied] = pins[original] - for stage in ('features', 'embeddings'): - original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied repeat carrots differ') - pins[copied] = pins[original] - codes = {} - for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), - ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: - verify_pins(pins) - with (evidence / f'{name}.log').open('w') as log: - result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], - stdout=log, stderr=subprocess.STDOUT, timeout=120) - require(result.returncode in (0, 3), 'comparator execution failed: ' + name) - codes[name] = result.returncode - report['comparisons'] = codes - report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) - verify_pins(pins) - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - p = policies[-1][0] - host = Host(p) - fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) - try: - st = os.fstat(fd) - require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) - finally: - os.close(fd) - (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') - report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] - except Exception as error: - report['error'] = f'{type(error).__name__}: {error}' - finally: - (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') - print(json.dumps(report, indent=2)) - return 0 if report['pass'] else 3 - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md b/harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md deleted file mode 100644 index 0e5da7acb..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-qualification-README.md +++ /dev/null @@ -1,38 +0,0 @@ -# Prepared native HIP qualification - -**Executed: numerical ISSUES (exit3).** The first standalone component run is recorded in `native-hip-first/`. All probe processes completed; corn repeat was byte-identical, but feature and embedding comparisons failed unchanged thresholds. The harness never launches/stops converters, a text server, or the operator service. - -Current deployed harness SHA256 is `152af330bb37f77f8e6f9c795ae46c12f787e8587c42fc210aeb671b205296d5`. It accepts a completed text-proof directory or `--component-only` as argument1, a fresh absolute evidence directory as argument2, and `--gpu-window-released` as argument3. Component-only execution is independently authorized while CPU conversion runs; it makes no text/chat acceptance claim. Both modes require a released window and double checks for inactive operator/PID0, free8016/8217, empty KFD process inventory, and at least8GiB host and discrete VRAM available. Component mode passed independent review in `component-window-review.md`. - -The selected runtime is `4bf727077cf007352997798edc52f32c1f887023` in `soulf:~/lucebox-ds4v-runtime`. The existing `/tmp/ds4v-runtime-hip-build/ds4v_vision_probe` and its three GGML shared libraries are pinned by SHA256 in the harness. Probe SHA256 is `4dc9430cd56ca5afe6e649adfa22ad5065b2c97cede86d0ac0b75b2f9296377e`. The executed device check confirmed hip:0 as Radeon RX7900XT/gfx1100; device1 enumerates as gfx1151. - -For the completed-text-proof mode, run manually with **both exact directories**, never a `latest` pointer: - -```sh -bash /tmp/ds4v-hip-qualification.sh \ - /home/marcelorm/lucebox-ds4v-mix-parallel/artifacts/fitter-fix/load-proof-EXACT_COMPLETED_RUN \ - /home/marcelorm/lucebox-ds4v-runtime/artifacts/hip-qualification-UNIQUE_RUN \ - --gpu-window-released -``` - -These example directory suffixes are placeholders. The text evidence must be an existing `load-proof-*` directory under the serial or parallel converter's `artifacts/fitter-fix`. It must contain `harness.exit=0`, a cleanup record, the passing text/math/speculation verdict and a recorded server PID that is no longer present. The new absolute HIP evidence directory must not exist, and its parent must exist. Gate failures create no HIP evidence and make no GPU call. The explicit release flag records the caller's already-granted GPU window; it is not automatic authorization. - -The harness then: - -1. Uses the existing immutable CPU-reference venv interpreter with `-I` under `env -i`. Only HOME, fixed PATH/locale and two-thread CPU math limits remain. All inherited device masks/overrides, GGML/DS4/DFLASH controls, Python settings and dynamic-loader overrides are removed. -2. Verifies the exact source commit, clean tracked source, pinned binary/libraries/compare script, accepted mmproj SHA256 `58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c`, original reference manifest and every used patch/feature/embedding file. Records resolved transitive shared-library hashes, Python, environment, harness and text-proof file hashes in `provenance.json`. -3. Runs `--load-only 4096 129280 hip:0` and requires the actual log to identify `backend=ROCm0 requested=hip:0`, device0 as 7900 XT/gfx1100 and device1 as gfx1151. Only hip:0 receives a runtime/weight allocation. Device enumeration does not execute the graph on device1. An unavailable HIP backend fails; there is no CPU fallback command. -4. Runs carrots 42×61, corn 23×34, then corn again, each as a separate sequential hip:0 process. Stage dumping is disabled to keep scratch bounded. The existing probe reports weights/scratch/encode duration and exercises its release/error checks. This measures complete probe behavior including its host transfers; it is not a warmed persistent-runtime throughput benchmark. -5. Runs the **unchanged** selected `compare.py` twice. The second native directory contains the new corn result and explicit symlinks to the first carrots result because the comparator requires both names. It does not run carrots twice or replace any reference. Shape, finite, max-absolute, RMSE and cosine measurements remain in `comparison.json` and `repeat-comparison.json`. Corn repeat byte identity is reported separately. - -The fixed gates remain features max-absolute≤0.25, RMSE≤0.03, cosine≥0.9995; embeddings max-absolute≤0.75, RMSE≤0.08, cosine≥0.9990. Either fixed comparison's exit **3** is preserved as the final harness exit even if a later step also fails; it never becomes a success. Features and embeddings have separate summary statuses, so passing embeddings cannot erase feature ISSUES. A repeat-byte mismatch also yields overall ISSUES/exit3; it is separately identified rather than changing the numerical thresholds. Execution/shape/provenance failures remain unqualified, with their error and individual process exits recorded. Interrupted/timed-out runs exit 130/143 or 124 unless an already-recorded numerical exit3 takes precedence. - -Each process has an exact command/PID/log/exit and `*.time.json` containing `wait4` user/system time, wall time and peak RSS KiB. `*.memory.jsonl` samples process status, host memory and DRM VRAM/GTT usage every half-second. DRM observations are device-wide and may include unrelated allocations; they are not an isolated per-process GPU peak. `memory-before.json`, `memory-after.json`, `outputs.sha256.json`, `summary.json` and `harness.exit` complete the evidence. Each process has a 900-second ceiling. - -On interruption or timeout, cleanup terminates only the harness's unreaped direct child whose PID it recorded; after five seconds it may kill that same child. It uses no name matching, port cleanup, GPU reset or other-process signal. Inputs, references, venv, source and libraries are read-only. Output files remain in the fresh evidence directory even on failure. - -Preparation checks: soulf `bash -n` and embedded Python `ast.parse` both passed without executing qualification code or the probe. Parent subsequently reviewed the actual probe/comparator contract, text-verdict schema, pinned inputs, clean environment, PID ownership and preserved numerical failure status, then copied the script to soulf and verified its hash and shell syntax. No qualification PASS, HIP feature parity, maximum-grid behavior or decoder/HTTP acceptance is claimed by preparation. - -## First component result - -`native-hip-first/summary.json` records all commands, PIDs and exits. All native probes exit0; both comparisons exit3. Corn feature cosine0.98593858015, carrots0.99388401645; embedding cosine0.99263256728 and0.99645496479 respectively. All four fail their unchanged cosine gate. Corn repeat is byte-identical. No HTTP/decoder image support is qualified by this result. diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh b/harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh deleted file mode 100644 index 5dbf90e65..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-qualification.sh +++ /dev/null @@ -1,226 +0,0 @@ -#!/usr/bin/env bash -# Standalone component checks may precede text proof in an idle, explicitly released GPU window. -set -euo pipefail -[[ $(hostname) = soulf && $# = 3 && $3 = --gpu-window-released ]] || { - echo 'Usage on soulf: hip-qualification.sh COMPLETED_TEXT_PROOF_OR_--component-only NEW_EVIDENCE_DIR --gpu-window-released' >&2; exit 2; -} -python="$HOME/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python" -# No inherited device masks, overrides, GGML/DS4/DFLASH controls, LD_PRELOAD, or Python settings. -exec env -i HOME="$HOME" PATH=/usr/bin:/bin LANG=C LC_ALL=C \ - OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 MKL_NUM_THREADS=2 \ - "$python" -I - "$1" "$2" "$(realpath "$0")" <<'PY' -import hashlib, json, os, re, signal, subprocess, sys, time -from pathlib import Path - -home = Path.home() -root = home / 'lucebox-ds4v-runtime' -build = Path('/tmp/ds4v-runtime-hip-build') -binary = build / 'ds4v_vision_probe' -reference = home / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -mmproj = home / 'ds4v-work/ds4v-mmproj.gguf' -compare = root / 'server/tools/ds4v_vision/compare.py' -component_only = sys.argv[1] == '--component-only' -text_proof = None if component_only else Path(sys.argv[1]) -evidence, harness = map(Path, sys.argv[2:]) -source_sha = '4bf727077cf007352997798edc52f32c1f887023' -pinned = { - binary: '4dc9430cd56ca5afe6e649adfa22ad5065b2c97cede86d0ac0b75b2f9296377e', - compare: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - mmproj: '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - reference / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - build / 'ggml/src/libggml-base.so.0': '378b6c81052532d19bd86b32de236553cc1e57f824ce35fb733569470786ed29', - build / 'ggml/src/libggml-cpu.so.0': 'b33faf3a600eeff2bea8b692360cff6de397aaf3082ea0c73a9f3a1af9ee70d2', - build / 'ggml/src/ggml-hip/libggml-hip.so.0': 'b8991450ee422983b91cfbfcdf8d6b612e92f62f1128c6cce0c6b3e37ff8ef7e', -} - -def check(ok, message): - if not ok: raise RuntimeError(message) - -def digest(path): - with open(path, 'rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() - -def dump(name, value): - (evidence / name).write_text(json.dumps(value, indent=2) + '\n') - -def idle_window(): - bus_env = dict(os.environ, XDG_RUNTIME_DIR=f'/run/user/{os.getuid()}') - state = subprocess.check_output(['systemctl', '--user', 'show', 'deepseek-dflash.service', - '--property=ActiveState', '--property=MainPID'], env=bus_env, text=True) - properties = dict(line.split('=', 1) for line in state.splitlines()) - check(properties.get('ActiveState') in ('inactive', 'failed') and properties.get('MainPID') == '0', - 'operator service is not down') - listeners = subprocess.check_output(['ss', '-ltn'], text=True) - check(not any(len(row.split()) > 3 and row.split()[3].endswith((':8016', ':8217')) - for row in listeners.splitlines()), 'operator or private text port is occupied') - processes = list(Path('/sys/class/kfd/kfd/proc').iterdir()) - check(not processes, 'GPU compute processes already exist') - available = int(next(row.split()[1] for row in Path('/proc/meminfo').read_text().splitlines() - if row.startswith('MemAvailable:'))) * 1024 - check(available >= 8 * 1024**3, 'less than 8 GiB host memory available for component check') - cards = [p for p in Path('/sys/class/drm').glob('card[0-9]*/device') - if (p / 'device').is_file() and (p / 'device').read_text().strip() == '0x744c'] - check(len(cards) == 1, 'expected exactly one RX 7900 XT device') - free_vram = int((cards[0] / 'mem_info_vram_total').read_text()) - int((cards[0] / 'mem_info_vram_used').read_text()) - check(free_vram >= 8 * 1024**3, 'less than 8 GiB discrete VRAM available') - return {'operator': properties, 'kfd_processes': [], 'host_available_bytes': available, - 'discrete_free_vram_bytes': free_vram} - -# No GPU call or evidence mutation before the explicit mode and idle-window gates. -if not component_only: - text_proof = text_proof.resolve(strict=True) - allowed = [home / f'{tree}/artifacts/fitter-fix' for tree in - ('lucebox-ds4v-mix-fix', 'lucebox-ds4v-mix-parallel')] - check(text_proof.parent in allowed and text_proof.name.startswith('load-proof-'), 'unexpected text-proof directory') - check((text_proof / 'harness.exit').read_text().strip() == '0', 'private text proof did not complete successfully') - check(bool((text_proof / 'cleanup.txt').read_text().strip()), 'private text proof cleanup missing') - verdict = json.loads((text_proof / 'verdict.json').read_text()) - check(all(verdict.get(k) == 'PASS' for k in ('text_load_smoke', 'math_answer', 'longer_decode_speculation')), - 'private text verdict is not PASS') - server_pid = int((text_proof / 'server.pid').read_text()) - check(server_pid > 1 and not Path(f'/proc/{server_pid}').exists(), 'private text server PID remains present') -initial_window = idle_window() -check(evidence.is_absolute() and not evidence.exists(), 'provide a fresh absolute evidence directory') -evidence.mkdir() # Parent must already exist; never remove or reuse evidence. -print(f'Evidence: {evidence}', flush=True) -active = None -summary = {'status': 'ISSUES', 'features': 'ISSUES', 'embeddings': 'NOT_QUALIFIED', - 'source_sha': source_sha, 'device': 'hip:0', 'text_proof': str(text_proof) if text_proof else None, - 'scope': 'standalone component; no chat/server acceptance', 'component_only': component_only, - 'initial_window': initial_window, 'lanes': []} -exit_code = 1 - -def interrupted(signum, frame): - raise InterruptedError(signum) - -signal.signal(signal.SIGINT, interrupted) -signal.signal(signal.SIGTERM, interrupted) - -def memory(): - result = {'monotonic_seconds': time.monotonic(), 'host': Path('/proc/meminfo').read_text()} - result['drm'] = {str(p): p.read_text().strip() for p in Path('/sys/class/drm').glob('card*/device/mem_info_*') - if p.name in ('mem_info_vram_total', 'mem_info_vram_used', 'mem_info_gtt_used')} - return result - -def stop_owned(): - global active - if active is not None: - # This unreaped direct child cannot have its PID reused. Never pkill or signal other processes. - active.terminate() - try: active.wait(timeout=5) - except subprocess.TimeoutExpired: active.kill(); active.wait() - lane = summary['lanes'][-1] - lane.update(exit=active.returncode, stopped_by_harness=True) - (evidence / f"{lane['name']}.exit").write_text(str(active.returncode) + '\n') - dump(f"{lane['name']}.time.json", lane) - active = None - -def run(name, command, timeout=900): - global active - lane = {'name': name, 'command': list(map(str, command))} - summary['lanes'].append(lane) - started = time.monotonic() - with open(evidence / f'{name}.log', 'w') as log, open(evidence / f'{name}.memory.jsonl', 'w') as samples: - active = subprocess.Popen(lane['command'], stdout=log, stderr=subprocess.STDOUT) - lane['pid'] = active.pid - (evidence / f'{name}.pid').write_text(str(active.pid) + '\n') - dump('summary.json', summary) - while True: - pid, status, usage = os.wait4(active.pid, os.WNOHANG) - if pid: - code = os.waitstatus_to_exitcode(status) - active.returncode = code - active = None - lane.update(exit=code, elapsed_seconds=time.monotonic()-started, - user_seconds=usage.ru_utime, system_seconds=usage.ru_stime, max_rss_kib=usage.ru_maxrss) - (evidence / f'{name}.exit').write_text(str(code) + '\n') - dump(f'{name}.time.json', lane) - dump('summary.json', summary) - return code - sample = memory() - try: sample['process_status'] = Path(f'/proc/{active.pid}/status').read_text() - except FileNotFoundError: pass - samples.write(json.dumps(sample) + '\n'); samples.flush() - if time.monotonic()-started > timeout: raise TimeoutError(f'{name} exceeded {timeout}s') - time.sleep(0.5) - -def verify_device(name): - log = (evidence / f'{name}.log').read_text() - check(re.search(r'^backend=ROCm0 requested=hip:0$', log, re.M), f'{name}: not the requested HIP backend') - check(re.search(r'Device 0: [^\n]*7900 XT[^\n]*gfx1100', log), f'{name}: device0 is not 7900 XT/gfx1100') - check(re.search(r'Device 1: [^\n]*gfx1151', log), f'{name}: device1 order changed') - -try: - check(subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() == source_sha, - 'runtime source commit changed') - subprocess.run(['git', '-C', str(root), 'diff', '--quiet', 'HEAD', '--'], check=True) - for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed: {path}') - manifest = json.loads((reference / 'manifest.json').read_text()) - check(set(manifest['images']) == {'carrots', 'corn'}, 'unexpected fixture set') - fixtures = {} - for label, grid in [('carrots', [42, 61]), ('corn', [23, 34])]: - entry = manifest['images'][label] - check(entry['vit_grid'] == grid, f'{label}: grid changed') - for stage in ('patches', 'features', 'embeddings'): - meta = entry[stage]; path = reference / meta['file'] - check(path.parent == reference and digest(path) == meta['sha256'], f'{label}/{stage}: fixture changed') - fixtures[str(path)] = meta - ldd = subprocess.check_output(['/usr/bin/ldd', str(binary)], text=True) - check('not found' not in ldd and 'libggml-hip.so' in ldd, 'HIP dependency resolution failed') - (evidence / 'ldd.txt').write_text(ldd) - libraries = sorted(set(re.findall(r'(?:=>\s+|^\s*)(/[^\s]+)\s+\(', ldd, re.M))) - software = {path: digest(path) for path in libraries} - software[str(Path(sys.executable).resolve())] = digest(Path(sys.executable).resolve()) - dump('provenance.json', {'source_sha': source_sha, 'pinned': {str(p): s for p, s in pinned.items()}, - 'shared_libraries_and_python': software, 'fixtures': fixtures, - 'environment': dict(os.environ), 'python': sys.version, 'harness_sha256': digest(harness), - 'text_proof_files': {p.name: digest(p) for p in text_proof.iterdir() if p.is_file()} if text_proof else {}}) - dump('memory-before.json', memory()) - dump('window-before-gpu.json', idle_window()) - check(run('device-check', [binary, mmproj, '--load-only', '4096', '129280', 'hip:0']) == 0, - 'HIP load-only/device check failed') - verify_device('device-check') - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for label, h, w, output, lane in [('carrots', 42, 61, native, 'carrots'), - ('corn', 23, 34, native, 'corn'), ('corn', 23, 34, repeat, 'corn-repeat')]: - check(run(lane, [binary, mmproj, reference / f'{label}-patches.f32', str(h), str(w), output, label, '0', 'hip:0']) == 0, - f'{lane}: HIP encode failed') - verify_device(lane) - # compare.py requires both fixture names: explicitly reuse the first carrots result in repeat comparison. - for stage in ('features', 'embeddings'): - (repeat / f'carrots-{stage}.f32').symlink_to(native / f'carrots-{stage}.f32') - statuses = [run(name, [sys.executable, '-I', compare, reference, output, '--output', evidence / f'{name}.json']) - for name, output in [('comparison', native), ('repeat-comparison', repeat)]] - check(all(code in (0, 3) for code in statuses), 'comparison execution/shape/hash failure') - comparisons = [json.loads((evidence / f'{name}.json').read_text()) for name in ('comparison', 'repeat-comparison')] - summary['features'] = 'PASS' if all(c[label]['features']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' - summary['embeddings'] = 'PASS' if all(c[label]['embeddings']['pass'] for c in comparisons for label in ('carrots', 'corn')) else 'ISSUES' - summary['corn_repeat_byte_identical'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') - for s in ('features', 'embeddings')) - dump('outputs.sha256.json', {str(p.relative_to(evidence)): digest(p) for directory in (native, repeat) for p in directory.glob('*.f32')}) - for path, sha in pinned.items(): check(digest(path) == sha, f'pinned input changed during qualification: {path}') - exit_code = 3 if 3 in statuses or not summary['corn_repeat_byte_identical'] else 0 - summary['status'] = 'PASS' if exit_code == 0 else 'ISSUES' -except InterruptedError as error: - exit_code = 128 + int(error.args[0]); summary['error'] = f'interrupted by signal {error.args[0]}' - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -except TimeoutError as error: - exit_code = 124; summary['error'] = str(error) - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -except Exception as error: - summary['error'] = f'{type(error).__name__}: {error}' - summary.update(status='ISSUES', features='ISSUES', embeddings='NOT_QUALIFIED') -finally: - signal.signal(signal.SIGINT, signal.SIG_IGN) - signal.signal(signal.SIGTERM, signal.SIG_IGN) - stop_owned() - # Once the fixed comparator returned 3, no later command may turn that into success or mask it. - if any(lane['name'] in ('comparison', 'repeat-comparison') and lane.get('exit') == 3 for lane in summary['lanes']): - exit_code = 3 - summary['exit'] = exit_code - dump('summary.json', summary) - dump('memory-after.json', memory()) - (evidence / 'harness.exit').write_text(str(exit_code) + '\n') - print(json.dumps(summary, indent=2), flush=True) -raise SystemExit(exit_code) -PY diff --git a/harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py b/harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py deleted file mode 100644 index e932c7903..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/hip-unbiased-qualification.py +++ /dev/null @@ -1,263 +0,0 @@ -"""Full-tower numerical qualification. Policies must be frozen/reviewed before launch. - -This supervises three sequential Radeon-only lanes through the separate live -operator guard. It makes no isolated performance or HTTP acceptance claim. -""" -import argparse -import fcntl -import hashlib -import json -import os -from pathlib import Path -import re -import shutil -import signal -import stat -import subprocess -import sys - -HOME = Path('/home/marcelorm') -ROOT = HOME / 'lucebox-ds4v-vision-unbiased' -BUILD = Path('/tmp/ds4v-unbiased-hip-build') -SOURCE = '53bf07d8d327f93fc4a4f479220023907154cb3a' -REFERENCE = HOME / 'ds4v-work/source-rocm210-reference/source-hip-reference' -CPU_REFERENCE = HOME / 'lucebox-ds4v-mix-fix/artifacts/vision-reference' -COMPARE = ROOT / 'server/tools/ds4v_vision/compare.py' -PYTHON = HOME / 'lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python' -FIXED = { - BUILD / 'ds4v_vision_probe': 'd0e610716acd357cf295013d018caa26066d520722a9b35f21ba60013d5b36cc', - COMPARE: '9171b902efce4b53a073d0632ded2347daf7de4b6985baa8bcd9cae1a96d694a', - HOME / 'ds4v-work/ds4v-mmproj.gguf': '58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c', - REFERENCE / 'manifest.json': '677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86', - CPU_REFERENCE / 'manifest.json': '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f', - REFERENCE.parent / 'source-hip-reference-freeze.json': '8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0', -} -LIBRARIES = { - 'libggml-base.so.0': (BUILD / 'ggml/src/libggml-base.so.0', 'ddd61d98d11d5209466e74a803c421079dbddba5dcb92d2acb6c638d55b78278'), - 'libggml-cpu.so.0': (BUILD / 'ggml/src/libggml-cpu.so.0', '744352d31cc70b3f4afcbf17f56f49399056f80b493748f65e05de556572bfe9'), - 'libggml-hip.so.0': (BUILD / 'ggml/src/ggml-hip/libggml-hip.so.0', '17d1f73ffeb9ac6701bc5662b6a888bc35a9545c2e6c8025e46117eb7ad4aa49'), - 'libggml.so.0': (BUILD / 'ggml/src/libggml.so.0', 'a18bc2a1adbebc448c652e79b3e97862fd1b09926044025e339ab951a515ae68'), - 'libhipblaslt.so.1': (Path('/opt/rocm-7.2.4/lib/libhipblaslt.so.1'), '4c89a592944beafe388d21cd491c654820baba3d0c12d35fb8fe7020722bb950'), - 'libamdhip64.so.7': (Path('/opt/rocm-7.2.4/lib/libamdhip64.so.7'), 'f1043337461c8e54ee135e95fa979a7d0e4344676ad5b0554652f844f8f098ac'), -} - -def require(ok, why): - if not ok: - raise RuntimeError(why) - -def digest(path): - with Path(path).open('rb') as stream: - return hashlib.file_digest(stream, 'sha256').hexdigest() - -def verify_pins(pins): - for path, sha in pins.items(): - require(re.fullmatch('[0-9a-f]{64}', sha) is not None, 'unbound or invalid pin: ' + str(path)) - require(digest(path) == sha, 'pin changed: ' + str(path)) - -def guarded_run(command): - child = subprocess.Popen(command) - previous = {} - def interrupted(signum, frame): - raise InterruptedError(f'qualification interrupted: {signum}') - try: - for signum in (signal.SIGTERM, signal.SIGINT): - previous[signum] = signal.signal(signum, interrupted) - require(child.wait() == 0, 'lane supervision failed') - finally: - # The guard handles SIGTERM by stopping/reaping its direct GPU child. - # Never kill the guard while it might still own a live GPU process. - if child.poll() is None: - child.terminate() - child.wait(timeout=30) - for signum, handler in previous.items(): - signal.signal(signum, handler) - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('--config', type=Path, required=True) - parser.add_argument('--config-sha', required=True) - parser.add_argument('--parent-radeon-window-released', action='store_true') - args = parser.parse_args() - require(sys.flags.isolated and args.parent_radeon_window_released, 'explicit numerical release required') - require(digest(args.config) == args.config_sha, 'config changed') - cfg = json.loads(args.config.read_text()) - require(cfg.get('adapter_sha256') == digest(__file__), 'qualification adapter changed') - production_path = Path(cfg['production_pins']['path']) - production_sha = cfg['production_pins']['sha256'] - require(re.fullmatch('[0-9a-f]{64}', production_sha) is not None - and digest(production_path) == production_sha, 'production pin manifest changed') - production = json.loads(production_path.read_text()) - require(production.get('schema') == 'ds4v-unbiased-production-runtime-v1' - and production.get('source_root') == str(ROOT) - and production.get('source_commit') == SOURCE and isinstance(production.get('files'), dict) - and production['files'], 'production source/file pins missing') - linear_path = Path(cfg['linear_receipt']['path']) - require(digest(linear_path) == cfg['linear_receipt']['sha256'], 'linear acceptance receipt changed') - linear = json.loads(linear_path.read_text()) - require(linear.get('schema') == 'ds4v-unbiased-native-linear-proof-v1' and linear.get('pass') is True - and linear.get('scope') == 'numerical-only-concurrent', 'accepted linear numerical proof required') - require(linear['source_commit'] == SOURCE, 'linear source mismatch') - require(linear['pins_sha256'] == production_sha, 'linear production pins mismatch') - require([x['name'] for x in linear['lanes']] == ['tiny', 'patch', 'qkv', 'mlp_w1', 'mlp_w2'], 'five production linear lanes required') - for lane in linear['lanes']: - require(lane['guard_exit'] == 0 and lane['child_exit'] == 0 - and lane['component_numeric_pass'] is True, 'linear lane not accepted') - require(lane['source_bitwise_mismatches'] == 0, 'production linear differs from frozen source') - expected_dispatch = 1 if lane['name'] in ('mlp_w1', 'mlp_w2') else 2 - require(lane['explicit_ops'] == lane['actual_lt_launches'] == expected_dispatch, - 'wrong production linear dispatch count') - require(digest(lane['guard_report_path']) == lane['guard_report_sha256'], 'linear guard evidence changed') - pins = dict(FIXED) - pins[args.config] = args.config_sha - pins[linear_path] = cfg['linear_receipt']['sha256'] - pins[production_path] = production_sha - pins.update({path: sha for path, sha in LIBRARIES.values()}) - for path, sha in production['files'].items(): - require(Path(path).is_absolute(), 'absolute production file pin required') - require(Path(path) not in pins or pins[Path(path)] == sha, 'production pin conflicts with qualification pin: ' + path) - pins[Path(path)] = sha - for lane in linear['lanes']: - pins[Path(lane['guard_report_path'])] = lane['guard_report_sha256'] - runtime = Path(cfg['comparator_runtime']) - policy = Path(cfg['qualification_policy']) - pins.update({runtime: '3a57f18efb7768403c250a316f37b25f3a592a6ef165289e68483c3c357b32cb', - policy: '62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f'}) - verify_pins(pins) - pins.update(json.loads(runtime.read_text())['files']) - for directory in (REFERENCE, CPU_REFERENCE): - manifest = json.loads((directory / 'manifest.json').read_text()) - require(set(manifest['images']) == {'corn', 'carrots'}, 'fixture set changed') - for label, entry in manifest['images'].items(): - require(entry['vit_grid'] == ([23, 34] if label == 'corn' else [42, 61]), 'grid changed') - for stage in ('patches', 'features', 'embeddings'): - path = directory / entry[stage]['file'] - require(path.parent == directory, 'fixture path escapes reference') - pins[path] = entry[stage]['sha256'] - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - ldd = subprocess.check_output(['/usr/bin/ldd', str(BUILD / 'ds4v_vision_probe')], text=True) - require('not found' not in ldd, 'unresolved dependency') - resolved = dict(re.findall(r'^\s*(\S+) => (/\S+) \(', ldd, re.M)) - for soname, (path, _) in LIBRARIES.items(): - # The standalone probe links the backend libraries directly; the umbrella - # libggml is built/pinned but omitted by the linker's --as-needed rule. - if soname == 'libggml.so.0' and soname not in resolved: - continue - require(soname in resolved and Path(resolved[soname]).resolve() == path.resolve(), 'wrong effective library: ' + soname) - verify_pins(pins) - # Guard code and lane policies are frozen before importing or invoking them. - guard_dir = Path(cfg['guard_dir']) - require(guard_dir == HOME / 'ds4v-work/radeon-numerical-retry', 'wrong retry guard directory') - pins.update({guard_dir / name: sha for name, sha in cfg['guard_pins'].items()}) - verify_pins(pins) - require(set(cfg['guard_pins']) == {'guard.py', 'host.py', 'receipt.py', 'run.py', 'private_npu_exec.py'}, 'guard pin set incomplete') - sys.path.insert(0, str(guard_dir)) - from run import load_policy, launch_command - from host import Host - from guard import prepare_preflight - require([x['name'] for x in cfg['lanes']] == ['carrots', 'corn', 'corn-repeat'], 'lane order changed') - policies = [] - for lane in cfg['lanes']: - p = load_policy(Path(lane['policy']), lane['sha256']) - pins[Path(lane['policy'])] = lane['sha256'] - isolation_pins = p.get('isolation_pins', {}) - require('/usr/bin/python3.12' in isolation_pins and - all(p['component_pins'].get(path) == sha for path, sha in isolation_pins.items()), - 'namespace runtime is not included in guarded component pins') - for path, sha in p['component_pins'].items(): - require(Path(path) not in pins or pins[Path(path)] == sha, 'lane pin conflicts with qualification pin: ' + path) - pins[Path(path)] = sha - label = lane['name'].split('-')[0] - h, w = (42, 61) if label == 'carrots' else (23, 34) - out = guard_dir / p['run_name'] - expected = [str(BUILD / 'ds4v_vision_probe'), str(HOME / 'ds4v-work/ds4v-mmproj.gguf'), - str(REFERENCE / f'{label}-patches.f32'), str(h), str(w), str(out), label, '0', 'hip:0'] - require(p['command'] == expected and p['expected_exit'] == 0 and p['role'] == 'candidate', 'wrong lane command/role') - require('hip_vision_linear_launches=131 retained_workspace_bytes=79691776' in p['required_log_lines'], 'dispatch contract missing') - sizes = {str(out / f'{label}-features.f32'): h*w*1024*4, - str(out / f'{label}-embeddings.f32'): ((h+2)//3)*((w+2)//3)*4096*4} - require(set(p['required_outputs']) == set(sizes), 'wrong output contract') - require(all(p['required_outputs'][k]['bytes'] == v for k, v in sizes.items()), 'wrong output dimensions') - policies.append((p, out, label)) - evidence = Path(cfg['evidence']) - require(evidence.is_absolute() and not evidence.exists(), 'fresh evidence required') - evidence.mkdir() - report = {'pass': False, 'scope': 'concurrent numerical-only; CPU portability separate; no HTTP/performance acceptance', - 'source_commit': SOURCE, 'config_sha256': args.config_sha, 'adapter_sha256': digest(__file__), - 'reference': str(REFERENCE), 'policy_sha256': pins[policy], 'linear_receipt': cfg['linear_receipt'], - 'production_pins': cfg['production_pins'], 'lanes': []} - try: - for lane, (p, out, label) in zip(cfg['lanes'], policies): - guarded_run([str(PYTHON), '-I', str(guard_dir / 'run.py'), '--policy', lane['policy'], - '--policy-sha', lane['sha256'], '--parent-radeon-window-released']) - result = json.loads((out / 'guard.json').read_text()) - require(result['pass'] and result['device_proof_verified'], 'guard/result failed') - require(result.get('namespace_verified') is True, 'private NPU namespace not verified') - require(result.get('launch_command') == launch_command(p), 'namespace launcher command changed') - require(result['policy_sha256'] == lane['sha256'] and result['command'] == p['command'] - and result['exit'] == 0 and result['numeric_verdict'] == 'pending-independent-comparison', - 'guard report belongs to a different command/policy/outcome') - require(set(result['output_evidence']) == set(p['required_outputs']), 'guard output set differs') - for file, meta in result['output_evidence'].items(): - require(meta['bytes'] == p['required_outputs'][file]['bytes'] and Path(file).stat().st_size == meta['bytes'], 'guard output size differs') - require(digest(file) == meta['sha256'], 'guard output changed before copying') - pins[Path(file)] = meta['sha256'] - pins[out / 'guard.json'] = digest(out / 'guard.json') - log = (out / 'child.log').read_text() - require(re.findall(r'^hip_vision_linear_launches=(\d+) retained_workspace_bytes=(\d+)$', log, re.M) == [('131', '79691776')], 'ambiguous/incomplete full-tower dispatch') - pins[out / 'child.log'] = digest(out / 'child.log') - report['lanes'].append({'name': lane['name'], 'policy_path': lane['policy'], 'policy_sha256': lane['sha256'], - 'guard_report_path': str(out / 'guard.json'), 'guard_report_sha256': pins[out / 'guard.json'], - 'child_log_path': str(out / 'child.log'), 'child_log_sha256': pins[out / 'child.log'], - 'command': p['command'], 'launch_command': result['launch_command'], - 'namespace_verified': result['namespace_verified'], - 'child_exit': result['exit'], 'outputs': result['output_evidence']}) - native, repeat = evidence / 'native', evidence / 'repeat' - native.mkdir(); repeat.mkdir() - for index, (_, out, label) in enumerate(policies): - for stage in ('features', 'embeddings'): - original = out / f'{label}-{stage}.f32' - copied = (repeat if index == 2 else native) / f'{label}-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied output differs from guarded output') - pins[copied] = pins[original] - for stage in ('features', 'embeddings'): - original, copied = native / f'carrots-{stage}.f32', repeat / f'carrots-{stage}.f32' - shutil.copyfile(original, copied) - require(digest(copied) == pins[original], 'copied repeat carrots differ') - pins[copied] = pins[original] - codes = {} - for name, ref, output in [('target', REFERENCE, native), ('repeat-target', REFERENCE, repeat), - ('native-vs-cpu', CPU_REFERENCE, native), ('source-hip-vs-cpu', CPU_REFERENCE, REFERENCE)]: - verify_pins(pins) - with (evidence / f'{name}.log').open('w') as log: - result = subprocess.run([str(PYTHON), '-I', str(COMPARE), str(ref), str(output), '--output', str(evidence / f'{name}.json')], - stdout=log, stderr=subprocess.STDOUT, timeout=120) - require(result.returncode in (0, 3), 'comparator execution failed: ' + name) - codes[name] = result.returncode - report['comparisons'] = codes - report['repeat_exact'] = all(digest(native / f'corn-{s}.f32') == digest(repeat / f'corn-{s}.f32') for s in ('features', 'embeddings')) - verify_pins(pins) - require(subprocess.check_output(['git', '-C', str(ROOT), 'rev-parse', 'HEAD'], text=True).strip() == SOURCE, 'source changed during qualification') - subprocess.run(['git', '-C', str(ROOT), 'diff', '--quiet', 'HEAD', '--'], check=True) - p = policies[-1][0] - host = Host(p) - fd = os.open(HOME / 'ds4v-work/radeon-component.lock', os.O_RDWR | os.O_NOFOLLOW) - try: - st = os.fstat(fd) - require(stat.S_ISREG(st.st_mode) and st.st_uid == os.getuid() and st.st_nlink == 1, 'untrusted final coordination lock') - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - _, first, second = prepare_preflight(p, host.snapshot, host.process_identity) - finally: - os.close(fd) - (evidence / 'final-operator.json').write_text(json.dumps({'first': first, 'second': second}, indent=2)+'\n') - report['pass'] = codes['target'] == codes['repeat-target'] == 0 and report['repeat_exact'] - except Exception as error: - report['error'] = f'{type(error).__name__}: {error}' - finally: - (evidence / 'summary.json').write_text(json.dumps(report, indent=2)+'\n') - print(json.dumps(report, indent=2)) - return 0 if report['pass'] else 3 - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/harness/qualification/deepseek4/ds4v-vision/how-backend.md b/harness/qualification/deepseek4/ds4v-vision/how-backend.md deleted file mode 100644 index 5dbd51460..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/how-backend.md +++ /dev/null @@ -1,19 +0,0 @@ -# Backend how pass - -PASS. Read-only explorer at07e3284. Inherited session model. - -HTTP entry is server/src/server/http_server.cpp route_request:2127. normalize_chat_messages:1031-1044 retains text blocks and silently discards image_url. render_and_tokenize_request:2043-2056 renders only text. Context validation:2060 counts those tokens. ParsedRequest in http_server.h:269 has original messages and tokens but no image data. GenerateRequest in common/model_backend.h:170 is token-only. DS4 uses the serialworker at http_server.cpp:3917, not the sequence engine. process_job:3926 builds GenerateRequest and dispatches generate/restore_and_generate:4078-4081. - -DeepSeek4Backend::generate_from_state:2342 selects fresh or restored prefill. do_prefill:1861 chunks, may split at snapshot or speculative capture boundaries, embeds at2041-2044, and dispatches paired graph at2086-2111. deepseek4_step_layer_range in deepseek4_graph.cpp accepts F32embeddings and original token IDs independently. It expands embeddings into HC at7080-7086. This is the embedding injection boundary. CpuEmbedder in qwen35/gguf_target_loader.cpp:83-93 rejects IDs outside vocabulary; DS4 ignores that return at backend.cpp:2044. - -Model loader deepseek4_loader.cpp:237-258 drops unknown global tensors. Binding1830-1900 lacks vision, aligner, image delimiters and bias_vl. Ordinary bias binds at1886. DeepSeek4Layer at internal.h:135 needs the image routing bias. - -Routing has several paths. GPU build_moe_routing at graph.cpp:3256-3299; old host hybrid4481-4520; sparse paired host5847-5900; standard layer-major6619-6629; generic layer-range8096-8184. Fused paths5424and5638also use hash routing. Sparse paired path5864explicitly rejects token IDs>=vocab. All image sentinel tokens require learned routing in hash layers. Mixture weights remain unbiased. - -Layer-major attention mask at graph.cpp:2023-2050 masks future rows and rows beyond ordinary sliding window. Reference model.py:283-305 gives image tokens left/right visibility across the complete image span. Raw KVrows already combine prior,current,compressed at1924-1928. Preserve compressed visibility while changing raw-image masking. Graph compressor-boundary recursion6973-7022and backend snapshot/capture splitting must not split an image span. A chunk1fallback is incorrect for vision. - -Token-only cache lookup at http_server.cpp:3159-3179 and3258-3264will collide for different images with equal layouts. Either incorporate image bytes and preprocessing/model identity or disable multimodal reuse initially. prepare_prompt2949may rewrite tokens through PFlash/FlowKV; PPP2149and3094may rearrange. These need a multimodal policy preserving alignment. process_job3951logs message JSON before truncation, so redact dataURLs. - -Backend factory435-455selects monolithic DS4 by default; hybrid expert parallelism lives inside this backend and is distinct from layer split. - -No runtime vision claims. Source parity and image HTTP lanes remain mandatory. diff --git a/harness/qualification/deepseek4/ds4v-vision/how-source.md b/harness/qualification/deepseek4/ds4v-vision/how-source.md deleted file mode 100644 index 947007404..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/how-source.md +++ /dev/null @@ -1,17 +0,0 @@ -# Parent source how pass - -PASS. Source files came from the verified parent checkpoint on soulf. The CPU reference fixtures ran the original Python files with torch 2.10.0+cpu, numpy, Pillow, and safetensors 0.7.0. Both supplied photos produced finite tower outputs and aligned embeddings. The manifest records source file hashes, image hashes, dimensions, and fixture hashes. - -The tower has 32 blocks, width 1024, 16 heads, and 14 by 14 RGB patches. Each block has RMSNorm with epsilon 1e-6, combined QKV projection and bias, full bidirectional attention, output projection and bias, then another RMSNorm and a fused gate/up SwiGLU MLP. The final tower norm also uses epsilon 1e-6. - -The two-dimensional rotary code uses a 64-dimensional head. It splits the head into two halves, each width 32. Height and width frequencies occupy 16 values each within those halves. It does not use adjacent-pair rotation. The positional phase derives from the row-major patch grid. This must match the reference rather than borrowing a different model's RoPE arrangement. - -The aligner pads the patch grid on its bottom and right to a multiple of 3. It gathers nonoverlapping 3 by 3 patches in channel-first unfold order. The resulting input width is 9216. A biased linear maps to 4096, exact GELU follows, and another biased 4096 linear produces language embeddings. - -Preprocessing uses the parent's resize budget, aspect-ratio policy, RGB padding, and pixel normalization. It casts normalized pixels to BF16 before arranging channel-major patches. ImageOps.pad and Pillow's resize behavior are part of the numerical reference. A replacement decoder/resizer needs an empirical comparison, not an assumption that bilinear resizing is equivalent. - -build_image_block introduces compression-alignment padding based on the current prompt position. It interleaves pairs of image rows in N order and carries a separate permutation for aligned image embeddings. Types are start=0, pad=1, image=2, newline=3, end=4. Sentinel IDs are vocabulary size plus type. All sentinel types use image routing, including padding outside the bidirectional start/end interval. - -The reference images are already present on soulf at the parent inference/examples/images directory. Carrots uses a 42 by 61 ViT grid and a 14 by 21 aligner grid. Corn uses a 23 by 34 ViT grid and an 8 by 12 aligner grid. Fixtures cover start positions 0, 1, 2, 3, and 127. - -The reference data stays on soulf under ~/lucebox-ds4v-mix-fix/artifacts/vision-reference. Only the manifest and run log have been copied to this Mac. diff --git a/harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py b/harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py deleted file mode 100644 index 389a031cc..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/mmproj-byte-proof.py +++ /dev/null @@ -1,48 +0,0 @@ -import hashlib -import json -from pathlib import Path -import struct -import sys - -repo, source, output, evidence = map(Path, sys.argv[1:]) -sys.path.insert(0, str(repo / 'server/deps/llama.cpp/gguf-py')) -import gguf - -weight_map = json.loads((source / 'model.safetensors.index.json').read_text())['weight_map'] -selected = {name: shard for name, shard in weight_map.items() - if name.startswith(('vision.', 'aligner.', 'image_'))} -reader = gguf.GGUFReader(output) -assert set(selected) == {tensor.name for tensor in reader.tensors} -assert len(selected) == 267 -assert reader.get_field('general.architecture').contents() == 'deepseek4_vision' -headers = {} -for shard in set(selected.values()): - with (source / shard).open('rb') as handle: - size = struct.unpack(' "$proof/comparison.log" 2>&1 -comparison_exit=$? -set -e -printf '%s\n' "$comparison_exit" > "$proof/comparison.exit" -[ "$comparison_exit" = 3 ] -"$python" - "$proof" <<'PY' -import json, sys -from pathlib import Path -import numpy as np -p=Path(sys.argv[1]) -best=(0,0,0) -for h in range(1,1153): - for w in range(1,1153): - a,b=(h+2)//3,(w+2)//3 - rows=a+a%2 - block=rows*(b+1)+2+(rows//2*(b+1)%2)*2 - if block+3<=384 and h*w>best[0]: - best=(h*w,h,w) -assert best==(3366,6,561),best -values=((np.arange(best[0]*588,dtype=np.int32)%31)-15).astype(np.float32)/16 -values.tofile(p/'maximum-patches.f32') -(p/'maximum-grid.json').write_text(json.dumps(dict(patches=best[0],height=best[1],width=best[2],purpose='largest grid permitted by full block budget, not a resize aspect-policy fixture'))+'\n') -PY -for stages in 0 1; do - /usr/bin/time -v "$probe" "$HOME/ds4v-work/ds4v-mmproj.gguf" "$proof/maximum-patches.f32" 6 561 "$proof/maximum-$stages" maximum "$stages" > "$proof/maximum-$stages.log" 2>&1 -done -"$python" - "$proof" <<'PY' -from pathlib import Path -import hashlib,json,sys -import numpy as np -p=Path(sys.argv[1]); result={} -for stage in ('features','embeddings'): - paths=[p/f'maximum-{i}'/f'maximum-{stage}.f32' for i in (0,1)] - arrays=[np.fromfile(path,np.float32) for path in paths] - assert all(np.isfinite(a).all() for a in arrays) - assert arrays[0].size==(3366*1024 if stage=='features' else 374*4096) - assert np.array_equal(*arrays) - result[stage]=dict(finite=True,observer_invariance='PASS',elements=arrays[0].size,sha256=hashlib.sha256(paths[0].read_bytes()).hexdigest()) -(p/'maximum-verdict.json').write_text(json.dumps(result,indent=2)+'\n') -PY -date -u +%FT%TZ > "$proof/qualification.finished" diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py deleted file mode 100644 index 5476f430e..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/compare-corn-three-way.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -"""CPU-only three-way comparison of already completed corn output files.""" -import hashlib -import json -import os -from pathlib import Path -import sys -import numpy as np - -assert sys.flags.isolated == 1 -for key in ('HIP_VISIBLE_DEVICES','ROCR_VISIBLE_DEVICES','CUDA_VISIBLE_DEVICES'): - assert os.environ.get(key) == '-1' -reference, source, native, output = map(Path, sys.argv[1:]) -assert not output.exists() -manifest_bytes = (reference/'manifest.json').read_bytes() -sha = lambda b: hashlib.sha256(b).hexdigest() -assert sha(manifest_bytes) == '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f' -manifest = json.loads(manifest_bytes) -source_report = json.loads((source/'report.json').read_text()) -assert source_report['script_sha256'] == 'cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c' -assert source_report['device']['name'] == 'Radeon RX 7900 XT' -assert source_report['device']['gcn_arch'].split(':')[0] == 'gfx1100' -assert source_report['image'] == 'corn' -native_sha = {'features':'59bd19a13750d07f7f1018c32c5a43c4ae2cd7a7ff132da3f6201b08c400cc4e', - 'embeddings':'a398c9c10a7b2bbeb63f4b910bf5f71bb9fefd0aa388b276a3bf06ca3e280a06'} -gates = {'features':{'max_abs':.25,'rmse':.03,'cosine':.9995}, - 'embeddings':{'max_abs':.75,'rmse':.08,'cosine':.9990}} -report = {'diagnostic_only':True, 'native_acceptance':'NOT_QUALIFIED', 'image':'corn', - 'script_sha256':sha(Path(__file__).read_bytes()), 'hashes':{}, 'comparisons':{}} -for stage,gate in gates.items(): - expected = manifest['images']['corn'][stage] - raw_cpu = (reference/expected['file']).read_bytes() - raw_source = (source/source_report['outputs'][stage]['file']).read_bytes() - raw_native = (native/('corn-'+stage+'.f32')).read_bytes() - assert sha(raw_cpu) == expected['sha256'] - assert sha(raw_source) == source_report['outputs'][stage]['sha256'] - assert sha(raw_native) == native_sha[stage] - values = {name:np.frombuffer(raw,np.float32).reshape(expected['shape']) - for name,raw in [('original_cpu',raw_cpu),('source_hip',raw_source),('native_hip',raw_native)]} - report['hashes'][stage] = {'original_cpu':sha(raw_cpu),'source_hip':sha(raw_source),'native_hip':sha(raw_native)} - report['comparisons'][stage] = {} - for name,left,right in [('source_hip_vs_original_cpu','source_hip','original_cpu'), - ('native_hip_vs_source_hip','native_hip','source_hip'), - ('native_hip_vs_original_cpu','native_hip','original_cpu')]: - a,b=values[left].astype(np.float64).ravel(),values[right].astype(np.float64).ravel() - d=a-b - row={'shape':expected['shape'],'finite':bool(np.isfinite(a).all() and np.isfinite(b).all()), - 'max_abs':float(np.abs(d).max()),'rmse':float(np.sqrt(np.mean(d*d))), - 'cosine':float(np.dot(a,b)/(np.linalg.norm(a)*np.linalg.norm(b))), - 'exact_fraction':float(np.mean(a==b)), - 'byte_identical':report['hashes'][stage][left]==report['hashes'][stage][right], 'gate':gate} - row['pass']=row['finite'] and row['max_abs']<=gate['max_abs'] and row['rmse']<=gate['rmse'] and row['cosine']>=gate['cosine'] - report['comparisons'][stage][name]=row -output.write_text(json.dumps(report,indent=2)+'\n') -print(json.dumps(report,indent=2)) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt deleted file mode 100644 index d28a76065..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/constraints.txt +++ /dev/null @@ -1,4 +0,0 @@ -torch==2.10.0+rocm7.2.4.lw.git3d3aa833 -numpy==2.5.2 -Pillow==12.3.0 -safetensors==0.7.0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py deleted file mode 100644 index 5acf5c03c..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/cpu-runtime-info.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -"""CPU-only import metadata: deliberately makes no torch.cuda calls.""" -import importlib.metadata -import hashlib -import json -import os -from pathlib import Path -import platform -import sys - -assert sys.flags.isolated == 1 -assert sys.prefix != sys.base_prefix -for key in ('HIP_VISIBLE_DEVICES','ROCR_VISIBLE_DEVICES','CUDA_VISIBLE_DEVICES'): - assert os.environ.get(key) == '-1' -private_lib = Path.home()/'ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib' -assert os.environ.get('LD_LIBRARY_PATH') == str(private_lib) -with (private_lib/'libMIOpen.so.1').open('rb') as f: - assert hashlib.file_digest(f,'sha256').hexdigest() == 'bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd' -sys.dont_write_bytecode = True -import torch -import numpy -import PIL -import safetensors -assert importlib.metadata.version('torch') == '2.10.0+rocm7.2.4.lw.git3d3aa833' -assert torch.__version__ == '2.10.0+rocm7.2.4.git3d3aa833' -torch.set_num_threads(2) -torch.set_num_interop_threads(2) -libs = sorted({line.split()[-1] for line in Path('/proc/self/maps').read_text().splitlines() - if '.so' in line and line.split()[-1].startswith('/')}) -report = dict(torch=torch.__version__, torch_git=torch.version.git_version, - hip_version=torch.version.hip, rocm_version=torch.version.rocm, torch_module=torch.__file__, torch_extension=torch._C.__file__, - numpy=numpy.__version__, pillow=PIL.__version__, safetensors=safetensors.__version__, - python=sys.version, prefix=sys.prefix, base_prefix=sys.base_prefix, path=sys.path, - platform=platform.platform(), system_rocm=Path('/opt/rocm/.info/version').read_text().strip(), - venv_config=(Path(sys.prefix)/'pyvenv.cfg').read_text(), - environment={k:os.environ.get(k) for k in ('HIP_VISIBLE_DEVICES','ROCR_VISIBLE_DEVICES','CUDA_VISIBLE_DEVICES', - 'OMP_NUM_THREADS','MKL_NUM_THREADS','OPENBLAS_NUM_THREADS','LD_LIBRARY_PATH','LD_PRELOAD')}, - packages={d.metadata['Name']:d.version for d in importlib.metadata.distributions()}, - loaded_libraries=libs, torch_config=torch.__config__.show(), - parallel_info=torch.__config__.parallel_info(), gpu_api_called=False) -print(json.dumps(report,indent=2,sort_keys=True)) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit deleted file mode 100644 index 573541ac9..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.exit +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log deleted file mode 100644 index af30448e5..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.log +++ /dev/null @@ -1,83 +0,0 @@ -{ - "diagnostic_only": true, - "native_acceptance": "NOT_QUALIFIED", - "device": { - "kind": "cpu", - "gpu_api_called": false - }, - "torch": "2.10.0+rocm7.2.4.git3d3aa833", - "torch_git": "3d3aa833db84eed6b7f5595cb5f162c2f78300a4", - "torch_hip": "7.2.53211", - "numpy": "2.5.2", - "private_library_path": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", - "threads": [ - 2, - 2 - ], - "script_sha256": "17ba9d66260d25c056ecc56a35192748a662b040605bb0bf8864bd704a66267b", - "reference_manifest_sha256": "38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f", - "source_hashes": { - "config.json": "6cd841bdd6702f5e2ac34671bc78047ed80817102465525ae2a41c502abbcd75", - "inference/vision.py": "a4f089069310398d42ca17fd4496cec82da64cbbfde9b0230679ce1537cc0bb1", - "inference/image_processor.py": "cac2ff6af15207ce53d0319dc52c6ed1fa5f4fed21f75795fe0fc014632e9086" - }, - "index_sha256": "507977e3d3818865264e68c0fdab139aa7f3929d0d0cf693dacc47428da56395", - "weight_inventory_sha256": "b0556c40a8bff3f4c2c262d57137a97123cbdbf7444a7fae495ef17cd28469ee", - "image": "corn", - "patch_sha256": "6ef206f7dbe317c0d456de4e9501dc5e734b05d91d7a5341d463187a28233d3a", - "outputs": { - "features": { - "file": "corn-features.f32", - "shape": [ - 782, - 1024 - ], - "sha256": "aa7c43be7182759f83881cf823661bf52c14b73222645d1ec37b14c6502bc982" - }, - "embeddings": { - "file": "corn-embeddings.f32", - "shape": [ - 96, - 4096 - ], - "sha256": "c96d59ae722ad8ac31299aabb4e758b788a1ee4be30ea94b833c753721229040" - } - }, - "comparisons": { - "features": { - "finite": true, - "max_abs": 0.0, - "rmse": 0.0, - "cosine": 1.0, - "exact_fraction": 1.0, - "byte_identical": true, - "gate": { - "max_abs": 0.25, - "rmse": 0.03, - "cosine": 0.9995 - }, - "pass": true - }, - "embeddings": { - "finite": true, - "max_abs": 0.0, - "rmse": 0.0, - "cosine": 0.9999999999999998, - "exact_fraction": 1.0, - "byte_identical": true, - "gate": { - "max_abs": 0.75, - "rmse": 0.08, - "cosine": 0.999 - }, - "pass": true - } - }, - "forward_seconds": 1.8079554990399629, - "attention": "Unchanged original SDPA call, default dispatch; no backend forcing/autocast/compile/patch", - "rotary_context": "cpu", - "default_dtype": "torch.bfloat16", - "source_portability_gate": "PASS", - "elapsed_seconds_after_imports": 4.954526190995239, - "max_rss_kib": 2418408 -} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time deleted file mode 100644 index 7088b89ef..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-corn.time +++ /dev/null @@ -1,23 +0,0 @@ - Command being timed: "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python -I -B /home/marcelorm/ds4v-work/source-rocm210-reference/source-forward.py --device cpu --image corn --source /home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored --reference /home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference --output /home/marcelorm/ds4v-work/source-rocm210-reference/cpu-corn" - User time (seconds): 6.40 - System time (seconds): 1.03 - Percent of CPU this job got: 113% - Elapsed (wall clock) time (h:mm:ss or m:ss): 0:06.58 - Average shared text size (kbytes): 0 - Average unshared data size (kbytes): 0 - Average stack size (kbytes): 0 - Average total size (kbytes): 0 - Maximum resident set size (kbytes): 2418408 - Average resident set size (kbytes): 0 - Major (requiring I/O) page faults: 380 - Minor (reclaiming a frame) page faults: 1013278 - Voluntary context switches: 3482 - Involuntary context switches: 108 - Swaps: 0 - File system inputs: 1848888 - File system outputs: 9496 - Socket messages sent: 0 - Socket messages received: 0 - Signals delivered: 0 - Page size (bytes): 4096 - Exit status: 0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr deleted file mode 100644 index 9378d942a..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-info.stderr +++ /dev/null @@ -1,7 +0,0 @@ -Traceback (most recent call last): - File "/home/marcelorm/ds4v-work/source-rocm210-reference/cpu-runtime-info.py", line 15, in - import torch - File "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/__init__.py", line 431, in - from torch._C import * # noqa: F403 - ^^^^^^^^^^^^^^^^^^^^^^ -ImportError: libMIOpen.so.1: cannot open shared object file: No such file or directory diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-private.stderr b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/cpu-runtime-private.stderr deleted file mode 100644 index e69de29bb..000000000 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log deleted file mode 100644 index bf9a0da6d..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/download.log +++ /dev/null @@ -1,19 +0,0 @@ -3.12.3 (main, Jun 19 2026, 12:46:00) [GCC 13.3.0] -/home/marcelorm/ds4v-work/source-rocm210-reference/.venv -/usr -https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/torch-2.10.0%2Brocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl -wheel bytes 1647409999 sha256 e3a4b7f11eacc4037bc405fbf8beacf2ce19cc135ad283bb653b93a127f379d0 -Name: torch -Version: 2.10.0+rocm7.2.4.lw.git3d3aa833 -Requires-Python: >=3.10 -Requires-Dist: filelock -Requires-Dist: typing-extensions>=4.10.0 -Requires-Dist: setuptools; python_version >= "3.12" -Requires-Dist: sympy>=1.13.3 -Requires-Dist: networkx>=2.5.1 -Requires-Dist: jinja2 -Requires-Dist: fsspec>=0.8.5 -Requires-Dist: triton==3.6.0+rocm7.2.4.git4ed88892; platform_system == "Linux" and platform_machine == "x86_64" -Requires-Dist: optree>=0.13.0; extra == "optree" -Requires-Dist: opt-einsum>=3.3; extra == "opt-einsum" -Requires-Dist: pyyaml; extra == "pyyaml" diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt deleted file mode 100644 index 2f204abc4..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/freeze.txt +++ /dev/null @@ -1,15 +0,0 @@ -filelock==3.32.5 -fsspec==2026.7.0 -Jinja2==3.1.6 -MarkupSafe==3.0.3 -mpmath==1.3.0 -networkx==3.6.1 -numpy==2.5.2 -pillow==12.3.0 -pip==24.0 -safetensors==0.7.0 -setuptools==84.0.0 -sympy==1.14.0 -torch==2.10.0+rocm7.2.4.lw.git3d3aa833 -triton==3.6.0+rocm7.2.4.git4ed88892 -typing_extensions==4.16.0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 deleted file mode 100644 index 0a3e5814a..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/frozen-scripts.sha256 +++ /dev/null @@ -1,3 +0,0 @@ -17ba9d66260d25c056ecc56a35192748a662b040605bb0bf8864bd704a66267b /home/marcelorm/ds4v-work/source-rocm210-reference/source-forward.py -301fac1f88cc476e713e4e0217c298330ec8fda14e8c13958cbcd3121988b2a4 /home/marcelorm/ds4v-work/source-rocm210-reference/cpu-runtime-info.py -8799d8267cf66d7a4d9f22f7e18cb2cf28b6d75f6dad805b4995b4f953ee41c3 /home/marcelorm/ds4v-work/source-rocm210-reference/run-cpu-control.sh diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log deleted file mode 100644 index 9fa591451..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/install.log +++ /dev/null @@ -1,85 +0,0 @@ -Looking in links: /home/marcelorm/ds4v-work/source-rocm210-reference/wheels, https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/ -Collecting torch==2.10.0+rocm7.2.4.lw.git3d3aa833 - File was already downloaded /home/marcelorm/ds4v-work/source-rocm210-reference/wheels/torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl -Collecting numpy==2.5.2 - Downloading numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (6.6 kB) -Collecting Pillow==12.3.0 - Downloading pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl.metadata (9.1 kB) -Collecting safetensors==0.7.0 - Downloading safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (4.1 kB) -Collecting filelock (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading filelock-3.32.5-py3-none-any.whl.metadata (2.0 kB) -Collecting typing-extensions>=4.10.0 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading typing_extensions-4.16.0-py3-none-any.whl.metadata (3.3 kB) -Collecting setuptools (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading setuptools-84.0.0-py3-none-any.whl.metadata (6.6 kB) -Collecting sympy>=1.13.3 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading sympy-1.14.0-py3-none-any.whl.metadata (12 kB) -Collecting networkx>=2.5.1 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading networkx-3.6.1-py3-none-any.whl.metadata (6.8 kB) -Collecting jinja2 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading jinja2-3.1.6-py3-none-any.whl.metadata (2.9 kB) -Collecting fsspec>=0.8.5 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading fsspec-2026.7.0-py3-none-any.whl.metadata (10 kB) -Collecting triton==3.6.0+rocm7.2.4.git4ed88892 (from torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/triton-3.6.0%2Brocm7.2.4.git4ed88892-cp312-cp312-linux_x86_64.whl (298.5 MB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 298.5/298.5 MB 27.4 MB/s eta 0:00:00 -Collecting mpmath<1.4,>=1.1.0 (from sympy>=1.13.3->torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading mpmath-1.3.0-py3-none-any.whl.metadata (8.6 kB) -Collecting MarkupSafe>=2.0 (from jinja2->torch==2.10.0+rocm7.2.4.lw.git3d3aa833) - Downloading markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl.metadata (2.7 kB) -Downloading numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (16.7 MB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.7/16.7 MB 75.3 MB/s eta 0:00:00 -Downloading pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (6.9 MB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.9/6.9 MB 98.9 MB/s eta 0:00:00 -Downloading safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (507 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 507.2/507.2 kB 71.1 MB/s eta 0:00:00 -Downloading fsspec-2026.7.0-py3-none-any.whl (206 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 206.6/206.6 kB 44.7 MB/s eta 0:00:00 -Downloading networkx-3.6.1-py3-none-any.whl (2.1 MB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.1/2.1 MB 60.6 MB/s eta 0:00:00 -Downloading sympy-1.14.0-py3-none-any.whl (6.3 MB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 6.3/6.3 MB 97.5 MB/s eta 0:00:00 -Downloading typing_extensions-4.16.0-py3-none-any.whl (45 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 45.6/45.6 kB 12.7 MB/s eta 0:00:00 -Downloading filelock-3.32.5-py3-none-any.whl (100 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100.0/100.0 kB 24.4 MB/s eta 0:00:00 -Downloading jinja2-3.1.6-py3-none-any.whl (134 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 134.9/134.9 kB 35.8 MB/s eta 0:00:00 -Downloading setuptools-84.0.0-py3-none-any.whl (818 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 818.2/818.2 kB 81.4 MB/s eta 0:00:00 -Downloading markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (22 kB) -Downloading mpmath-1.3.0-py3-none-any.whl (536 kB) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 536.2/536.2 kB 67.2 MB/s eta 0:00:00 -Saved ./ds4v-work/source-rocm210-reference/wheels/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/triton-3.6.0+rocm7.2.4.git4ed88892-cp312-cp312-linux_x86_64.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/fsspec-2026.7.0-py3-none-any.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/networkx-3.6.1-py3-none-any.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/sympy-1.14.0-py3-none-any.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/typing_extensions-4.16.0-py3-none-any.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/filelock-3.32.5-py3-none-any.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/jinja2-3.1.6-py3-none-any.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/setuptools-84.0.0-py3-none-any.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl -Saved ./ds4v-work/source-rocm210-reference/wheels/mpmath-1.3.0-py3-none-any.whl -Successfully downloaded torch numpy Pillow safetensors triton fsspec networkx sympy typing-extensions filelock jinja2 setuptools MarkupSafe mpmath -Looking in links: /home/marcelorm/ds4v-work/source-rocm210-reference/wheels -Processing ./ds4v-work/source-rocm210-reference/wheels/filelock-3.32.5-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 1)) -Processing ./ds4v-work/source-rocm210-reference/wheels/fsspec-2026.7.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 2)) -Processing ./ds4v-work/source-rocm210-reference/wheels/jinja2-3.1.6-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 3)) -Processing ./ds4v-work/source-rocm210-reference/wheels/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 4)) -Processing ./ds4v-work/source-rocm210-reference/wheels/mpmath-1.3.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 5)) -Processing ./ds4v-work/source-rocm210-reference/wheels/networkx-3.6.1-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 6)) -Processing ./ds4v-work/source-rocm210-reference/wheels/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 7)) -Processing ./ds4v-work/source-rocm210-reference/wheels/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 8)) -Processing ./ds4v-work/source-rocm210-reference/wheels/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 9)) -Processing ./ds4v-work/source-rocm210-reference/wheels/setuptools-84.0.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 10)) -Processing ./ds4v-work/source-rocm210-reference/wheels/sympy-1.14.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 11)) -Processing ./ds4v-work/source-rocm210-reference/wheels/torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 12)) -Processing ./ds4v-work/source-rocm210-reference/wheels/triton-3.6.0+rocm7.2.4.git4ed88892-cp312-cp312-linux_x86_64.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 13)) -Processing ./ds4v-work/source-rocm210-reference/wheels/typing_extensions-4.16.0-py3-none-any.whl (from -r /home/marcelorm/ds4v-work/source-rocm210-reference/requirements.lock (line 14)) -Installing collected packages: mpmath, typing_extensions, triton, sympy, setuptools, safetensors, pillow, numpy, networkx, MarkupSafe, fsspec, filelock, Jinja2, torch -Successfully installed Jinja2-3.1.6 MarkupSafe-3.0.3 filelock-3.32.5 fsspec-2026.7.0 mpmath-1.3.0 networkx-3.6.1 numpy-2.5.2 pillow-12.3.0 safetensors-0.7.0 setuptools-84.0.0 sympy-1.14.0 torch-2.10.0+rocm7.2.4.lw.git3d3aa833 triton-3.6.0+rocm7.2.4.git4ed88892 typing_extensions-4.16.0 -No broken requirements found. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt deleted file mode 100644 index 1e14fc6a3..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-dynamic.txt +++ /dev/null @@ -1,53 +0,0 @@ - -Dynamic section at offset 0x15edae48 contains 50 entries: - Tag Type Name/Value - 0x0000000000000001 (NEEDED) Shared library: [libc10_hip.so] - 0x0000000000000001 (NEEDED) Shared library: [libMIOpen.so.1] - 0x0000000000000001 (NEEDED) Shared library: [libhiprtc.so.7] - 0x0000000000000001 (NEEDED) Shared library: [libhipblas.so.3] - 0x0000000000000001 (NEEDED) Shared library: [libhipfft.so.0] - 0x0000000000000001 (NEEDED) Shared library: [libhiprand.so.1] - 0x0000000000000001 (NEEDED) Shared library: [libhipsparse.so.4] - 0x0000000000000001 (NEEDED) Shared library: [libhipsolver.so.1] - 0x0000000000000001 (NEEDED) Shared library: [librocsolver.so.0] - 0x0000000000000001 (NEEDED) Shared library: [libhipsparselt.so.0] - 0x0000000000000001 (NEEDED) Shared library: [libaotriton_v2.so.0.11.1] - 0x0000000000000001 (NEEDED) Shared library: [librccl.so.1] - 0x0000000000000001 (NEEDED) Shared library: [librocm_smi64.so.1] - 0x0000000000000001 (NEEDED) Shared library: [libc10.so] - 0x0000000000000001 (NEEDED) Shared library: [libtorch_cpu.so] - 0x0000000000000001 (NEEDED) Shared library: [libpthread.so.0] - 0x0000000000000001 (NEEDED) Shared library: [librocblas.so.5] - 0x0000000000000001 (NEEDED) Shared library: [libhipblaslt.so.1] - 0x0000000000000001 (NEEDED) Shared library: [libamdhip64.so.7] - 0x0000000000000001 (NEEDED) Shared library: [libmagma.so] - 0x0000000000000001 (NEEDED) Shared library: [libstdc++.so.6] - 0x0000000000000001 (NEEDED) Shared library: [libm.so.6] - 0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1] - 0x0000000000000001 (NEEDED) Shared library: [libc.so.6] - 0x0000000000000001 (NEEDED) Shared library: [ld-linux-x86-64.so.2] - 0x000000000000000e (SONAME) Library soname: [libtorch_hip.so] - 0x000000000000000f (RPATH) Library rpath: [$ORIGIN] - 0x000000000000000c (INIT) 0xcf8000 - 0x000000000000000d (FINI) 0x394f674 - 0x0000000000000019 (INIT_ARRAY) 0x15e0d948 - 0x000000000000001b (INIT_ARRAYSZ) 5744 (bytes) - 0x000000000000001a (FINI_ARRAY) 0x15e0efb8 - 0x000000000000001c (FINI_ARRAYSZ) 8 (bytes) - 0x000000006ffffef5 (GNU_HASH) 0x298 - 0x0000000000000005 (STRTAB) 0x11e5b0 - 0x0000000000000006 (SYMTAB) 0x48818 - 0x000000000000000a (STRSZ) 9342546 (bytes) - 0x000000000000000b (SYMENT) 24 (bytes) - 0x0000000000000003 (PLTGOT) 0x15eed000 - 0x0000000000000002 (PLTRELSZ) 365472 (bytes) - 0x0000000000000014 (PLTREL) RELA - 0x0000000000000017 (JMPREL) 0xc9dcc0 - 0x0000000000000007 (RELA) 0xa193f8 - 0x0000000000000008 (RELASZ) 2640072 (bytes) - 0x0000000000000009 (RELAENT) 24 (bytes) - 0x000000006ffffffe (VERNEED) 0xa19128 - 0x000000006fffffff (VERNEEDNUM) 8 - 0x000000006ffffff0 (VERSYM) 0xa07402 - 0x000000006ffffff9 (RELACOUNT) 81085 - 0x0000000000000000 (NULL) 0x0 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt deleted file mode 100644 index 909161b5f..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-ldd.txt +++ /dev/null @@ -1,45 +0,0 @@ - linux-vdso.so.1 (0x000077139c61b000) - libc10_hip.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10_hip.so (0x000077138649a000) - libMIOpen.so.1 => not found - libhiprtc.so.7 => /opt/rocm/lib/libhiprtc.so.7 (0x00007713863c7000) - libhipblas.so.3 => /opt/rocm/lib/libhipblas.so.3 (0x00007713862eb000) - libhipfft.so.0 => /opt/rocm/lib/libhipfft.so.0 (0x000077139c5f7000) - libhiprand.so.1 => /opt/rocm/lib/libhiprand.so.1 (0x000077139c5ef000) - libhipsparse.so.4 => /opt/rocm/lib/libhipsparse.so.4 (0x000077139c5aa000) - libhipsolver.so.1 => /opt/rocm/lib/libhipsolver.so.1 (0x00007713862a5000) - librocsolver.so.0 => /opt/rocm/lib/librocsolver.so.0 (0x0000771352600000) - libhipsparselt.so.0 => /opt/rocm/lib/libhipsparselt.so.0 (0x0000771352000000) - libaotriton_v2.so.0.11.1 => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libaotriton_v2.so.0.11.1 (0x000077134ec00000) - librccl.so.1 => /opt/rocm/lib/librccl.so.1 (0x000077132c800000) - librocm_smi64.so.1 => /opt/rocm/lib/librocm_smi64.so.1 (0x00007713524aa000) - libc10.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10.so (0x000077138618f000) - libtorch_cpu.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libtorch_cpu.so (0x0000771317e00000) - libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x000077139c5a3000) - librocblas.so.5 => /opt/rocm/lib/librocblas.so.5 (0x0000771314e00000) - libhipblaslt.so.1 => /opt/rocm/lib/libhipblaslt.so.1 (0x0000771314800000) - libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7 (0x0000771312e00000) - libmagma.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libmagma.so (0x00007712ce600000) - libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007712ce200000) - libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x000077134eb17000) - libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x000077139c571000) - libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007712cde00000) - /lib64/ld-linux-x86-64.so.2 (0x000077139c61d000) - librocfft.so.0 => /opt/rocm/lib/librocfft.so.0 (0x00007712cc400000) - librocrand.so.1 => /opt/rocm/lib/librocrand.so.1 (0x00007712a0800000) - librocsparse.so.1 => /opt/rocm/lib/librocsparse.so.1 (0x0000771283600000) - libroctx64.so.4 => /opt/rocm/lib/libroctx64.so.4 (0x000077139c56a000) - libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x000077139c565000) - liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x000077134eae5000) - librocprofiler-register.so.0 => /opt/rocm/lib/librocprofiler-register.so.0 (0x000077134ea56000) - libnuma.so.1 => /lib/x86_64-linux-gnu/libnuma.so.1 (0x0000771351ff2000) - librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x0000771386188000) - libgomp.so.1 => /lib/x86_64-linux-gnu/libgomp.so.1 (0x0000771317daa000) - libroctracer64.so.4 => /opt/rocm/lib/libroctracer64.so.4 (0x0000771312d91000) - librocroller.so.1 => /opt/rocm/lib/librocroller.so.1 (0x0000771281400000) - libhsa-runtime64.so.1 => /opt/rocm/lib/libhsa-runtime64.so.1 (0x0000771280e00000) - libamd_comgr.so.3 => /opt/rocm/lib/../lib/libamd_comgr.so.3 (0x0000771277400000) - libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x0000771351fd3000) - libdrm.so.2 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm.so.2 (0x000077134ea3d000) - libdrm_amdgpu.so.1 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm_amdgpu.so.1 (0x000077134ea2e000) - libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x000077134ea12000) - libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007712ce546000) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt deleted file mode 100644 index 5072ffcf4..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/libtorch-hip-private-ldd.txt +++ /dev/null @@ -1,46 +0,0 @@ - linux-vdso.so.1 (0x00007b570a556000) - libc10_hip.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10_hip.so (0x00007b570a3e8000) - libMIOpen.so.1 => /home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib/libMIOpen.so.1 (0x00007b56b5200000) - libhiprtc.so.7 => /opt/rocm/lib/libhiprtc.so.7 (0x00007b56f432d000) - libhipblas.so.3 => /opt/rocm/lib/libhipblas.so.3 (0x00007b56b5124000) - libhipfft.so.0 => /opt/rocm/lib/libhipfft.so.0 (0x00007b570a3cc000) - libhiprand.so.1 => /opt/rocm/lib/libhiprand.so.1 (0x00007b570a3c4000) - libhipsparse.so.4 => /opt/rocm/lib/libhipsparse.so.4 (0x00007b570a37f000) - libhipsolver.so.1 => /opt/rocm/lib/libhipsolver.so.1 (0x00007b56f42e7000) - librocsolver.so.0 => /opt/rocm/lib/librocsolver.so.0 (0x00007b5681400000) - libhipsparselt.so.0 => /opt/rocm/lib/libhipsparselt.so.0 (0x00007b5680e00000) - libaotriton_v2.so.0.11.1 => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libaotriton_v2.so.0.11.1 (0x00007b567da00000) - librccl.so.1 => /opt/rocm/lib/librccl.so.1 (0x00007b565b600000) - librocm_smi64.so.1 => /opt/rocm/lib/librocm_smi64.so.1 (0x00007b56b4fce000) - libc10.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libc10.so (0x00007b56812ea000) - libtorch_cpu.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libtorch_cpu.so (0x00007b5646c00000) - libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007b570a378000) - librocblas.so.5 => /opt/rocm/lib/librocblas.so.5 (0x00007b5643c00000) - libhipblaslt.so.1 => /opt/rocm/lib/libhipblaslt.so.1 (0x00007b5643600000) - libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7 (0x00007b5641c00000) - libmagma.so => /home/marcelorm/ds4v-work/source-rocm210-reference/.venv/lib/python3.12/site-packages/torch/lib/libmagma.so (0x00007b55fd400000) - libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007b55fd000000) - libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007b567d917000) - libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007b56b4fa0000) - libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007b55fcc00000) - /lib64/ld-linux-x86-64.so.2 (0x00007b570a558000) - libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007b567d85d000) - libamd_comgr.so.3 => /opt/rocm/lib/libamd_comgr.so.3 (0x00007b55f3200000) - librocm-core.so.1 => /opt/rocm/lib/librocm-core.so.1 (0x00007b570a36f000) - libroctx64.so.4 => /opt/rocm/lib/libroctx64.so.4 (0x00007b570a368000) - librocfft.so.0 => /opt/rocm/lib/librocfft.so.0 (0x00007b55f1800000) - librocrand.so.1 => /opt/rocm/lib/librocrand.so.1 (0x00007b55c5c00000) - librocsparse.so.1 => /opt/rocm/lib/librocsparse.so.1 (0x00007b55a8a00000) - libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007b56f42e2000) - liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007b56812b8000) - librocprofiler-register.so.0 => /opt/rocm/lib/librocprofiler-register.so.0 (0x00007b5641b71000) - libnuma.so.1 => /lib/x86_64-linux-gnu/libnuma.so.1 (0x00007b56f42d4000) - librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007b56f42cf000) - libgomp.so.1 => /lib/x86_64-linux-gnu/libgomp.so.1 (0x00007b5646baa000) - libroctracer64.so.4 => /opt/rocm/lib/libroctracer64.so.4 (0x00007b55fd391000) - librocroller.so.1 => /opt/rocm/lib/librocroller.so.1 (0x00007b55a6800000) - libhsa-runtime64.so.1 => /opt/rocm/lib/libhsa-runtime64.so.1 (0x00007b55a6200000) - libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007b5680de4000) - libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x00007b567d83e000) - libdrm.so.2 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm.so.2 (0x00007b567d825000) - libdrm_amdgpu.so.1 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm_amdgpu.so.1 (0x00007b56b4f8f000) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt deleted file mode 100644 index 3bd0c6183..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-metadata.txt +++ /dev/null @@ -1,18 +0,0 @@ -Package: miopen-hip -Architecture: amd64 -Conflicts: miopen-opencl -Depends: hip-runtime-amd, comgr, roctracer, rocblas, hipblaslt, rocm-core, rocrand, rocm-core -Priority: optional -Section: devel -Filename: pool/main/m/miopen-hip/miopen-hip_3.5.1.70204-93~24.04_amd64.deb -Size: 307969764 -SHA256: b5759989f8d95b367d83309f4d9da3c55c1e5868703aaede1647434df375b6c2 -SHA1: d86fc79231d175df7f833b022487e72d9a0d2a98 -MD5sum: 0109619cddbabe2347e757d157ea9540 -Description: AMD DNN Library -Description-md5: -Maintainer: MIOpen Maintainer -Recommends: miopen-hip-dev (>=3.5.1.70204) -Version: 3.5.1.70204-93~24.04 -Installed-Size: 2931663 - diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt deleted file mode 100644 index b3490c4eb..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-apt-policy.txt +++ /dev/null @@ -1,6 +0,0 @@ -miopen-hip: - Installed: (none) - Candidate: 3.5.1.70204-93~24.04 - Version table: - 3.5.1.70204-93~24.04 600 - 600 https://repo.radeon.com/rocm/apt/7.2.4 noble/main amd64 Packages diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt deleted file mode 100644 index 0158e8dd3..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-ldd.txt +++ /dev/null @@ -1,22 +0,0 @@ - linux-vdso.so.1 (0x000076d7c8a0c000) - libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x000076d7c8941000) - libhiprtc.so.7 => /opt/rocm/lib/libhiprtc.so.7 (0x000076d78972d000) - libamd_comgr.so.3 => /opt/rocm/lib/libamd_comgr.so.3 (0x000076d77fc00000) - librocm-core.so.1 => /opt/rocm/lib/librocm-core.so.1 (0x000076d7c893c000) - librocblas.so.5 => /opt/rocm/lib/librocblas.so.5 (0x000076d77cc00000) - libhipblaslt.so.1 => /opt/rocm/lib/libhipblaslt.so.1 (0x000076d77c600000) - libroctx64.so.4 => /opt/rocm/lib/libroctx64.so.4 (0x000076d7c8935000) - libamdhip64.so.7 => /opt/rocm/lib/libamdhip64.so.7 (0x000076d77ac00000) - libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x000076d77a800000) - libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x000076d789644000) - libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x000076d7c8907000) - libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x000076d77a400000) - /lib64/ld-linux-x86-64.so.2 (0x000076d7c8a0e000) - libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x000076d7c88e9000) - librocroller.so.1 => /opt/rocm/lib/librocroller.so.1 (0x000076d778200000) - librocprofiler-register.so.0 => /opt/rocm/lib/librocprofiler-register.so.0 (0x000076d7895b5000) - libhsa-runtime64.so.1 => /opt/rocm/lib/libhsa-runtime64.so.1 (0x000076d777c00000) - libelf.so.1 => /lib/x86_64-linux-gnu/libelf.so.1 (0x000076d789596000) - libdrm.so.2 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm.so.2 (0x000076d7c88ce000) - libdrm_amdgpu.so.1 => /opt/amdgpu/lib/x86_64-linux-gnu/libdrm_amdgpu.so.1 (0x000076d789587000) - libnuma.so.1 => /lib/x86_64-linux-gnu/libnuma.so.1 (0x000076d789579000) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log deleted file mode 100644 index 3f265bc25..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/miopen-repair.log +++ /dev/null @@ -1,4 +0,0 @@ -Get:1 https://repo.radeon.com/rocm/apt/7.2.4 noble/main amd64 miopen-hip amd64 3.5.1.70204-93~24.04 [308 MB] -Fetched 308 MB in 3s (90.3 MB/s) -verified miopen-hip_3.5.1.70204-93~24.04_amd64.deb b5759989f8d95b367d83309f4d9da3c55c1e5868703aaede1647434df375b6c2 -private library /home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib/libMIOpen.so.1.0.70204 sha256 bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html deleted file mode 100644 index b5a01ebec..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/official-index.html +++ /dev/null @@ -1,140 +0,0 @@ - -Index of /rocm/manylinux/rocm-rel-7.2.4/ - -

Release notes

-For information on available ROCm releases, please refer to the - -ROCm Release Notes

-For information on available Radeon Software for Linux releases, -please refer to - -Linux® Drivers for AMD Radeon™ and Radeon PRO™ Graphics.

- -

Index of /rocm/manylinux/rocm-rel-7.2.4/


../
-apex-1.10.0+rocm7.2.4.git751f5dd5-cp310-cp310-l..> 22-May-2026 03:58            21035408
-apex-1.10.0+rocm7.2.4.git751f5dd5-cp311-cp311-l..> 22-May-2026 02:02            21035409
-apex-1.10.0+rocm7.2.4.git751f5dd5-cp312-cp312-l..> 22-May-2026 02:00            21035407
-apex-1.10.0+rocm7.2.4.git751f5dd5-cp313-cp313-l..> 22-May-2026 01:56            21035407
-apex-1.11.0+rocm7.2.4.gitc0f56f7e-cp312-cp312-l..> 17-Aug-2026 16:01            21043771
-apex-1.7.0+rocm7.2.4.git215398d0-cp310-cp310-li..> 22-May-2026 04:37            96426289
-apex-1.7.0+rocm7.2.4.git215398d0-cp311-cp311-li..> 22-May-2026 03:17            96889282
-apex-1.7.0+rocm7.2.4.git215398d0-cp312-cp312-li..> 22-May-2026 04:38            96929010
-apex-1.7.0+rocm7.2.4.git215398d0-cp313-cp313-li..> 22-May-2026 02:11            96929504
-apex-1.8.0+rocm7.2.4.gitb1302357-cp310-cp310-li..> 21-May-2026 22:00            21035400
-apex-1.8.0+rocm7.2.4.gitb1302357-cp311-cp311-li..> 21-May-2026 19:24            21035400
-apex-1.8.0+rocm7.2.4.gitb1302357-cp312-cp312-li..> 21-May-2026 19:33            21035399
-apex-1.8.0+rocm7.2.4.gitb1302357-cp313-cp313-li..> 21-May-2026 20:58            21035400
-apex-1.9.0+rocm7.2.4.git355db9b8-cp310-cp310-li..> 25-May-2026 01:28            21035401
-apex-1.9.0+rocm7.2.4.git355db9b8-cp311-cp311-li..> 21-May-2026 20:33            21035401
-apex-1.9.0+rocm7.2.4.git355db9b8-cp312-cp312-li..> 25-May-2026 01:19            21035399
-apex-1.9.0+rocm7.2.4.git355db9b8-cp313-cp313-li..> 25-May-2026 01:03            21035401
-jax_rocm7_pjrt-0.8.2+rocm7.2.4-py3-none-linux_x..> 21-May-2026 16:57           193640359
-jax_rocm7_pjrt-0.8.2+rocm7.2.4-py3-none-manylin..> 21-May-2026 16:57           193640343
-jax_rocm7_pjrt-0.8.2+rocm7.2.4-py3-none-manylin..> 21-May-2026 16:57           193640725
-jax_rocm7_plugin-0.8.2+rocm7.2.4-cp311-cp311-li..> 21-May-2026 16:57             7943490
-jax_rocm7_plugin-0.8.2+rocm7.2.4-cp311-cp311-ma..> 21-May-2026 16:57             7943500
-jax_rocm7_plugin-0.8.2+rocm7.2.4-cp311-cp311-ma..> 21-May-2026 16:57             7943776
-jax_rocm7_plugin-0.8.2+rocm7.2.4-cp312-cp312-li..> 21-May-2026 16:57             7938671
-jax_rocm7_plugin-0.8.2+rocm7.2.4-cp312-cp312-ma..> 21-May-2026 16:57             7938677
-jax_rocm7_plugin-0.8.2+rocm7.2.4-cp312-cp312-ma..> 21-May-2026 16:57             7938953
-jaxlib-0.8.2+rocm7.2.4-cp311-cp311-linux_x86_64..> 21-May-2026 16:57            90176250
-jaxlib-0.8.2+rocm7.2.4-cp311-cp311-manylinux_2_..> 21-May-2026 16:57            90176259
-jaxlib-0.8.2+rocm7.2.4-cp311-cp311-manylinux_2_..> 21-May-2026 16:57            90178532
-jaxlib-0.8.2+rocm7.2.4-cp312-cp312-linux_x86_64..> 21-May-2026 16:57            90187829
-jaxlib-0.8.2+rocm7.2.4-cp312-cp312-manylinux_2_..> 21-May-2026 16:57            90187837
-jaxlib-0.8.2+rocm7.2.4-cp312-cp312-manylinux_2_..> 21-May-2026 16:57            90190110
-onnxruntime_migraphx-1.23.2-cp310-cp310-manylin..> 21-May-2026 16:19            20597183
-onnxruntime_migraphx-1.23.2-cp310-cp310-manylin..> 21-May-2026 16:19           498983680
-onnxruntime_migraphx-1.23.2-cp312-cp312-manylin..> 21-May-2026 16:11            20599434
-onnxruntime_migraphx-1.23.2-cp312-cp312-manylin..> 21-May-2026 16:11           499060628
-tensorflow_rocm-2.18.1-cp310-cp310-manylinux_2_..> 21-May-2026 15:01           494043826
-tensorflow_rocm-2.18.1-cp312-cp312-manylinux_2_..> 21-May-2026 14:31           494346732
-tensorflow_rocm-2.19.1-cp310-cp310-manylinux_2_..> 21-May-2026 14:48           523401732
-tensorflow_rocm-2.19.1-cp312-cp312-manylinux_2_..> 21-May-2026 18:57           523705480
-tensorflow_rocm-2.20.0.dev0+selfbuilt-cp310-cp3..> 21-May-2026 14:16           500660116
-tensorflow_rocm-2.20.0.dev0+selfbuilt-cp312-cp3..> 21-May-2026 14:18           500942603
-tf_nightly_rocm-2.21.0.dev0+selfbuilt-cp310-cp3..> 21-May-2026 14:42           526713985
-tf_nightly_rocm-2.21.0.dev0+selfbuilt-cp312-cp3..> 21-May-2026 15:06           527116041
-torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp310-cp3..> 22-May-2026 03:58          1647327128
-torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp311-cp3..> 22-May-2026 02:02          1647356819
-torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp3..> 22-May-2026 02:00          1647409999
-torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp313-cp3..> 22-May-2026 01:56          1647417939
-torch-2.11.0+rocm7.2.4.lw.git5fbd98f3-cp312-cp3..> 17-Aug-2026 16:01          1661396411
-torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp310-cp31..> 22-May-2026 04:37          1157090225
-torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp311-cp31..> 22-May-2026 03:17          1157111687
-torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp312-cp31..> 22-May-2026 04:38          1156966596
-torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp313-cp31..> 22-May-2026 02:11          1156973421
-torch-2.7.1+rocm7.2.4.lw.git1dab218d-cp39-cp39-..> 22-May-2026 02:22          1157085617
-torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp310-cp31..> 21-May-2026 22:00          1546165243
-torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp311-cp31..> 21-May-2026 19:24          1546186143
-torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp312-cp31..> 21-May-2026 19:33          1546039806
-torch-2.8.0+rocm7.2.4.lw.git6bea3e0b-cp313-cp31..> 21-May-2026 20:58          1546046386
-torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp310-cp31..> 13-Aug-2026 19:25          1546155487
-torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp311-cp31..> 13-Aug-2026 19:25          1546176166
-torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp312-cp31..> 13-Aug-2026 19:28          1546031167
-torch-2.8.0+rocm7.2.4.lw.gitd618db82-cp313-cp31..> 13-Aug-2026 19:20          1546038149
-torch-2.9.1+rocm7.2.4.lw.git39497456-cp310-cp31..> 25-May-2026 01:28          1650524563
-torch-2.9.1+rocm7.2.4.lw.git39497456-cp311-cp31..> 21-May-2026 20:33          1650547330
-torch-2.9.1+rocm7.2.4.lw.git39497456-cp312-cp31..> 25-May-2026 01:19          1650466828
-torch-2.9.1+rocm7.2.4.lw.git39497456-cp313-cp31..> 25-May-2026 01:03          1650467274
-torchaudio-2.10.0+rocm7.2.4.git5047768f-cp310-c..> 22-May-2026 03:58              409174
-torchaudio-2.10.0+rocm7.2.4.git5047768f-cp311-c..> 22-May-2026 02:02              410596
-torchaudio-2.10.0+rocm7.2.4.git5047768f-cp312-c..> 22-May-2026 02:01              410816
-torchaudio-2.10.0+rocm7.2.4.git5047768f-cp313-c..> 22-May-2026 01:56              411175
-torchaudio-2.11.0+rocm7.2.4.git143129b5-cp312-c..> 17-Aug-2026 16:02             1542328
-torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp310-cp..> 22-May-2026 04:37             1795413
-torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp311-cp..> 22-May-2026 03:17             1802570
-torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp312-cp..> 22-May-2026 04:39             1802292
-torchaudio-2.7.1+rocm7.2.4.git95c61b41-cp313-cp..> 22-May-2026 02:11             1802009
-torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp310-cp..> 21-May-2026 22:01             1806058
-torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp311-cp..> 21-May-2026 19:24             1813761
-torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp312-cp..> 21-May-2026 19:33             1813780
-torchaudio-2.8.0+rocm7.2.4.git6e1c7fe9-cp313-cp..> 21-May-2026 20:58             1813690
-torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp310-cp..> 25-May-2026 01:28              487390
-torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp311-cp..> 21-May-2026 20:33              488959
-torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp312-cp..> 25-May-2026 01:20              488600
-torchaudio-2.9.0+rocm7.2.4.gite3c6ee2b-cp313-cp..> 25-May-2026 01:04              488428
-torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp310-..> 22-May-2026 04:37             3039072
-torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp311-..> 22-May-2026 03:17             3040859
-torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp312-..> 22-May-2026 04:39             3042060
-torchvision-0.22.1+rocm7.2.4.git59a3e1f9-cp313-..> 22-May-2026 02:11             3042184
-torchvision-0.23.0+rocm7.2.4.git824e8c87-cp310-..> 21-May-2026 22:01             2929689
-torchvision-0.23.0+rocm7.2.4.git824e8c87-cp311-..> 21-May-2026 19:24             2931331
-torchvision-0.23.0+rocm7.2.4.git824e8c87-cp312-..> 21-May-2026 19:33             2933024
-torchvision-0.23.0+rocm7.2.4.git824e8c87-cp313-..> 21-May-2026 20:58             2932920
-torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp310-..> 25-May-2026 01:28             2942796
-torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp311-..> 21-May-2026 20:33             2944670
-torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp312-..> 25-May-2026 01:20             2946520
-torchvision-0.24.0+rocm7.2.4.gitb919bd0c-cp313-..> 25-May-2026 01:04             2946149
-torchvision-0.25.0+rocm7.2.4.git82df5f59-cp310-..> 22-May-2026 03:58             2950918
-torchvision-0.25.0+rocm7.2.4.git82df5f59-cp311-..> 22-May-2026 02:02             2953900
-torchvision-0.25.0+rocm7.2.4.git82df5f59-cp312-..> 22-May-2026 02:01             2954534
-torchvision-0.25.0+rocm7.2.4.git82df5f59-cp313-..> 22-May-2026 01:56             2954242
-torchvision-0.26.0+rocm7.2.4.git3d50b215-cp312-..> 17-Aug-2026 16:02             2938387
-transformer_engine-2.6.0-py3-none-any.whl          25-May-2026 13:46              606630
-transformer_engine_jax-2.6.0.tar.gz                25-May-2026 13:46              404582
-transformer_engine_rocm-2.6.0-py3-none-manylinu..> 25-May-2026 13:45           777770071
-transformer_engine_torch-2.6.0.tar.gz              25-May-2026 13:46              430209
-triton-3.3.1+rocm7.2.4.git28a7371e-cp310-cp310-..> 22-May-2026 04:37           270082005
-triton-3.3.1+rocm7.2.4.git28a7371e-cp311-cp311-..> 22-May-2026 03:17           270179300
-triton-3.3.1+rocm7.2.4.git28a7371e-cp312-cp312-..> 22-May-2026 04:39           270154950
-triton-3.3.1+rocm7.2.4.git28a7371e-cp313-cp313-..> 22-May-2026 02:11           270164456
-triton-3.3.1+rocm7.2.4.git28a7371e-cp39-cp39-li..> 22-May-2026 02:22           270084490
-triton-3.4.0+rocm7.2.4.git0cace8d2-cp310-cp310-..> 21-May-2026 22:01           268668362
-triton-3.4.0+rocm7.2.4.git0cace8d2-cp311-cp311-..> 21-May-2026 19:24           268763459
-triton-3.4.0+rocm7.2.4.git0cace8d2-cp312-cp312-..> 21-May-2026 19:33           268744128
-triton-3.4.0+rocm7.2.4.git0cace8d2-cp313-cp313-..> 21-May-2026 20:58           268746983
-triton-3.5.1+rocm7.2.4.gita272dfa8-cp310-cp310-..> 25-May-2026 01:28           284355039
-triton-3.5.1+rocm7.2.4.gita272dfa8-cp311-cp311-..> 21-May-2026 20:33           284445474
-triton-3.5.1+rocm7.2.4.gita272dfa8-cp312-cp312-..> 25-May-2026 01:20           284425085
-triton-3.5.1+rocm7.2.4.gita272dfa8-cp313-cp313-..> 25-May-2026 01:04           284438750
-triton-3.6.0+rocm7.2.4.git4ed88892-cp310-cp310-..> 22-May-2026 03:58           298359274
-triton-3.6.0+rocm7.2.4.git4ed88892-cp311-cp311-..> 22-May-2026 02:02           298493223
-triton-3.6.0+rocm7.2.4.git4ed88892-cp312-cp312-..> 22-May-2026 02:01           298453497
-triton-3.6.0+rocm7.2.4.git4ed88892-cp313-cp313-..> 22-May-2026 01:56           298465376
-triton-3.7.0+rocm7.2.4.gitb4e20bbe-cp312-cp312-..> 17-Aug-2026 16:02           308035330
-xformers-0.0.32+db55a2f5.d20260521-cp39-abi3-li..> 21-May-2026 22:12             9809687
-xformers-0.0.32+db55a2f5.d20260522-cp39-abi3-li..> 22-May-2026 04:52             9688226
-xformers-0.0.32+db55a2f5.d20260525-cp39-abi3-li..> 25-May-2026 01:32             9996328
-

- diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit deleted file mode 100644 index 897bdc820..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/offloading.exit +++ /dev/null @@ -1 +0,0 @@ -139 diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt deleted file mode 100644 index ea7f8b472..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/private-library-path.txt +++ /dev/null @@ -1 +0,0 @@ -/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log deleted file mode 100644 index f543c5988..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze-policy-copy-race.log +++ /dev/null @@ -1,5 +0,0 @@ -Traceback (most recent call last): - File "/home/marcelorm/ds4v-work/source-rocm210-reference/freeze-source-reference.py", line 27, in - assert digest(path) == expected - ^^^^^^^^^^^^^^^^^^^^^^^^ -AssertionError diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log deleted file mode 100644 index e055a7eea..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/reference-freeze.log +++ /dev/null @@ -1,8 +0,0 @@ -{ - "canonical_directory": "/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference", - "manifest_sha256": "677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86", - "freeze_manifest": "/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json", - "freeze_manifest_sha256": "8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0", - "status": "SOURCE_REFERENCE_STABILITY_PASS", - "frozen_utc": "2026-09-05T01:40:51.838089+00:00" -} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log deleted file mode 100644 index cd53451da..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-first-controller.log +++ /dev/null @@ -1,68 +0,0 @@ -{ - "command": [ - "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python", - "-I", - "-B", - "/home/marcelorm/ds4v-work/source-rocm210-reference/source-forward-radeon-name.py", - "--device", - "hip", - "--image", - "carrots", - "--gpu-window-released", - "--rocr-visible-device", - "GPU-93a97448a27aeff3", - "--source", - "/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored", - "--reference", - "/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference", - "--output", - "/home/marcelorm/ds4v-work/source-rocm210-reference/source-carrots-first" - ], - "environment": { - "HOME": "/home/marcelorm", - "PATH": "/usr/bin:/bin", - "LANG": "C.UTF-8", - "OMP_NUM_THREADS": "2", - "MKL_NUM_THREADS": "2", - "OPENBLAS_NUM_THREADS": "2", - "LD_LIBRARY_PATH": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", - "TORCH_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/torch-cache", - "TRITON_CACHE_DIR": "/home/marcelorm/ds4v-work/source-rocm210-reference/triton-cache", - "XDG_CACHE_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/cache" - }, - "timeout_seconds": 300, - "before": { - "operator": { - "MainPID": "0", - "ActiveState": "inactive" - }, - "ports": [], - "kfd_processes": [], - "host_available_bytes": 36105768960, - "discrete_free_vram_bytes": 21430087680, - "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" - }, - "script_sha256": "cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c", - "miopen_sha256": "bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd", - "wrapper_sha256": "b4b2d7f69a7d4c9aa50f18beffb75da5076192f758b7c701ed64fad2bc8bc6cd", - "gpu_release": "parent explicitly released fixed source stability lane", - "prospective_command_equivalence": "same frozen Python argv/environment; os.wait4 supplies timing in place of GNU time", - "python_pid": 3367102, - "exit": 0, - "elapsed_seconds": 5.763090842985548, - "user_seconds": 4.771837, - "system_seconds": 0.783909, - "max_rss_kib": 2517764, - "timed_out": false, - "after": { - "operator": { - "MainPID": "0", - "ActiveState": "inactive" - }, - "ports": [], - "kfd_processes": [], - "host_available_bytes": 36018761728, - "discrete_free_vram_bytes": 21430087680, - "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" - } -} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log deleted file mode 100644 index 367eee577..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-carrots-repeat-controller.log +++ /dev/null @@ -1,68 +0,0 @@ -{ - "command": [ - "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python", - "-I", - "-B", - "/home/marcelorm/ds4v-work/source-rocm210-reference/source-forward-radeon-name.py", - "--device", - "hip", - "--image", - "carrots", - "--gpu-window-released", - "--rocr-visible-device", - "GPU-93a97448a27aeff3", - "--source", - "/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored", - "--reference", - "/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference", - "--output", - "/home/marcelorm/ds4v-work/source-rocm210-reference/source-carrots-repeat" - ], - "environment": { - "HOME": "/home/marcelorm", - "PATH": "/usr/bin:/bin", - "LANG": "C.UTF-8", - "OMP_NUM_THREADS": "2", - "MKL_NUM_THREADS": "2", - "OPENBLAS_NUM_THREADS": "2", - "LD_LIBRARY_PATH": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", - "TORCH_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/torch-cache", - "TRITON_CACHE_DIR": "/home/marcelorm/ds4v-work/source-rocm210-reference/triton-cache", - "XDG_CACHE_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/cache" - }, - "timeout_seconds": 300, - "before": { - "operator": { - "MainPID": "0", - "ActiveState": "inactive" - }, - "ports": [], - "kfd_processes": [], - "host_available_bytes": 36015874048, - "discrete_free_vram_bytes": 21430087680, - "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" - }, - "script_sha256": "cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c", - "miopen_sha256": "bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd", - "wrapper_sha256": "b4b2d7f69a7d4c9aa50f18beffb75da5076192f758b7c701ed64fad2bc8bc6cd", - "gpu_release": "parent explicitly released fixed source stability lane", - "prospective_command_equivalence": "same frozen Python argv/environment; os.wait4 supplies timing in place of GNU time", - "python_pid": 3367383, - "exit": 0, - "elapsed_seconds": 5.7501774380216375, - "user_seconds": 4.690938, - "system_seconds": 0.736495, - "max_rss_kib": 2517508, - "timed_out": false, - "after": { - "operator": { - "MainPID": "0", - "ActiveState": "inactive" - }, - "ports": [], - "kfd_processes": [], - "host_available_bytes": 35733037056, - "discrete_free_vram_bytes": 21430087680, - "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" - } -} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log deleted file mode 100644 index 0bb00015e..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/source-corn-repeat-controller.log +++ /dev/null @@ -1,68 +0,0 @@ -{ - "command": [ - "/home/marcelorm/ds4v-work/source-rocm210-reference/.venv/bin/python", - "-I", - "-B", - "/home/marcelorm/ds4v-work/source-rocm210-reference/source-forward-radeon-name.py", - "--device", - "hip", - "--image", - "corn", - "--gpu-window-released", - "--rocr-visible-device", - "GPU-93a97448a27aeff3", - "--source", - "/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored", - "--reference", - "/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference", - "--output", - "/home/marcelorm/ds4v-work/source-rocm210-reference/source-corn-repeat" - ], - "environment": { - "HOME": "/home/marcelorm", - "PATH": "/usr/bin:/bin", - "LANG": "C.UTF-8", - "OMP_NUM_THREADS": "2", - "MKL_NUM_THREADS": "2", - "OPENBLAS_NUM_THREADS": "2", - "LD_LIBRARY_PATH": "/home/marcelorm/ds4v-work/source-rocm210-reference/private-miopen/opt/rocm-7.2.4/lib", - "TORCH_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/torch-cache", - "TRITON_CACHE_DIR": "/home/marcelorm/ds4v-work/source-rocm210-reference/triton-cache", - "XDG_CACHE_HOME": "/home/marcelorm/ds4v-work/source-rocm210-reference/cache" - }, - "timeout_seconds": 300, - "before": { - "operator": { - "MainPID": "0", - "ActiveState": "inactive" - }, - "ports": [], - "kfd_processes": [], - "host_available_bytes": 36105142272, - "discrete_free_vram_bytes": 21430087680, - "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" - }, - "script_sha256": "cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c", - "miopen_sha256": "bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd", - "wrapper_sha256": "b4b2d7f69a7d4c9aa50f18beffb75da5076192f758b7c701ed64fad2bc8bc6cd", - "gpu_release": "parent explicitly released fixed source stability lane", - "prospective_command_equivalence": "same frozen Python argv/environment; os.wait4 supplies timing in place of GNU time", - "python_pid": 3366985, - "exit": 0, - "elapsed_seconds": 5.234832148998976, - "user_seconds": 4.2197189999999996, - "system_seconds": 0.7277429999999999, - "max_rss_kib": 2503140, - "timed_out": false, - "after": { - "operator": { - "MainPID": "0", - "ActiveState": "inactive" - }, - "ports": [], - "kfd_processes": [], - "host_available_bytes": 36105420800, - "discrete_free_vram_bytes": 21430087680, - "discrete_sysfs": "/sys/devices/pci0000:00/0000:00:02.5/0000:c4:00.0/0000:c5:00.0/0000:c6:00.0" - } -} diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA deleted file mode 100644 index c0957e954..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-METADATA +++ /dev/null @@ -1,624 +0,0 @@ -Metadata-Version: 2.4 -Name: torch -Version: 2.10.0+rocm7.2.4.lw.git3d3aa833 -Summary: Tensors and Dynamic neural networks in Python with strong GPU acceleration -Author-email: PyTorch Team -License: BSD-3-Clause -Project-URL: Homepage, https://pytorch.org -Project-URL: Repository, https://github.com/pytorch/pytorch -Project-URL: Documentation, https://pytorch.org/docs -Project-URL: Issue Tracker, https://github.com/pytorch/pytorch/issues -Project-URL: Forum, https://discuss.pytorch.org -Keywords: pytorch,machine learning -Classifier: Development Status :: 5 - Production/Stable -Classifier: Intended Audience :: Developers -Classifier: Intended Audience :: Education -Classifier: Intended Audience :: Science/Research -Classifier: Topic :: Scientific/Engineering -Classifier: Topic :: Scientific/Engineering :: Mathematics -Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence -Classifier: Topic :: Software Development -Classifier: Topic :: Software Development :: Libraries -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Classifier: Programming Language :: C++ -Classifier: Programming Language :: Python :: 3 :: Only -Classifier: Programming Language :: Python :: 3.10 -Classifier: Programming Language :: Python :: 3.11 -Classifier: Programming Language :: Python :: 3.12 -Classifier: Programming Language :: Python :: 3.13 -Classifier: Programming Language :: Python :: 3.14 -Requires-Python: >=3.10 -Description-Content-Type: text/markdown -License-File: LICENSE -License-File: NOTICE -Requires-Dist: filelock -Requires-Dist: typing-extensions>=4.10.0 -Requires-Dist: setuptools; python_version >= "3.12" -Requires-Dist: sympy>=1.13.3 -Requires-Dist: networkx>=2.5.1 -Requires-Dist: jinja2 -Requires-Dist: fsspec>=0.8.5 -Requires-Dist: triton==3.6.0+rocm7.2.4.git4ed88892; platform_system == "Linux" and platform_machine == "x86_64" -Provides-Extra: optree -Requires-Dist: optree>=0.13.0; extra == "optree" -Provides-Extra: opt-einsum -Requires-Dist: opt-einsum>=3.3; extra == "opt-einsum" -Provides-Extra: pyyaml -Requires-Dist: pyyaml; extra == "pyyaml" -Dynamic: license-file -Dynamic: requires-dist - -![PyTorch Logo](https://github.com/pytorch/pytorch/raw/main/docs/source/_static/img/pytorch-logo-dark.png) - --------------------------------------------------------------------------------- - -PyTorch is a Python package that provides two high-level features: -- Tensor computation (like NumPy) with strong GPU acceleration -- Deep neural networks built on a tape-based autograd system - -You can reuse your favorite Python packages such as NumPy, SciPy, and Cython to extend PyTorch when needed. - -Our trunk health (Continuous Integration signals) can be found at [hud.pytorch.org](https://hud.pytorch.org/ci/pytorch/pytorch/main). - - - -- [More About PyTorch](#more-about-pytorch) - - [A GPU-Ready Tensor Library](#a-gpu-ready-tensor-library) - - [Dynamic Neural Networks: Tape-Based Autograd](#dynamic-neural-networks-tape-based-autograd) - - [Python First](#python-first) - - [Imperative Experiences](#imperative-experiences) - - [Fast and Lean](#fast-and-lean) - - [Extensions Without Pain](#extensions-without-pain) -- [Installation](#installation) - - [Binaries](#binaries) - - [NVIDIA Jetson Platforms](#nvidia-jetson-platforms) - - [From Source](#from-source) - - [Prerequisites](#prerequisites) - - [NVIDIA CUDA Support](#nvidia-cuda-support) - - [AMD ROCm Support](#amd-rocm-support) - - [Intel GPU Support](#intel-gpu-support) - - [Get the PyTorch Source](#get-the-pytorch-source) - - [Install Dependencies](#install-dependencies) - - [Install PyTorch](#install-pytorch) - - [Adjust Build Options (Optional)](#adjust-build-options-optional) - - [Docker Image](#docker-image) - - [Using pre-built images](#using-pre-built-images) - - [Building the image yourself](#building-the-image-yourself) - - [Building the Documentation](#building-the-documentation) - - [Building a PDF](#building-a-pdf) - - [Previous Versions](#previous-versions) -- [Getting Started](#getting-started) -- [Resources](#resources) -- [Communication](#communication) -- [Releases and Contributing](#releases-and-contributing) -- [The Team](#the-team) -- [License](#license) - - - -## More About PyTorch - -[Learn the basics of PyTorch](https://pytorch.org/tutorials/beginner/basics/intro.html) - -At a granular level, PyTorch is a library that consists of the following components: - -| Component | Description | -| ---- | --- | -| [**torch**](https://pytorch.org/docs/stable/torch.html) | A Tensor library like NumPy, with strong GPU support | -| [**torch.autograd**](https://pytorch.org/docs/stable/autograd.html) | A tape-based automatic differentiation library that supports all differentiable Tensor operations in torch | -| [**torch.jit**](https://pytorch.org/docs/stable/jit.html) | A compilation stack (TorchScript) to create serializable and optimizable models from PyTorch code | -| [**torch.nn**](https://pytorch.org/docs/stable/nn.html) | A neural networks library deeply integrated with autograd designed for maximum flexibility | -| [**torch.multiprocessing**](https://pytorch.org/docs/stable/multiprocessing.html) | Python multiprocessing, but with magical memory sharing of torch Tensors across processes. Useful for data loading and Hogwild training | -| [**torch.utils**](https://pytorch.org/docs/stable/data.html) | DataLoader and other utility functions for convenience | - -Usually, PyTorch is used either as: - -- A replacement for NumPy to use the power of GPUs. -- A deep learning research platform that provides maximum flexibility and speed. - -Elaborating Further: - -### A GPU-Ready Tensor Library - -If you use NumPy, then you have used Tensors (a.k.a. ndarray). - -![Tensor illustration](https://github.com/pytorch/pytorch/raw/main/docs/source/_static/img/tensor_illustration.png) - -PyTorch provides Tensors that can live either on the CPU or the GPU and accelerates the -computation by a huge amount. - -We provide a wide variety of tensor routines to accelerate and fit your scientific computation needs -such as slicing, indexing, mathematical operations, linear algebra, reductions. -And they are fast! - -### Dynamic Neural Networks: Tape-Based Autograd - -PyTorch has a unique way of building neural networks: using and replaying a tape recorder. - -Most frameworks such as TensorFlow, Theano, Caffe, and CNTK have a static view of the world. -One has to build a neural network and reuse the same structure again and again. -Changing the way the network behaves means that one has to start from scratch. - -With PyTorch, we use a technique called reverse-mode auto-differentiation, which allows you to -change the way your network behaves arbitrarily with zero lag or overhead. Our inspiration comes -from several research papers on this topic, as well as current and past work such as -[torch-autograd](https://github.com/twitter/torch-autograd), -[autograd](https://github.com/HIPS/autograd), -[Chainer](https://chainer.org), etc. - -While this technique is not unique to PyTorch, it's one of the fastest implementations of it to date. -You get the best of speed and flexibility for your crazy research. - -![Dynamic graph](https://github.com/pytorch/pytorch/raw/main/docs/source/_static/img/dynamic_graph.gif) - -### Python First - -PyTorch is not a Python binding into a monolithic C++ framework. -It is built to be deeply integrated into Python. -You can use it naturally like you would use [NumPy](https://www.numpy.org/) / [SciPy](https://www.scipy.org/) / [scikit-learn](https://scikit-learn.org) etc. -You can write your new neural network layers in Python itself, using your favorite libraries -and use packages such as [Cython](https://cython.org/) and [Numba](http://numba.pydata.org/). -Our goal is to not reinvent the wheel where appropriate. - -### Imperative Experiences - -PyTorch is designed to be intuitive, linear in thought, and easy to use. -When you execute a line of code, it gets executed. There isn't an asynchronous view of the world. -When you drop into a debugger or receive error messages and stack traces, understanding them is straightforward. -The stack trace points to exactly where your code was defined. -We hope you never spend hours debugging your code because of bad stack traces or asynchronous and opaque execution engines. - -### Fast and Lean - -PyTorch has minimal framework overhead. We integrate acceleration libraries -such as [Intel MKL](https://software.intel.com/mkl) and NVIDIA ([cuDNN](https://developer.nvidia.com/cudnn), [NCCL](https://developer.nvidia.com/nccl)) to maximize speed. -At the core, its CPU and GPU Tensor and neural network backends -are mature and have been tested for years. - -Hence, PyTorch is quite fast — whether you run small or large neural networks. - -The memory usage in PyTorch is extremely efficient compared to Torch or some of the alternatives. -We've written custom memory allocators for the GPU to make sure that -your deep learning models are maximally memory efficient. -This enables you to train bigger deep learning models than before. - -### Extensions Without Pain - -Writing new neural network modules, or interfacing with PyTorch's Tensor API was designed to be straightforward -and with minimal abstractions. - -You can write new neural network layers in Python using the torch API -[or your favorite NumPy-based libraries such as SciPy](https://pytorch.org/tutorials/advanced/numpy_extensions_tutorial.html). - -If you want to write your layers in C/C++, we provide a convenient extension API that is efficient and with minimal boilerplate. -No wrapper code needs to be written. You can see [a tutorial here](https://pytorch.org/tutorials/advanced/cpp_extension.html) and [an example here](https://github.com/pytorch/extension-cpp). - - -## Installation - -### Binaries -Commands to install binaries via Conda or pip wheels are on our website: [https://pytorch.org/get-started/locally/](https://pytorch.org/get-started/locally/) - - -#### NVIDIA Jetson Platforms - -Python wheels for NVIDIA's Jetson Nano, Jetson TX1/TX2, Jetson Xavier NX/AGX, and Jetson AGX Orin are provided [here](https://forums.developer.nvidia.com/t/pytorch-for-jetson-version-1-10-now-available/72048) and the L4T container is published [here](https://catalog.ngc.nvidia.com/orgs/nvidia/containers/l4t-pytorch) - -They require JetPack 4.2 and above, and [@dusty-nv](https://github.com/dusty-nv) and [@ptrblck](https://github.com/ptrblck) are maintaining them. - - -### From Source - -#### Prerequisites -If you are installing from source, you will need: -- Python 3.10 or later -- A compiler that fully supports C++17, such as clang or gcc (gcc 9.4.0 or newer is required, on Linux) -- Visual Studio or Visual Studio Build Tool (Windows only) - -\* PyTorch CI uses Visual C++ BuildTools, which come with Visual Studio Enterprise, -Professional, or Community Editions. You can also install the build tools from -https://visualstudio.microsoft.com/visual-cpp-build-tools/. The build tools *do not* -come with Visual Studio Code by default. - -An example of environment setup is shown below: - -* Linux: - -```bash -$ source /bin/activate -$ conda create -y -n -$ conda activate -``` - -* Windows: - -```bash -$ source \Scripts\activate.bat -$ conda create -y -n -$ conda activate -$ call "C:\Program Files\Microsoft Visual Studio\\Community\VC\Auxiliary\Build\vcvarsall.bat" x64 -``` - -A conda environment is not required. You can also do a PyTorch build in a -standard virtual environment, e.g., created with tools like `uv`, provided -your system has installed all the necessary dependencies unavailable as pip -packages (e.g., CUDA, MKL.) - -##### NVIDIA CUDA Support -If you want to compile with CUDA support, [select a supported version of CUDA from our support matrix](https://pytorch.org/get-started/locally/), then install the following: -- [NVIDIA CUDA](https://developer.nvidia.com/cuda-downloads) -- [NVIDIA cuDNN](https://developer.nvidia.com/cudnn) v8.5 or above -- [Compiler](https://gist.github.com/ax3l/9489132) compatible with CUDA - -Note: You could refer to the [cuDNN Support Matrix](https://docs.nvidia.com/deeplearning/cudnn/backend/latest/reference/support-matrix.html) for cuDNN versions with the various supported CUDA, CUDA driver, and NVIDIA hardware. - -If you want to disable CUDA support, export the environment variable `USE_CUDA=0`. -Other potentially useful environment variables may be found in `setup.py`. If -CUDA is installed in a non-standard location, set PATH so that the nvcc you -want to use can be found (e.g., `export PATH=/usr/local/cuda-12.8/bin:$PATH`). - -If you are building for NVIDIA's Jetson platforms (Jetson Nano, TX1, TX2, AGX Xavier), Instructions to install PyTorch for Jetson Nano are [available here](https://devtalk.nvidia.com/default/topic/1049071/jetson-nano/pytorch-for-jetson-nano/) - -##### AMD ROCm Support -If you want to compile with ROCm support, install -- [AMD ROCm](https://rocm.docs.amd.com/en/latest/deploy/linux/quick_start.html) 4.0 and above installation -- ROCm is currently supported only for Linux systems. - -By default the build system expects ROCm to be installed in `/opt/rocm`. If ROCm is installed in a different directory, the `ROCM_PATH` environment variable must be set to the ROCm installation directory. The build system automatically detects the AMD GPU architecture. Optionally, the AMD GPU architecture can be explicitly set with the `PYTORCH_ROCM_ARCH` environment variable [AMD GPU architecture](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/reference/system-requirements.html#supported-gpus) - -If you want to disable ROCm support, export the environment variable `USE_ROCM=0`. -Other potentially useful environment variables may be found in `setup.py`. - -##### Intel GPU Support -If you want to compile with Intel GPU support, follow these -- [PyTorch Prerequisites for Intel GPUs](https://www.intel.com/content/www/us/en/developer/articles/tool/pytorch-prerequisites-for-intel-gpus.html) instructions. -- Intel GPU is supported for Linux and Windows. - -If you want to disable Intel GPU support, export the environment variable `USE_XPU=0`. -Other potentially useful environment variables may be found in `setup.py`. - -#### Get the PyTorch Source - -```bash -git clone https://github.com/pytorch/pytorch -cd pytorch -# if you are updating an existing checkout -git submodule sync -git submodule update --init --recursive -``` - -#### Install Dependencies - -**Common** - -```bash -# Run this command from the PyTorch directory after cloning the source code using the “Get the PyTorch Source“ section above -pip install --group dev -``` - -**On Linux** - -```bash -pip install mkl-static mkl-include -# CUDA only: Add LAPACK support for the GPU if needed -# magma installation: run with active conda environment. specify CUDA version to install -.ci/docker/common/install_magma_conda.sh 12.4 - -# (optional) If using torch.compile with inductor/triton, install the matching version of triton -# Run from the pytorch directory after cloning -# For Intel GPU support, please explicitly `export USE_XPU=1` before running command. -make triton -``` - -**On MacOS** - -```bash -# Add this package on intel x86 processor machines only -pip install mkl-static mkl-include -# Add these packages if torch.distributed is needed -conda install pkg-config libuv -``` - -**On Windows** - -```bash -pip install mkl-static mkl-include -# Add these packages if torch.distributed is needed. -# Distributed package support on Windows is a prototype feature and is subject to changes. -conda install -c conda-forge libuv=1.51 -``` - -#### Install PyTorch - -**On Linux** - -If you're compiling for AMD ROCm then first run this command: - -```bash -# Only run this if you're compiling for ROCm -python tools/amd_build/build_amd.py -``` - -Install PyTorch - -```bash -# the CMake prefix for conda environment -export CMAKE_PREFIX_PATH="${CONDA_PREFIX:-'$(dirname $(which conda))/../'}:${CMAKE_PREFIX_PATH}" -python -m pip install --no-build-isolation -v -e . - -# the CMake prefix for non-conda environment, e.g. Python venv -# call following after activating the venv -export CMAKE_PREFIX_PATH="${VIRTUAL_ENV}:${CMAKE_PREFIX_PATH}" -``` - -**On macOS** - -```bash -python -m pip install --no-build-isolation -v -e . -``` - -**On Windows** - -If you want to build legacy python code, please refer to [Building on legacy code and CUDA](https://github.com/pytorch/pytorch/blob/main/CONTRIBUTING.md#building-on-legacy-code-and-cuda) - -**CPU-only builds** - -In this mode PyTorch computations will run on your CPU, not your GPU. - -```cmd -python -m pip install --no-build-isolation -v -e . -``` - -Note on OpenMP: The desired OpenMP implementation is Intel OpenMP (iomp). In order to link against iomp, you'll need to manually download the library and set up the building environment by tweaking `CMAKE_INCLUDE_PATH` and `LIB`. The instruction [here](https://github.com/pytorch/pytorch/blob/main/docs/source/notes/windows.rst#building-from-source) is an example for setting up both MKL and Intel OpenMP. Without these configurations for CMake, Microsoft Visual C OpenMP runtime (vcomp) will be used. - -**CUDA based build** - -In this mode PyTorch computations will leverage your GPU via CUDA for faster number crunching - -[NVTX](https://docs.nvidia.com/gameworks/content/gameworkslibrary/nvtx/nvidia_tools_extension_library_nvtx.htm) is needed to build Pytorch with CUDA. -NVTX is a part of CUDA distributive, where it is called "Nsight Compute". To install it onto an already installed CUDA run CUDA installation once again and check the corresponding checkbox. -Make sure that CUDA with Nsight Compute is installed after Visual Studio. - -Currently, VS 2017 / 2019, and Ninja are supported as the generator of CMake. If `ninja.exe` is detected in `PATH`, then Ninja will be used as the default generator, otherwise, it will use VS 2017 / 2019. -
If Ninja is selected as the generator, the latest MSVC will get selected as the underlying toolchain. - -Additional libraries such as -[Magma](https://developer.nvidia.com/magma), [oneDNN, a.k.a. MKLDNN or DNNL](https://github.com/oneapi-src/oneDNN), and [Sccache](https://github.com/mozilla/sccache) are often needed. Please refer to the [installation-helper](https://github.com/pytorch/pytorch/tree/main/.ci/pytorch/win-test-helpers/installation-helpers) to install them. - -You can refer to the [build_pytorch.bat](https://github.com/pytorch/pytorch/blob/main/.ci/pytorch/win-test-helpers/build_pytorch.bat) script for some other environment variables configurations - -```cmd -cmd - -:: Set the environment variables after you have downloaded and unzipped the mkl package, -:: else CMake would throw an error as `Could NOT find OpenMP`. -set CMAKE_INCLUDE_PATH={Your directory}\mkl\include -set LIB={Your directory}\mkl\lib;%LIB% - -:: Read the content in the previous section carefully before you proceed. -:: [Optional] If you want to override the underlying toolset used by Ninja and Visual Studio with CUDA, please run the following script block. -:: "Visual Studio 2019 Developer Command Prompt" will be run automatically. -:: Make sure you have CMake >= 3.12 before you do this when you use the Visual Studio generator. -set CMAKE_GENERATOR_TOOLSET_VERSION=14.27 -set DISTUTILS_USE_SDK=1 -for /f "usebackq tokens=*" %i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -version [15^,17^) -products * -latest -property installationPath`) do call "%i\VC\Auxiliary\Build\vcvarsall.bat" x64 -vcvars_ver=%CMAKE_GENERATOR_TOOLSET_VERSION% - -:: [Optional] If you want to override the CUDA host compiler -set CUDAHOSTCXX=C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.27.29110\bin\HostX64\x64\cl.exe - -python -m pip install --no-build-isolation -v -e . -``` - -**Intel GPU builds** - -In this mode PyTorch with Intel GPU support will be built. - -Please make sure [the common prerequisites](#prerequisites) as well as [the prerequisites for Intel GPU](#intel-gpu-support) are properly installed and the environment variables are configured prior to starting the build. For build tool support, `Visual Studio 2022` is required. - -Then PyTorch can be built with the command: - -```cmd -:: CMD Commands: -:: Set the CMAKE_PREFIX_PATH to help find corresponding packages -:: %CONDA_PREFIX% only works after `conda activate custom_env` - -if defined CMAKE_PREFIX_PATH ( - set "CMAKE_PREFIX_PATH=%CONDA_PREFIX%\Library;%CMAKE_PREFIX_PATH%" -) else ( - set "CMAKE_PREFIX_PATH=%CONDA_PREFIX%\Library" -) - -python -m pip install --no-build-isolation -v -e . -``` - -##### Adjust Build Options (Optional) - -You can adjust the configuration of cmake variables optionally (without building first), by doing -the following. For example, adjusting the pre-detected directories for CuDNN or BLAS can be done -with such a step. - -On Linux - -```bash -export CMAKE_PREFIX_PATH="${CONDA_PREFIX:-'$(dirname $(which conda))/../'}:${CMAKE_PREFIX_PATH}" -CMAKE_ONLY=1 python setup.py build -ccmake build # or cmake-gui build -``` - -On macOS - -```bash -export CMAKE_PREFIX_PATH="${CONDA_PREFIX:-'$(dirname $(which conda))/../'}:${CMAKE_PREFIX_PATH}" -MACOSX_DEPLOYMENT_TARGET=11.0 CMAKE_ONLY=1 python setup.py build -ccmake build # or cmake-gui build -``` - -### Docker Image - -#### Using pre-built images - -You can also pull a pre-built docker image from Docker Hub and run with docker v19.03+ - -```bash -docker run --gpus all --rm -ti --ipc=host pytorch/pytorch:latest -``` - -Please note that PyTorch uses shared memory to share data between processes, so if torch multiprocessing is used (e.g. -for multithreaded data loaders) the default shared memory segment size that container runs with is not enough, and you -should increase shared memory size either with `--ipc=host` or `--shm-size` command line options to `nvidia-docker run`. - -#### Building the image yourself - -**NOTE:** Must be built with a docker version > 18.06 - -The `Dockerfile` is supplied to build images with CUDA 11.1 support and cuDNN v8. -You can pass `PYTHON_VERSION=x.y` make variable to specify which Python version is to be used by Miniconda, or leave it -unset to use the default. - -```bash -make -f docker.Makefile -# images are tagged as docker.io/${your_docker_username}/pytorch -``` - -You can also pass the `CMAKE_VARS="..."` environment variable to specify additional CMake variables to be passed to CMake during the build. -See [setup.py](./setup.py) for the list of available variables. - -```bash -make -f docker.Makefile -``` - -### Building the Documentation - -To build documentation in various formats, you will need [Sphinx](http://www.sphinx-doc.org) -and the pytorch_sphinx_theme2. - -Before you build the documentation locally, ensure `torch` is -installed in your environment. For small fixes, you can install the -nightly version as described in [Getting Started](https://pytorch.org/get-started/locally/). - -For more complex fixes, such as adding a new module and docstrings for -the new module, you might need to install torch [from source](#from-source). -See [Docstring Guidelines](https://github.com/pytorch/pytorch/wiki/Docstring-Guidelines) -for docstring conventions. - -```bash -cd docs/ -pip install -r requirements.txt -make html -make serve -``` - -Run `make` to get a list of all available output formats. - -If you get a katex error run `npm install katex`. If it persists, try -`npm install -g katex` - -> [!NOTE] -> If you installed `nodejs` with a different package manager (e.g., -> `conda`) then `npm` will probably install a version of `katex` that is not -> compatible with your version of `nodejs` and doc builds will fail. -> A combination of versions that is known to work is `node@6.13.1` and -> `katex@0.13.18`. To install the latter with `npm` you can run -> ```npm install -g katex@0.13.18``` - -> [!NOTE] -> If you see a numpy incompatibility error, run: -> ``` -> pip install 'numpy<2' -> ``` - -When you make changes to the dependencies run by CI, edit the -`.ci/docker/requirements-docs.txt` file. - -#### Building a PDF - -To compile a PDF of all PyTorch documentation, ensure you have -`texlive` and LaTeX installed. On macOS, you can install them using: - -``` -brew install --cask mactex -``` - -To create the PDF: - -1. Run: - - ``` - make latexpdf - ``` - - This will generate the necessary files in the `build/latex` directory. - -2. Navigate to this directory and execute: - - ``` - make LATEXOPTS="-interaction=nonstopmode" - ``` - - This will produce a `pytorch.pdf` with the desired content. Run this - command one more time so that it generates the correct table - of contents and index. - -> [!NOTE] -> To view the Table of Contents, switch to the **Table of Contents** -> view in your PDF viewer. - - -### Previous Versions - -Installation instructions and binaries for previous PyTorch versions may be found -on [our website](https://pytorch.org/get-started/previous-versions). - - -## Getting Started - -Three pointers to get you started: -- [Tutorials: get you started with understanding and using PyTorch](https://pytorch.org/tutorials/) -- [Examples: easy to understand PyTorch code across all domains](https://github.com/pytorch/examples) -- [The API Reference](https://pytorch.org/docs/) -- [Glossary](https://github.com/pytorch/pytorch/blob/main/GLOSSARY.md) - -## Resources - -* [PyTorch.org](https://pytorch.org/) -* [PyTorch Tutorials](https://pytorch.org/tutorials/) -* [PyTorch Examples](https://github.com/pytorch/examples) -* [PyTorch Models](https://pytorch.org/hub/) -* [Intro to Deep Learning with PyTorch from Udacity](https://www.udacity.com/course/deep-learning-pytorch--ud188) -* [Intro to Machine Learning with PyTorch from Udacity](https://www.udacity.com/course/intro-to-machine-learning-nanodegree--nd229) -* [Deep Neural Networks with PyTorch from Coursera](https://www.coursera.org/learn/deep-neural-networks-with-pytorch) -* [PyTorch Twitter](https://twitter.com/PyTorch) -* [PyTorch Blog](https://pytorch.org/blog/) -* [PyTorch YouTube](https://www.youtube.com/channel/UCWXI5YeOsh03QvJ59PMaXFw) - -## Communication -* Forums: Discuss implementations, research, etc. https://discuss.pytorch.org -* GitHub Issues: Bug reports, feature requests, install issues, RFCs, thoughts, etc. -* Slack: The [PyTorch Slack](https://pytorch.slack.com/) hosts a primary audience of moderate to experienced PyTorch users and developers for general chat, online discussions, collaboration, etc. If you are a beginner looking for help, the primary medium is [PyTorch Forums](https://discuss.pytorch.org). If you need a slack invite, please fill this form: https://goo.gl/forms/PP1AGvNHpSaJP8to1 -* Newsletter: No-noise, a one-way email newsletter with important announcements about PyTorch. You can sign-up here: https://eepurl.com/cbG0rv -* Facebook Page: Important announcements about PyTorch. https://www.facebook.com/pytorch -* For brand guidelines, please visit our website at [pytorch.org](https://pytorch.org/) - -## Releases and Contributing - -Typically, PyTorch has three minor releases a year. Please let us know if you encounter a bug by [filing an issue](https://github.com/pytorch/pytorch/issues). - -We appreciate all contributions. If you are planning to contribute back bug-fixes, please do so without any further discussion. - -If you plan to contribute new features, utility functions, or extensions to the core, please first open an issue and discuss the feature with us. -Sending a PR without discussion might end up resulting in a rejected PR because we might be taking the core in a different direction than you might be aware of. - -To learn more about making a contribution to Pytorch, please see our [Contribution page](CONTRIBUTING.md). For more information about PyTorch releases, see [Release page](RELEASE.md). - -## The Team - -PyTorch is a community-driven project with several skillful engineers and researchers contributing to it. - -PyTorch is currently maintained by [Soumith Chintala](http://soumith.ch), [Gregory Chanan](https://github.com/gchanan), [Dmytro Dzhulgakov](https://github.com/dzhulgakov), [Edward Yang](https://github.com/ezyang), [Alban Desmaison](https://github.com/albanD), [Piotr Bialecki](https://github.com/ptrblck) and [Nikita Shulga](https://github.com/malfet) with major contributions coming from hundreds of talented individuals in various forms and means. -A non-exhaustive but growing list needs to mention: [Trevor Killeen](https://github.com/killeent), [Sasank Chilamkurthy](https://github.com/chsasank), [Sergey Zagoruyko](https://github.com/szagoruyko), [Adam Lerer](https://github.com/adamlerer), [Francisco Massa](https://github.com/fmassa), [Alykhan Tejani](https://github.com/alykhantejani), [Luca Antiga](https://github.com/lantiga), [Alban Desmaison](https://github.com/albanD), [Andreas Koepf](https://github.com/andreaskoepf), [James Bradbury](https://github.com/jekbradbury), [Zeming Lin](https://github.com/ebetica), [Yuandong Tian](https://github.com/yuandong-tian), [Guillaume Lample](https://github.com/glample), [Marat Dukhan](https://github.com/Maratyszcza), [Natalia Gimelshein](https://github.com/ngimel), [Christian Sarofeen](https://github.com/csarofeen), [Martin Raison](https://github.com/martinraison), [Edward Yang](https://github.com/ezyang), [Zachary Devito](https://github.com/zdevito). - -Note: This project is unrelated to [hughperkins/pytorch](https://github.com/hughperkins/pytorch) with the same name. Hugh is a valuable contributor to the Torch community and has helped with many things Torch and PyTorch. - -## License - -PyTorch has a BSD-style license, as found in the [LICENSE](LICENSE) file. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL deleted file mode 100644 index fb12b9fcb..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-WHEEL +++ /dev/null @@ -1,5 +0,0 @@ -Wheel-Version: 1.0 -Generator: setuptools (79.0.1) -Root-Is-Purelib: false -Tag: cp312-cp312-linux_x86_64 - diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt deleted file mode 100644 index 2579adb5d..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-url.txt +++ /dev/null @@ -1 +0,0 @@ -https://repo.radeon.com/rocm/manylinux/rocm-rel-7.2.4/torch-2.10.0%2Brocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt deleted file mode 100644 index 0f1dbd261..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-version-static.txt +++ /dev/null @@ -1,10 +0,0 @@ -from typing import Optional - -__all__ = ['__version__', 'debug', 'cuda', 'git_version', 'hip', 'rocm', 'xpu'] -__version__ = '2.10.0+rocm7.2.4.git3d3aa833' -debug = False -cuda: Optional[str] = None -git_version = '3d3aa833db84eed6b7f5595cb5f162c2f78300a4' -hip: Optional[str] = '7.2.53211' -rocm: Optional[str] = '7.2.4' -xpu: Optional[str] = None diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 deleted file mode 100644 index 975b944c2..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/torch-wheel.sha256 +++ /dev/null @@ -1 +0,0 @@ -e3a4b7f11eacc4037bc405fbf8beacf2ce19cc135ad283bb653b93a127f379d0 torch-2.10.0+rocm7.2.4.lw.git3d3aa833-cp312-cp312-linux_x86_64.whl diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt deleted file mode 100644 index 01912075c..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/evidence/venv-config.txt +++ /dev/null @@ -1,5 +0,0 @@ -home = /usr/bin -include-system-site-packages = false -version = 3.12.3 -executable = /usr/bin/python3.12 -command = /usr/bin/python3 -m venv /home/marcelorm/ds4v-work/source-rocm210-reference/.venv diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py deleted file mode 100644 index 41533d0a8..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/freeze-source-reference.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -"""Freeze first source-HIP outputs after exact repeat validation; CPU-only file work.""" -import datetime -import filecmp -import hashlib -import json -from pathlib import Path -import shutil - -home = Path.home() -root = home/'ds4v-work/source-rocm210-reference' -cpu = home/'lucebox-ds4v-mix-fix/artifacts/vision-reference' -canonical = root/'source-hip-reference' -freeze_path = root/'source-hip-reference-freeze.json' -assert not canonical.exists() and not freeze_path.exists() - -def digest(path): - with path.open('rb') as f: - return hashlib.file_digest(f, 'sha256').hexdigest() - -def read(path): - return json.loads(path.read_text()) - -policy = {'reviewed':(root/'reference-policy-reviewed.md','7edde20ee70b804cc903b827dbea1dbc9b8d43d9d352e22f82672758430f9682'), - 'adopted':(root/'reference-policy-adopted.md','62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f')} -for path, expected in policy.values(): - assert digest(path) == expected -assert [x for x in policy['reviewed'][0].read_text().splitlines() if not x.startswith('Status:')] == \ - [x for x in policy['adopted'][0].read_text().splitlines() if not x.startswith('Status:')] -assert digest(cpu/'manifest.json') == '38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f' -original = read(cpu/'manifest.json') -runner = root/'source-forward-radeon-name.py' -assert digest(runner) == 'cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c' -pairs = {'corn':('hip-corn-confirmed','hip-supervision-confirmed','source-corn-repeat','source-corn-repeat-supervision'), - 'carrots':('source-carrots-first','source-carrots-first-supervision','source-carrots-repeat','source-carrots-repeat-supervision')} -gates = {'features':{'max_abs':.25,'rmse':.03,'cosine':.9995}, - 'embeddings':{'max_abs':.75,'rmse':.08,'cosine':.9990}} -freeze = {'status':'SOURCE_REFERENCE_STABILITY_PASS', 'candidate_acceptance':'NOT_EVALUATED', - 'frozen_utc':datetime.datetime.now(datetime.timezone.utc).isoformat(), - 'policy':{k:{'file':str(p),'sha256':h} for k,(p,h) in policy.items()}, - 'original_cpu_manifest':{'file':str(cpu/'manifest.json'),'sha256':digest(cpu/'manifest.json')}, - 'runner':{'file':str(runner),'sha256':digest(runner)}, 'gates':gates, 'images':{}, - 'reference_selection':'FIRST completed source corn and FIRST source carrots outputs; repeats only validate stability', - 'scope':'Original source on this RX7900XT/software configuration; CPU portability remains separate'} -manifest = {'torch':'2.10.0+rocm7.2.4.git3d3aa833', 'reference_kind':'original_source_hip_7900xt', - 'config':original['config'], 'source_hashes':original['source_hashes'], 'images':{}, - 'policy_sha256':policy['adopted'][1], 'original_cpu_manifest_sha256':freeze['original_cpu_manifest']['sha256']} -all_libraries = set() -for image,(first_name,first_supervision,repeat_name,repeat_supervision) in pairs.items(): - first,repeat = root/first_name,root/repeat_name - reports = [read(first/'report.json'),read(repeat/'report.json')] - runs = [read(root/first_supervision/'run.json'),read(root/repeat_supervision/'run.json')] - for report,run in zip(reports,runs): - assert run['exit'] == 0 and not run.get('timed_out') and not run.get('error') - assert report['script_sha256'] == freeze['runner']['sha256'] - assert report['source_hashes'] == original['source_hashes'] and report['image'] == image - assert report['reference_manifest_sha256'] == freeze['original_cpu_manifest']['sha256'] - assert report['weight_inventory_sha256'] == 'b0556c40a8bff3f4c2c262d57137a97123cbdbf7444a7fae495ef17cd28469ee' - assert report['device']['name'] == 'Radeon RX 7900 XT' and report['device']['gcn_arch'].split(':')[0] == 'gfx1100' - assert report['device']['rocr_visible_device'] == 'GPU-93a97448a27aeff3' - assert report['torch_git'] == '3d3aa833db84eed6b7f5595cb5f162c2f78300a4' and report['torch_hip'] == '7.2.53211' - assert report['threads'] == [2,2] and report['default_dtype'] == 'torch.bfloat16' - all_libraries.update(report['loaded_libraries']) - item = {key:original['images'][image][key] for key in ('image_sha256','vit_grid','aligner_grid','patches')} - assert digest(cpu/item['patches']['file']) == item['patches']['sha256'] - evidence = {'first_directory':str(first),'repeat_directory':str(repeat), - 'first_report_sha256':digest(first/'report.json'),'repeat_report_sha256':digest(repeat/'report.json'), - 'first_run':runs[0],'repeat_run':runs[1], 'hardware':reports[0]['device'], - 'outputs':{}, 'cpu_portability':{'first':reports[0]['comparisons'],'repeat':reports[1]['comparisons']}} - for stage in gates: - a,b = reports[0]['outputs'][stage],reports[1]['outputs'][stage] - assert a['shape'] == b['shape'] == original['images'][image][stage]['shape'] - assert digest(first/a['file']) == a['sha256'] and digest(repeat/b['file']) == b['sha256'] - assert a['sha256'] == b['sha256'] and filecmp.cmp(first/a['file'],repeat/b['file'],shallow=False) - for report in reports: - assert report['comparisons'][stage]['gate'] == gates[stage] and report['comparisons'][stage]['finite'] - item[stage] = a.copy() - evidence['outputs'][stage] = {'first_sha256':a['sha256'],'repeat_sha256':b['sha256'],'byte_identical':True} - freeze['images'][image] = evidence - manifest['images'][image] = item - -# These hashes capture the actual loaded shared-library set, not a guessed loader path. -freeze['loaded_libraries'] = [{'file':name,'sha256':digest(Path(name)),'bytes':Path(name).stat().st_size} - for name in sorted(all_libraries)] -freeze['provenance_files'] = {name:{'file':str(root/name),'sha256':digest(root/name)} for name in ( - 'requirements.lock','constraints.txt','evidence/wheels.json','evidence/install-report.json', - 'evidence/cpu-runtime-private.json','evidence/miopen-package.json','evidence/miopen-library.json', - 'evidence/freeze.txt','reference-policy-reviewed.md','reference-policy-adopted.md', - 'run-hip-confirmed-supervised.py','run-source-stability-supervised.py','compare-corn-three-way.py')} -freeze['wheel_inventory'] = read(root/'evidence/wheels.json') -freeze['miopen_package'] = read(root/'evidence/miopen-package.json') -freeze['miopen_library'] = read(root/'evidence/miopen-library.json') -freeze['source_weights'] = {'inventory_sha256':reports[0]['weight_inventory_sha256'], - 'index_sha256':reports[0]['index_sha256'],'source_hashes':original['source_hashes']} -freeze['freeze_script_sha256'] = digest(Path(__file__)) -canonical.mkdir() -for image,item in manifest['images'].items(): - first = Path(freeze['images'][image]['first_directory']) - for stage in ('patches','features','embeddings'): - entry = item[stage] - src = cpu/entry['file'] if stage == 'patches' else first/entry['file'] - dst = canonical/entry['file'] - shutil.copyfile(src,dst) - assert digest(dst) == entry['sha256'] - dst.chmod(0o444) -(canonical/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n') -(canonical/'manifest.json').chmod(0o444) -freeze['canonical_reference'] = {'directory':str(canonical),'manifest_sha256':digest(canonical/'manifest.json')} -freeze_path.write_text(json.dumps(freeze,indent=2)+'\n') -freeze_path.chmod(0o444) -print(json.dumps({'canonical_directory':str(canonical),'manifest_sha256':digest(canonical/'manifest.json'), - 'freeze_manifest':str(freeze_path),'freeze_manifest_sha256':digest(freeze_path), - 'status':freeze['status'],'frozen_utc':freeze['frozen_utc']},indent=2)) diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md deleted file mode 100644 index e592d8987..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-initial-failure.md +++ /dev/null @@ -1,17 +0,0 @@ -# Original-source corn HIP control - -**Initial released attempt stopped at the device-name guard before weight loading or a tower forward. No source-HIP tensors were produced.** The frozen script was not changed for this attempt, and no numerical threshold was changed. - -The parent's explicit GPU release authorized the single original-source corn lane. `run-hip-supervised.py` verified source-forward SHA256 `17ba9d66260d25c056ecc56a35192748a662b040605bb0bf8864bd704a66267b` and private MIOpen SHA256 `bad776611dcee04ec70ca17674999e1af606ab9bfa0c6c6309ccf933ab1cdbbd`, then checked operator inactive/MainPID0, no listeners8016/8217, empty KFD process directory, and at least8 GiB host and discrete VRAM available. Actual preflight:36102742016 host bytes and21430087680 discrete bytes free. - -The Python argv/environment matched the prepared command; direct child supervision with `os.wait4` supplied actual PID/resource timing instead of GNU time. The finite deadline was300 seconds, with termination restricted to the recorded unreaped direct child. No timeout or signal was needed. - -- Actual Python PID3359202, exit1, elapsed2.091728805 seconds. -- User1.455116 seconds, system0.239513 seconds, peak RSS744712 KiB. -- Error:`RuntimeError: actual device is not RX 7900 XT` at the exact `props.name == 'AMD Radeon RX 7900 XT'` guard. -- The frozen script constructs an identity dictionary before that guard but does not print it on this failure path, so the actual Torch name was not captured. No weight loading, forward or output directory creation occurred. -- After exit:operator inactive/PID0, ports free, KFD empty, discrete VRAM unchanged at21430087680 free bytes. - -The completed native lane's existing `device-check.log` reports Device0 as `Radeon RX 7900 XT` without the `AMD` prefix, gfx1100,20464 MiB. This is a plausible display-name mismatch, not proof of the source attempt's actual selected device. No further GPU identity query or retry was performed in this initial attempt. - -Evidence:remote `~/ds4v-work/source-rocm210-reference/hip-supervision/`; local copied `hip-supervision/{run.json,hip-corn.log,memory.jsonl,python.pid,exit}`. The wrapper source is in this report's directory. Source-HIP/CPU and native-HIP/source-HIP comparisons remain unavailable until a source forward completes. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md deleted file mode 100644 index c44f6238d..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/hip-control-report.md +++ /dev/null @@ -1,51 +0,0 @@ -# Original-source corn HIP control — completed diagnostic - -**The source HIP forward completed, but source-HIP/CPU feature portability fails the unchanged gate. Native HIP also fails against source HIP for both features and embeddings. Native vision remains NOT QUALIFIED.** These results separate a source backend portability effect from an additional native discrepancy; they do not establish its exact cause or justify widening thresholds. - -## Execution and identity - -The first released attempt used the original frozen runner17ba9d66 unchanged. It exited1 at the exact marketing-name guard before weight loading/forward. That evidence is preserved in `hip-supervision/` and `hip-control-initial-failure.md`. - -The parent then authorized one identity-only query and a narrow name correction if the actual device agreed. Identity-only Python PID3362155 exited0, reporting exactly one visible device: `Radeon RX 7900 XT`, gfx1100,21458059264 bytes (20464 MiB), selected by ROCr UUID `GPU-93a97448a27aeff3`, PCI bus198/device0. The mismatch was the expected string's `AMD ` prefix. - -The reviewed correction changes only that exact expected name and prints actual properties before asserting them. The original source runner and original `vision.py` remain unchanged. `radeon-name-correction.diff` records the two-line correction. New runner `source-forward-radeon-name.py` SHA256: -`cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. - -The parent received this hash before execution and reviewed the diff. Supervision reused the operator/idle/memory/hash guards, created fresh `hip-supervision-confirmed/` and `hip-corn-confirmed/`, and imposed a300-second deadline. The exact Python command/environment are in `hip-supervision-confirmed/run.json`; direct `os.wait4` supervision recorded the actual Python PID/resource usage instead of GNU time. It signals only that unreaped direct child if necessary. - -- Before:operator inactive/MainPID0; no ports8016/8217; KFD empty;36154867712 bytes host available;21430087680 bytes discrete VRAM free. -- Actual corrected Python PID3362755, exit0, elapsed5.758919639 seconds; user4.532378/system0.985126 seconds; peak RSS2504072 KiB. -- Original source forward0.798916269 seconds; GPU peak allocated1176237056 bytes, reserved1186988032 bytes. -- After:operator inactive/PID0, ports free, KFD empty, discrete VRAM back to21430087680 free bytes. GPU ownership was released to the parent immediately after completion. - -There was exactly one actual source corn HIP forward. The initial guard failure and identity-only query did not run a model. No additional image, Torch version, SDPA variant, source math, native code, operator, converter or fixture was changed. - -## Fixed three-way comparison - -All compared tensors are finite and have exact expected shapes:features782×1024, embeddings96×4096. The CPU-only comparison verified the original manifest and every input file hash. Existing fixed gates remain features maxabs≤0.25/RMSE≤0.03/cosine≥0.9995; embeddings maxabs≤0.75/RMSE≤0.08/cosine≥0.9990. All three comparisons retain their own verdicts. - -| Stage | Pair | Max absolute | RMSE | Cosine | Gate | -|---|---|---:|---:|---:|---| -| Features | Source HIP vs original CPU | 2.73828125 | 0.007125659433 | 0.997729789065 | FAIL | -| Features | Native HIP vs source HIP | 1.2039794921875 | 0.014023936578 | 0.991196938632 | FAIL | -| Features | Native HIP vs original CPU | 2.9617919921875 | 0.017743029365 | 0.985938580153 | FAIL | -| Embeddings | Source HIP vs original CPU | 0.09130859375 | 0.003126610779 | 0.999115331698 | PASS | -| Embeddings | Native HIP vs source HIP | 0.16259765625 | 0.007664476777 | 0.994697536836 | FAIL | -| Embeddings | Native HIP vs original CPU | 0.176513671875 | 0.009035238955 | 0.992632567277 | FAIL | - -The new AMD Torch environment's earlier CPU outputs were byte-identical to the immutable original CPU fixtures. The source-HIP/CPU gap therefore appears when executing the original graph on HIP in this controlled environment. This observation is bounded to this image/runtime/default dispatch; it is not a universal backend-error estimate. Native-HIP/source-HIP still fails both stages, so source portability does not explain away the native discrepancy. The old native-HIP outputs are the parent's completed frozen `hip-component-first/native` files, not a new native run. - -## Exact output identities - -| Stage | Producer | SHA256 | -|---|---|---| -| Features | Original CPU | `aa7c43be7182759f83881cf823661bf52c14b73222645d1ec37b14c6502bc982` | -| Features | Source HIP | `5790c492de2618560f81bac3ab8a70282a272be0e1885b95246621aab0bf4bb8` | -| Features | Native HIP | `59bd19a13750d07f7f1018c32c5a43c4ae2cd7a7ff132da3f6201b08c400cc4e` | -| Embeddings | Original CPU | `c96d59ae722ad8ac31299aabb4e758b788a1ee4be30ea94b833c753721229040` | -| Embeddings | Source HIP | `80a30a096a9dd84e91f8d47d63b1cf00b3eded88f6e472b1018bbdabb697ab86` | -| Embeddings | Native HIP | `a398c9c10a7b2bbeb63f4b910bf5f71bb9fefd0aa388b276a3bf06ca3e280a06` | - -Comparison script SHA256:`3741e93cab886c9050e2aa484236a6b15d7aa8b6f3ed55ae1e8acfe37cf4d555`; comparison process exit0 means metrics completed, not numerical acceptance. - -Local evidence is under this report's directory: `hip-identity-supervision/`, `hip-supervision-confirmed/{run.json,hip-corn.log,memory.jsonl,three-way.json,three-way.log}`, and `hip-corn-confirmed/report.json`. Complete output tensor files remain in the corresponding soulf root `~/ds4v-work/source-rocm210-reference/`. Original runner17ba9d66, private library hash, original source/weight/reference hashes and exact pins remain preserved. This report does not qualify another image, source repeat stability, end-to-end image answers or later server integration. diff --git a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md b/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md deleted file mode 100644 index a17d61790..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/source-rocm-reference/reference-stability-report.md +++ /dev/null @@ -1,59 +0,0 @@ -# First-source HIP reference freeze - -**SOURCE_REFERENCE_STABILITY_PASS.** Corn and carrots each produced byte-identical features and embeddings in two independent fresh processes on the same7900XT. The FIRST completed output for each image is now the frozen reference. No corrected native full-tower output was inspected or used to select it. This establishes reference stability for the adopted target-GPU policy; it does not accept any native candidate or resolve the original CPU feature failure. - -The exact parent policy was independently reviewed at SHA256 `7edde20ee70b804cc903b827dbea1dbc9b8d43d9d352e22f82672758430f9682`. The parent changed only the status sentence after both independent PASS reviews; adopted policy SHA256 is `62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f`. Both text versions and hashes are pinned in the provenance manifest, and their bodies were checked identical apart from the Status line. The initial copy raced the parent's status update; the first freeze correctly rejected the reviewed-hash mismatch before writing a reference directory. The exact reviewed text was restored to a separate file and verified against7edde20e; both versions remain preserved. - -## Canonical reference and provenance - -Remote canonical directory: -`/home/marcelorm/ds4v-work/source-rocm210-reference/source-hip-reference` - -Its `manifest.json` uses the existing comparison schema: each image has vit_grid, aligner_grid and patches/features/embeddings entries with file, shape and SHA256. Patch files are exact copies of the immutable original CPU patches. Feature/embedding files are exact copies of FIRST source-HIP outputs, never repeat-selected samples. All canonical files are read-only. - -- Canonical `manifest.json` SHA256:`677b5ef033d009a4c44f9fcf7207276e55c4ec8968044ed237f709a108cb3f86`. -- Separate remote `~/ds4v-work/source-rocm210-reference/source-hip-reference-freeze.json` SHA256:`8ab35a8a7adc8c66678af7f50b6de610777f0cf03077e108d1d381957cbe8ce0`. -- Frozen UTC:`2026-09-05T01:40:51.838089+00:00`. -- Source runner SHA256:`cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. -- Original CPU manifest SHA256:`38d12f30b9a10ed2f0e99bf4d40a07357ab5e8c4880151c58cb90414e4a53f4f`. - -The freeze manifest pins first/repeat raw-byte hashes and report hashes; exact source argv/environment/PIDs/resources; first actual hardware identities; all14 wheel hashes/versions, install and private MIOpen provenance;77 actual loaded shared-library paths and SHA256 values; original source/index/weight inventory/patch identities; source supervision and comparison scripts; both policy hashes and all unchanged numerical gates. Repeats were checked with both SHA256 and whole-file byte comparisons. No original CPU reference was changed. - -## Source execution and resources - -All source forwards use the same unchanged original modules, weights, BF16/F32 boundaries, default SDPA, original hashed patches, AMD Torch/runtime and private MIOpen. Actual device identity is `Radeon RX 7900 XT`, gfx1100, UUID selection`GPU-93a97448a27aeff3`,20464 MiB. Source processes use two CPU threads. The earlier exact-name failure and identity-only query remain separate; they ran no model. - -| Lane | Actual Python PID | Exit | Process seconds | Forward seconds | Peak RSS KiB | GPU allocated / reserved bytes | -|---|---:|---:|---:|---:|---:|---| -| FIRST corn, retained | 3362755 | 0 | 5.758920 | 0.798916 | 2504072 | 1176237056 / 1186988032 | -| Corn repeat | 3366985 | 0 | 5.234832 | 0.389909 | 2503140 | 1176237056 / 1186988032 | -| FIRST carrots, retained | 3367102 | 0 | 5.763091 | 0.855355 | 2517764 | 2089592320 / 2134900736 | -| Carrots repeat | 3367383 | 0 | 5.750177 | 0.855419 | 2517508 | 2089592320 / 2134900736 | - -Every lane passed operator inactive/MainPID0, no ports8016/8217, empty KFD and≥8 GiB host/discrete-memory preflight. Each had a300-second deadline; no timeout or signal occurred. Each source child exited before the next launch. After each, KFD was empty and discrete free VRAM returned to21430087680 bytes. GPU ownership was released to the parent immediately after the three new source stability children exited; subsequent manifest work was CPU-only. The timing difference between first/repeat corn was not used to select a reference or make a speed claim. - -## Frozen FIRST output hashes - -| Image | Stage | SHA256, also matched by its repeat | -|---|---|---| -| Corn | Features | `5790c492de2618560f81bac3ab8a70282a272be0e1885b95246621aab0bf4bb8` | -| Corn | Embeddings | `80a30a096a9dd84e91f8d47d63b1cf00b3eded88f6e472b1018bbdabb697ab86` | -| Carrots | Features | `270b09f7b62d47162137613df78c5735284dee5b2d31a42892e12ec9631d57b1` | -| Carrots | Embeddings | `4eaf4a6de24d0b9c6cab3d42ec13a3a74e7a06627c21262106e6b059ac4bbb4f` | - -## Original CPU portability remains separate - -| Image/stage | Max absolute | RMSE | Cosine | Original gate | -|---|---:|---:|---:|---| -| Corn features | 2.73828125 | 0.007125659433 | 0.997729789065 | FAIL | -| Corn embeddings | 0.09130859375 | 0.003126610779 | 0.999115331698 | PASS | -| Carrots features | 0.1484375 | 0.002524693483 | 0.999693162950 | PASS | -| Carrots embeddings | 0.0302734375 | 0.001367435885 | 0.999825389498 | PASS | - -All outputs are finite and shapes match the original manifest. Feature gates remain maxabs≤0.25/RMSE≤0.03/cosine≥0.9995; embedding gates remain maxabs≤0.75/RMSE≤0.08/cosine≥0.9990. The failed corn CPU feature gate remains a failure. The previously measured native-HIP/source-HIP comparison still fails both corn stages; nothing in this freeze relabels it. - -## Review and limits - -The independent recommendation is `target-hip-acceptance-review.md`. Target-GPU fidelity is a justified, explicitly different question from cross-device CPU portability. Matching the original source on this GPU cannot rule out an underlying source-HIP backend defect shared by another implementation; it is not absolute numerical ground truth. That limitation, the unchanged CPU failures, default-SDPA dispatch dependence and the experimental AMD fork/host tuple must remain visible. Two stable fixtures are not universal reproducibility or accuracy coverage. The corrected native GEMM regression, target-tower gates and separate end-to-end image behavior remain required before claiming working vision. - -Local copies: `source-hip-reference-freeze.json`, `evidence/source-hip-reference-manifest.json`, `source-corn-repeat/`, `source-carrots-first/`, `source-carrots-repeat/`, and corresponding `*-supervision/` directories under this report's directory. The local manifest is a review copy; complete canonical tensor files remain on soulf. Copied manifest hashes were rechecked locally. No further GPU execution occurred after release. diff --git a/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md b/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md deleted file mode 100644 index b68515668..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy-review.md +++ /dev/null @@ -1,42 +0,0 @@ -# Prospective 7900 XT qualification policy review - -**Verdict: PASS as a prospective, target-scoped numerical policy.** Reviewed -`target-hip-qualification-policy.md` at SHA256 -`62b3daef05bcaaac175f6907b8748d64952a4bf96baab316bab9b2140451929f` -before any corrected full-tower output was produced. The post-review status-line -adoption did not change the reviewed procedure. This review does not qualify the -current or corrected native tower. - -The target reference is scientifically motivated by observed source behavior, -not selected from candidate results: the unchanged original source on the -7900 XT fails the existing CPU feature threshold, while its embedding remains -within the existing embedding threshold. A same-device source reference controls -the backend-dependent reduction schedule that the original CPU fixture cannot. -The policy keeps the already published CPU comparisons and their failures -visible, so a target result cannot be presented as CPU equivalence. - -The freeze is suitably prospective and resists result shopping. It requires the -same original model, weights, patches, BF16 boundaries, default source operations, -pinned software and exact 7900 XT identity; byte-identical source repeats for -corn and carrots; a manifest containing all source outputs and provenance before -the corrected native full tower runs; and no later reference replacement based -on candidate output. The corrected source runner's only semantic change is the -observed marketing-name check and identity logging; its SHA256 is frozen as -`cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. - -Acceptance remains demanding: both images must independently pass every existing -feature and embedding threshold with exact shapes and finite values, failures -must remain machine-visible, and the native corn repeat must be byte-identical. -The dyadic biased-linear regression can run before the reference freeze because -it neither executes the full tower nor supplies a reference output. - -Any PASS is limited to the native tower on the recorded 7900 XT/software tuple. -It does not qualify CPU numerics, gfx1151, other accelerators, decoder behavior, -HTTP image input, image-dependent answers, isolation, resource limits or cleanup. -Those separate gates remain required. The main residual scientific limitation is -fixture breadth: two images establish the selected deployment gate, not general -cross-image or cross-backend equivalence. No tolerance, source implementation or -reference backend may be changed after seeing corrected candidate results. - -This was a read-only policy and evidence review. No build, model execution, GPU -operation, server action or runtime source edit was performed. diff --git a/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md b/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md deleted file mode 100644 index 76d63a5ad..000000000 --- a/harness/qualification/deepseek4/ds4v-vision/target-hip-qualification-policy.md +++ /dev/null @@ -1,33 +0,0 @@ -# Prospective 7900 XT vision qualification - -Status: adopted prospectively after two independent PASS reviews. No corrected full-tower candidate output has been produced under this policy. The original CPU fixtures and their thresholds remain unchanged and all comparisons to them stay visible. - -## Evidence motivating the target reference - -The unchanged original source, fixed AMD Torch2.10/ROCm7.2.4 environment, original weights and identical corn patches produced a source-HIP feature cosine of0.997729789 and maximum absolute difference2.73828125 against original CPU. These fail the existing feature gate. Source-HIP embeddings pass the original embedding gate. This demonstrates that the existing CPU feature threshold is not portable even to the original implementation on this target GPU; it does not establish native correctness. The existing native HIP implementation also fails against source HIP, with feature cosine0.991196939 and embedding cosine0.994697537. - -Evidence: `source-rocm-reference/hip-supervision-confirmed/three-way.json`. The narrowly corrected original-source runner only logs actual device identity and matches the observed marketing name. Its source SHA256 is `cd34fd3df07963bea9014ccc6f405ec95400a7b8b70e4233fe5d3ead7dc9468c`. Model code, weights, precision boundaries, default SDPA, image patches and library versions are unchanged. - -## Reference freeze before candidate execution - -Use the same original-source runner on exactly Radeon RX7900XT/gfx1100, selected by UUID `GPU-93a97448a27aeff3`, with its pinned environment and weights. Retain the first completed corn output. Run one corn repeat and two carrots runs in fresh processes/directories. Require each image's features and embeddings to repeat byte-identically; otherwise stop qualification and investigate reference stability without choosing a favorable sample. - -Freeze the first source outputs for both images, their repeat hashes, the existing CPU manifest and patch hashes, runtime/library provenance, hardware identity, exact runner and comparison scripts in one manifest before executing the corrected native full tower. Never replace references based on candidate results. The small exact-dyadic biased-linear regression is independent of this full-tower policy and may execute first. - -## Unchanged numeric gates, explicit scope - -For both images separately, require exact expected dimensions, finite values and every existing gate against frozen same-GPU source output: - -- Final normalized features: maximum absolute difference<=0.25, RMSE<=0.03, cosine>=0.9995. -- Aligner embeddings: maximum absolute difference<=0.75, RMSE<=0.08, cosine>=0.9990. -- Native corn repeat must be byte-identical. Failed metrics remain machine-visible failures with nonzero qualification exit. - -Publish native-vs-original-CPU and source-HIP-vs-original-CPU results beside the target comparison using the unchanged thresholds. A target PASS cannot relabel those CPU portability failures as PASS. CPU functional/regression suites remain required, while numerical CPU tower qualification remains separately unresolved unless its original gate actually passes. - -A successful result qualifies only this native tower on this 7900XT/software configuration. It does not qualify Strix vision execution, other GPU architectures, CPU tower numerics, full decoder parity, other vision model architectures, or image chat. The supported placement keeps the tower on7900XT; Strix continues to own tail language experts. Memory limits, unsupported-path errors, transactional loading and exact projector/preprocessing contracts still apply. - -## Integration and behavior remain separate - -Only a passing target tower may clear the tower dependency for the selected heterogeneous runtime integration. Actual HTTP image input, correct image-dependent answers for both fixtures and equal-layout/different-image isolation, malformed-input behavior, text regression, GPU ownership, memory/latency and cleanup remain required. Sparse decoder prefill remains explicitly approximate. - -Do not sweep tolerances, source versions, precision modes or reference backends to obtain a pass. Any further change to qualification policy must be prospective, separately motivated and independently reviewed, with old failures retained. From 7651512fb0df87716387040bad8b4648109a9611 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:43:48 +0200 Subject: [PATCH 083/123] fix(ds4v): keep the text path identical to main without --mmproj Page-cache reclamation, staged HIP uploads, expert source paging and the GPU pool trims were unconditional. They now apply only to a vision load. The image-bias check reads the model's own dimensions instead of fixed DS4V sizes. Co-Authored-By: Claude Fable 5.1 --- server/src/common/copied_source_reclaim.h | 2 +- server/src/common/moe_hybrid_storage.cpp | 30 ++-------------- server/src/common/moe_hybrid_storage.h | 6 ++-- server/src/deepseek4/deepseek4_backend.cpp | 13 ++----- server/src/deepseek4/deepseek4_graph.cpp | 17 ++++----- server/src/deepseek4/deepseek4_internal.h | 8 ++++- server/src/deepseek4/deepseek4_loader.cpp | 42 +++++++++++++--------- 7 files changed, 50 insertions(+), 68 deletions(-) diff --git a/server/src/common/copied_source_reclaim.h b/server/src/common/copied_source_reclaim.h index f9dced63e..42344f7f3 100644 --- a/server/src/common/copied_source_reclaim.h +++ b/server/src/common/copied_source_reclaim.h @@ -49,7 +49,7 @@ inline CopiedSourceAdviceResult reclaim_copied_file_source( static_cast(range.size), POSIX_FADV_DONTNEED); } } - if (result.requested || result.range_error) { + if (result.range_error || result.madvise_error || result.fadvise_error) { std::fprintf(stderr, "[source-reclaim] %s layer=%d requested=%zu range_error=%d madvise_error=%d fadvise_error=%d\n", label, layer, result.requested, result.range_error, result.madvise_error, result.fadvise_error); diff --git a/server/src/common/moe_hybrid_storage.cpp b/server/src/common/moe_hybrid_storage.cpp index 52f6160a6..a3589d823 100644 --- a/server/src/common/moe_hybrid_storage.cpp +++ b/server/src/common/moe_hybrid_storage.cpp @@ -39,32 +39,8 @@ namespace { void advise_copied_source(const void * mapping, size_t mapping_size, const ExpertTensorFileData & tensor, int layer, int source_fd) { - if (source_fd >= 0) { - reclaim_copied_file_source(mapping, mapping_size, tensor.data, tensor.size, - source_fd, "expert", layer); - return; - } -#if defined(__linux__) && defined(MADV_PAGEOUT) - if (!tensor.data || tensor.size == 0) return; - const long page_size = ::sysconf(_SC_PAGESIZE); - MoeSourcePageRange range; - if (page_size <= 0 || !moe_source_page_range( - reinterpret_cast(mapping), mapping_size, - reinterpret_cast(tensor.data), tensor.size, - static_cast(page_size), range)) { - std::fprintf(stderr, "[hybrid-storage] layer %d source pageout rejected: errno=%d requested=%zu bytes\n", - layer, EINVAL, tensor.size); - return; - } - if (range.size == 0) return; - errno = 0; - const int rc = ::madvise(reinterpret_cast(range.address), range.size, MADV_PAGEOUT); - const int error = rc == 0 ? 0 : errno; - std::fprintf(stderr, "[hybrid-storage] layer %d source pageout advisory: requested=%zu bytes rc=%d errno=%d\n", - layer, range.size, rc, error); -#else - (void) mapping; (void) mapping_size; (void) tensor; (void) layer; -#endif + reclaim_copied_file_source(mapping, mapping_size, tensor.data, tensor.size, + source_fd, "expert", layer); } void unregister_mix_tensor(ggml_tensor * tensor) { @@ -734,7 +710,7 @@ bool build_moe_hybrid_storage_from_file( // Only the mmap-retaining wrapper supplies proven file-mapping bounds. // All synchronous hot/cold uploads have returned, and both temporary // slice buffers are gone. Retain the mapping for future streaming reads. - if (readonly_file_mmap && moe_source_pageout_eligible( + if (readonly_file_mmap && readonly_file_fd >= 0 && moe_source_pageout_eligible( out.cold_backend_kind == MoeHybridColdBackend::Gpu, cfg.materialize_hot_experts, cfg.materialize_cold_experts, allocate_cold && cold_count > 0 && dst.cold_buf != nullptr)) { diff --git a/server/src/common/moe_hybrid_storage.h b/server/src/common/moe_hybrid_storage.h index 6c1d0097a..773aac742 100644 --- a/server/src/common/moe_hybrid_storage.h +++ b/server/src/common/moe_hybrid_storage.h @@ -264,9 +264,9 @@ int moe_hybrid_cache_swap_in(MoeHybridLayerStorage & st, int global_expert, ggml_backend_t gpu_backend); // Build hybrid storage by loading expert data directly from file (mmap). -// Optional readonly_file_mmap metadata is supplied only by the mmap-retaining -// wrapper for a read-only file-backed mapping. It permits advisory reclamation -// of completed materialized GPU layers without invalidating source pointers. +// Optional: a caller that passes readonly_file_fd >= 0 for its read-only +// file-backed mapping opts in to advisory page-cache reclamation of completed +// materialized GPU layers. Source pointers stay valid; later reads refault. bool build_moe_hybrid_storage_from_file( const MoeHybridConfig & cfg, ggml_backend_t gpu_backend, diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index a0e9ec519..6ac548ac3 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -2923,21 +2923,14 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, // can strand smaller legacy-pool blocks across successive chunks. // Retire captured executables/memos and only returned pool blocks at // this synchronized boundary; leave reusable gallocr arenas intact. - if (bound_hybrid_scratch && n_tok >= 512 && + if (vision_ && bound_hybrid_scratch && n_tok >= 512 && kv_offset + n_total > 4096 && moe_hybrid_->cold_backend && moe_hybrid_->cold_backend != backend_) { ggml_backend_synchronize(backend_); ggml_backend_synchronize(moe_hybrid_->cold_backend); - const size_t primary_released = - ggml_backend_cuda_trim_pool(backend_); - const size_t cold_released = - ggml_backend_cuda_trim_pool(moe_hybrid_->cold_backend); - std::fprintf(stderr, - "[deepseek4] prefill chunk pool trim pos=%d " - "primary=%.2f MiB cold=%.2f MiB\n", - pos, primary_released / (1024.0 * 1024.0), - cold_released / (1024.0 * 1024.0)); + ggml_backend_cuda_trim_pool(backend_); + ggml_backend_cuda_trim_pool(moe_hybrid_->cold_backend); } } keep_spec_feature_tail(spec_feat_window_, diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index a5eded5ea..9bd978cc0 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -7782,6 +7782,7 @@ bool deepseek4_validate_image_batch( int count, int position, vision::ImageSpanView spans, bool & has_images, std::string & error) { has_images = false; + if (!spans.size) return true; const auto fail = [&](const char * message) { error = message; return false; }; if (count <= 0 || position < 0 || int64_t(position) + count > cache.max_ctx || !vision::valid_image_spans(spans, uint64_t(std::max(0, cache.max_ctx)))) @@ -8554,17 +8555,12 @@ bool deepseek4_step_layer_range( // Gallocr teardown does not return cached operator temporaries to // the driver. Retire backend captures/memos and trim free pool blocks // before allocating the new bulk-prefill scratch on either owner. - const size_t primary_released = ggml_backend_cuda_trim_pool(backend); - std::fprintf(stderr, - "[deepseek4] bulk prefill pool trim: owner=primary released=%zu bytes\n", - primary_released); - if (moe_hybrid && moe_hybrid->cold_backend && - moe_hybrid->cold_backend != backend) { - const size_t cold_released = + if (ds4_image_capable(w)) { + ggml_backend_cuda_trim_pool(backend); + if (moe_hybrid && moe_hybrid->cold_backend && + moe_hybrid->cold_backend != backend) { ggml_backend_cuda_trim_pool(moe_hybrid->cold_backend); - std::fprintf(stderr, - "[deepseek4] bulk prefill pool trim: owner=cold released=%zu bytes\n", - cold_released); + } } std::fprintf(stderr, "[deepseek4] released prior decode/tail arenas before " @@ -9106,6 +9102,7 @@ bool deepseek4_step_layer_range( i64_array_inputs, &f32_array_inputs, attention_impl, + /*boundary_checkpoint=*/nullptr, image_batch ? image_spans : vision::ImageSpanView{}); if (!attn_out) { ggml_free(ctx); return false; } ggml_set_output(attn_out); diff --git a/server/src/deepseek4/deepseek4_internal.h b/server/src/deepseek4/deepseek4_internal.h index 08e02555c..26530e78d 100644 --- a/server/src/deepseek4/deepseek4_internal.h +++ b/server/src/deepseek4/deepseek4_internal.h @@ -139,7 +139,7 @@ struct DeepSeek4Layer { // Router ggml_tensor * ffn_gate_inp = nullptr; // [n_embd, n_expert] router weights F16 ggml_tensor * ffn_exp_probs_b = nullptr; // [n_expert] optional routing bias - ggml_tensor * ffn_gate_bias_vl = nullptr; + ggml_tensor * ffn_gate_bias_vl = nullptr; // image router bias, loaded only with --mmproj // Hash routing table (first n_hash_layer layers only) ggml_tensor * ffn_gate_tid2eid = nullptr; // [n_expert_used, n_vocab] I32 @@ -249,6 +249,12 @@ struct DeepSeek4Weights { bool fused_verify_f16_kv = false; }; +// True when the image router biases were loaded, i.e. the backend was started +// with a vision projector. +inline bool ds4_image_capable(const DeepSeek4Weights & w) { + return !w.layers.empty() && w.layers.front().ffn_gate_bias_vl != nullptr; +} + inline bool deepseek4_is_eos_tok(int tok, const DeepSeek4Weights & w) { return (w.eos_chat_id >= 0 && tok == w.eos_chat_id) || (w.eos_id >= 0 && tok == w.eos_id); diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index a08d5e142..f1aba6c0f 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -243,7 +243,7 @@ static int image_bias_layer(const char * name) { if (*number < '0' || *number > '9') return -1; char * suffix = nullptr; const long layer = std::strtol(number, &suffix, 10); - if (layer < 0 || layer >= 43 || + if (layer < 0 || layer > std::numeric_limits::max() || std::strcmp(suffix, ".ffn.gate.bias_vl") != 0 || std::string(name) != "layers." + std::to_string(layer) + ".ffn.gate.bias_vl") return -1; return int(layer); @@ -1596,28 +1596,31 @@ bool load_deepseek4_gguf_partial(const std::string & path, // ── Collect tensors for allocation ────────────────────────────────── const int n_tensors = gguf_get_n_tensors(gctx); if (plan.load_ds4_image_bias && !plan.expert_metadata_only) { - bool valid = n_layer == 43 && n_embd == 4096 && n_vocab == 129280 && - n_expert == 256 && n_expert_used == 6 && - plan.layer_begin == 0 && plan.layer_end == 43; - std::array counts{}; + // Image routing needs one F32[n_expert] router bias per decoder layer. + bool valid = plan.layer_begin == 0 && plan.layer_end == int(n_layer); + std::vector counts(size_t(n_layer), 0); for (int ti = 0; ti < n_tensors; ++ti) { const char * name = gguf_get_tensor_name(gctx, ti); const int layer = image_bias_layer(name); if (layer < 0) continue; + if (layer >= int(n_layer)) { valid = false; continue; } ++counts[size_t(layer)]; const ggml_tensor * tensor = find_tensor(meta_ctx, name); valid = valid && tensor && tensor->type == GGML_TYPE_F32 && - tensor->ne[0] == 256 && tensor->ne[1] == 1 && + tensor->ne[0] == int64_t(n_expert) && tensor->ne[1] == 1 && tensor->ne[2] == 1 && tensor->ne[3] == 1; } valid = valid && std::all_of(counts.begin(), counts.end(), [](int count) { return count == 1; }); if (!valid) { - set_last_error("DS4V requires the supported decoder and exactly 43 F32[256] image router biases"); + set_last_error("--mmproj requires a full-range load and one F32[n_expert] image router bias per layer"); gguf_free(gctx); if (meta_ctx) ggml_free(meta_ctx); return false; } } + // A vision load packs the primary GPU tightly: hand file-backed source + // pages back to the kernel as soon as each tensor has been copied. + const bool reclaim_sources = plan.load_ds4_image_bias; const size_t data_offset = gguf_get_data_offset(gctx); ggml_backend_buffer_type_t buft = ggml_backend_get_default_buffer_type(backend); const size_t alignment = ggml_backend_buft_get_alignment(buft); @@ -1823,8 +1826,10 @@ bool load_deepseek4_gguf_partial(const std::string & path, ggml_backend_tensor_set(a.tensor, src_data, 0, a.file_size); #if defined(__linux__) // set_tensor has completed its source copy, including split buffers. - reclaim_copied_file_source(mmap.addr, mmap.len, src_data, a.file_size, - mmap.fd, ggml_get_name(a.tensor)); + if (reclaim_sources) { + reclaim_copied_file_source(mmap.addr, mmap.len, src_data, a.file_size, + mmap.fd, ggml_get_name(a.tensor)); + } #endif } if (!read_ok) { @@ -1844,7 +1849,8 @@ bool load_deepseek4_gguf_partial(const std::string & path, if (!a.upload_to_backend) continue; const void * src_data = (const char *)mmap.addr + a.file_offset; #if defined(__linux__) && (defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP)) - if (!a.dense_split && ggml_backend_is_cuda(backend) && !ggml_backend_cuda_buffer_is_managed(buf)) { + if (reclaim_sources && !a.dense_split && ggml_backend_is_cuda(backend) && + !ggml_backend_cuda_buffer_is_managed(buf)) { // HIP may pin pageable upload sources. Keep file-backed pages // out of that path so completed-source cache advice can act. if (!upload_copied_file_chunks(mmap.addr, mmap.len, a.file_offset, @@ -1867,8 +1873,10 @@ bool load_deepseek4_gguf_partial(const std::string & path, } #if defined(__linux__) // set_tensor has completed its source copy, including split buffers. - reclaim_copied_file_source(mmap.addr, mmap.len, src_data, a.file_size, - mmap.fd, ggml_get_name(a.tensor)); + if (reclaim_sources) { + reclaim_copied_file_source(mmap.addr, mmap.len, src_data, a.file_size, + mmap.fd, ggml_get_name(a.tensor)); + } #endif } } @@ -1888,9 +1896,11 @@ bool load_deepseek4_gguf_partial(const std::string & path, std::memcpy(out.embedder.tok_embd_owned.data(), (const char *)emb_mmap.addr + a.file_offset, a.file_size); #if defined(__linux__) - reclaim_copied_file_source(emb_mmap.addr, emb_mmap.len, - (const char *)emb_mmap.addr + a.file_offset, a.file_size, - emb_mmap.fd, "token_embd.weight"); + if (reclaim_sources) { + reclaim_copied_file_source(emb_mmap.addr, emb_mmap.len, + (const char *)emb_mmap.addr + a.file_offset, a.file_size, + emb_mmap.fd, "token_embd.weight"); + } #endif emb_mmap.close_map(); } else { @@ -2231,7 +2241,7 @@ bool build_deepseek4_moe_hybrid_storage_from_file_with_mmap( cfg, backend, placement, layer_descs, layer_file_data, mmap.addr, mmap.len, out, err, 0, cold_gpu_backend #if defined(__linux__) - , mmap.fd + , ds4_image_capable(w) ? mmap.fd : -1 #endif ); // Advice borrows the original fd only while construction is in progress. From f248dfded0c2b8a7d6e6a8a0b1f0e5cd9fc5978b Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:45:10 +0200 Subject: [PATCH 084/123] chore(ds4v): drop the rejected IQ85 recipe It failed its own frozen quality gate (12/16 vs 15/16) and reached 20 tok/s against a 35 tok/s target, and was never installed. The MIX converter stays. Co-Authored-By: Claude Fable 5.1 --- server/test/test_ds4_iq_converter.cpp | 212 ----------------- server/tools/ds4_mix_converter/CMakeLists.txt | 12 +- server/tools/ds4_mix_converter/README.md | 68 ------ .../ds4_mix_converter/ds4_mix_converter.cpp | 29 --- .../tools/ds4_mix_converter/expert_batches.h | 64 ------ .../ds4_mix_converter/iq85_converter.inc | 214 ------------------ server/tools/ds4_mix_converter/prove_iq85.py | 82 ------- 7 files changed, 2 insertions(+), 679 deletions(-) delete mode 100644 server/test/test_ds4_iq_converter.cpp delete mode 100644 server/tools/ds4_mix_converter/README.md delete mode 100644 server/tools/ds4_mix_converter/expert_batches.h delete mode 100644 server/tools/ds4_mix_converter/iq85_converter.inc delete mode 100644 server/tools/ds4_mix_converter/prove_iq85.py diff --git a/server/test/test_ds4_iq_converter.cpp b/server/test/test_ds4_iq_converter.cpp deleted file mode 100644 index 639932c46..000000000 --- a/server/test/test_ds4_iq_converter.cpp +++ /dev/null @@ -1,212 +0,0 @@ -#include -#define main ds4_mix_converter_main -#include "../tools/ds4_mix_converter/ds4_mix_converter.cpp" -#undef main - -template void must_fail(F fn) { - bool rejected = false; - try { fn(); } catch (const std::exception &) { rejected = true; } - if (!rejected) fail("expected rejection"); -} - -void test_source_artifact() { - char pattern[]="/tmp/ds4-iq-source-XXXXXX"; - if(!::mkdtemp(pattern)) fail("mkdtemp failed"); - const fs::path root=pattern; - struct Cleanup {fs::path path; ~Cleanup(){std::error_code ec;fs::remove_all(path,ec);}} cleanup{root}; - json config; - for(const char * key:{"num_hidden_layers","num_attention_heads","num_key_value_heads","head_dim", - "qk_rope_head_dim","q_lora_rank","o_lora_rank","o_groups","num_experts_per_tok","n_shared_experts", - "moe_intermediate_size","num_hash_layers","sliding_window","index_n_heads","index_head_dim", - "index_topk","hc_mult","hc_sinkhorn_iters"}) config[key]=1; - config["n_routed_experts"]=2;config["hidden_size"]=256;config["vocab_size"]=2; - std::ofstream(root/"config.json")< payload; - auto add=[&](const std::string & name,const std::string & dtype,const std::vector& shape,std::vector bytes){ - const size_t start=payload.size();payload.insert(payload.end(),bytes.begin(),bytes.end()); - header[name]={{"dtype",dtype},{"shape",shape},{"data_offsets",{start,payload.size()}}}; - index["weight_map"][name]="model.safetensors"; - }; - std::vector bf16(1024); - for(size_t i=0;i<512;++i){uint16_t v=float_to_bf16(std::sin(float(i)));std::memcpy(bf16.data()+i*2,&v,2);} - add("embed.weight","BF16",{2,256},bf16);add("head.weight","BF16",{2,256},bf16); - for(const char * name:{"norm.weight","hc_head_base","hc_head_fn","hc_head_scale"}) - add(name,"F32",{4},std::vector(16)); - add("layers.0.attn.wq_a.weight","F8_E4M3",{2,256},std::vector(512,0x38)); - add("layers.0.attn.wq_a.scale","F8_E8M0",{1,2},{127,128}); - add("layers.0.ffn.gate.tid2eid","I64",{2},std::vector(16)); - add("vision.test.weight","BF16",{2,3},std::vector(12,0x3f)); - for(uint32_t e=0;e<2;++e) for(const auto & recipe:kExpertRecipes) { - auto packed=std::vector(256); - for(size_t i=0;i(16,127)); - } - std::ofstream(root/"model.safetensors.index.json")<()) fail("plan size differs from serialized artifact"); - const auto serial=read_file(options.output); - must_fail([&]{run_iq85(options,source,1,2);}); - if(read_file(options.output)!=serial) fail("existing artifact was overwritten"); - options.output=root/"parallel.gguf";options.encode_threads=8;run_iq85(options,source,1,2); - if(read_file(options.output)!=serial) fail("full original-source serial/parallel GGUF differs"); - // Already-open source metadata does not mask a later short read; no final artifact may appear. - fs::resize_file(root/"model.safetensors",8+len+payload.size()-1); - options.output=root/"short-read.gguf"; - must_fail([&]{run_iq85(options,source,1,2);}); - if(fs::exists(options.output)) fail("failed conversion published final artifact"); -} - -void test_fp8_dense_analytic() { - // Independent analytical decoding oracle: these E4M3 bytes represent exactly - // +1, -1, +2 and +0.5. E8M0 127/128 mean scale 1/2. Both tile axes cross 128. - constexpr size_t rows=129,cols=256; - const std::array codes={0x38,0xb8,0x40,0x30}; - const std::array decoded={1.0f,-1.0f,2.0f,0.5f}; - const std::array scales={127,128,128,127}; - std::vector payload(rows*cols); - std::vector expected_values(rows*cols); - for(size_t row=0;row=128) != (col>=128)) ? 2.0f : 1.0f; - expected_values[row*cols+col]=decoded[choice]*scale; - } - char pattern[]="/tmp/ds4-iq-fp8-XXXXXX"; - const int fd=::mkstemp(pattern); - if(fd<0) fail("FP8 fixture mkstemp failed"); - struct Cleanup {const char * path;~Cleanup(){::unlink(path);}} cleanup{pattern}; - if(::write(fd,payload.data(),payload.size())!=ssize_t(payload.size()) || - ::write(fd,scales.data(),scales.size())!=ssize_t(scales.size())) { - ::close(fd);fail("FP8 fixture write failed"); - } - ::close(fd); - StEntry weight;weight.name="layers.0.attn.wq_a.weight";weight.dtype="F8_E4M3"; - weight.path=pattern;weight.shape={rows,cols};weight.size=payload.size(); - StEntry scale;scale.name="layers.0.attn.wq_a.scale";scale.dtype="F8_E8M0"; - scale.path=pattern;scale.shape={2,2};scale.offset=payload.size();scale.size=scales.size(); - TensorSpec spec;spec.name="blk.0.attn_q_a.weight";spec.source=&weight;spec.scale=&scale; - spec.ne=reverse_shape(weight);spec.type=GGML_TYPE_Q8_0;spec.producer=Producer::DenseFp8; - std::unique_ptr out(std::tmpfile(),std::fclose); - if(!out) fail("FP8 output tmpfile failed"); - iq85_write_dense(out.get(),spec); - std::vector expected(ggml_row_size(GGML_TYPE_Q8_0,cols)*rows),actual(expected.size()); - if(ggml_quantize_chunk(GGML_TYPE_Q8_0,expected_values.data(),expected.data(),0,rows,cols,nullptr)!=expected.size()) - fail("analytical Q8 expected byte size mismatch"); - std::rewind(out.get()); - if(std::fread(actual.data(),1,actual.size(),out.get())!=actual.size() || actual!=expected || std::fgetc(out.get())!=EOF) - fail("FP8 Q8 differs from independent analytical source/scales oracle"); -} - -int main() { - try { - StEntry source; - source.name = "layers.0.attn.wq_a.weight"; - source.shape = {2,256}; - TensorSpec spec; - spec.source = &source; spec.type = GGML_TYPE_BF16; spec.ne = reverse_shape(source); - for (const char * name : {"blk.0.attn_q_a.weight", "output.weight", "token_embd.weight"}) { - spec.name = name; - if (!iq85_dense(spec)) fail("eligible dense tensor excluded"); - } - for (const char * name : {"blk.0.hc_attn_fn.weight", "blk.0.ffn_gate_inp.weight", - "blk.0.indexer.proj.weight", "blk.0.attn_compressor_kv.weight", "vision.blocks.0.attn.wqkv.weight"}) { - spec.name = name; - if (iq85_dense(spec)) fail("protected tensor quantized"); - } - spec.name = "output.weight"; source.shape={256}; spec.ne=reverse_shape(source); - if (iq85_dense(spec)) fail("vector quantized"); - - // Actual dense BF16 source -> Q8 bytes against canonical row encoding. - char pattern[] = "/tmp/ds4-iq-test-XXXXXX"; - const int fd = ::mkstemp(pattern); - if (fd < 0) fail("mkstemp failed"); - source.path = pattern; source.dtype = "BF16"; source.shape = {2,256}; source.size = 1024; - std::vector bf16(512); - std::vector values(512); - for (size_t i=0;i<512;++i) { bf16[i] = float_to_bf16(std::sin(float(i))*2); values[i] = bf16_to_float(bf16[i]); } - if (::write(fd,bf16.data(),1024)!=1024) fail("fixture write failed"); - ::close(fd); - spec.ne = {256,2}; spec.type = GGML_TYPE_Q8_0; spec.producer = Producer::Raw; - std::unique_ptr output(std::tmpfile(),std::fclose); - if (!output) fail("tmpfile failed"); - iq85_write_dense(output.get(),spec); - std::vector expected(ggml_row_size(GGML_TYPE_Q8_0,256)*2), actual(expected.size()); - ggml_quantize_chunk(GGML_TYPE_Q8_0,values.data(),expected.data(),0,2,256,nullptr); - std::rewind(output.get()); - if (std::fread(actual.data(),1,actual.size(),output.get())!=actual.size() || actual!=expected) - fail("source-to-Q8 bytes differ from canonical encoder"); - ::unlink(pattern); - - spec.producer=Producer::Expert; spec.name="blk.0.ffn_gate_exps.weight"; spec.ne={256,2,1}; - std::optional imatrix=Imatrix{{spec.name,{1,std::vector(256,1)}}}; - iq85_validate_importance({spec},imatrix,"uniform-unvalidated"); - must_fail([&]{iq85_validate_importance({spec},imatrix,"activation-derived");}); - std::fill(imatrix->at(spec.name).values.begin(),imatrix->at(spec.name).values.end(),0); - must_fail([&]{iq85_validate_importance({spec},imatrix,"uniform-unvalidated");}); - imatrix->at(spec.name).values.resize(256*3); - for(size_t i=0;i<256*3;++i)imatrix->at(spec.name).values[i]=float(i+1); - if(iq85_importance(imatrix,spec,2,3)[0]!=513) fail("wrong per-expert importance slice"); - must_fail([&]{iq85_importance(imatrix,spec,0,2);}); - must_fail([&]{iq85_importance(imatrix,spec,3,3);}); - - // Canonical IQ rows encoded concurrently must equal serial bytes, including - // nonuniform importance and a batch tail. Shared lookup tables are initialized first. - std::vector importance(256); - for(size_t i=0;i<256;++i) importance[i]=0.25f+float(i%13); - for(auto type:{GGML_TYPE_IQ2_XXS,GGML_TYPE_IQ2_XS}) { - ggml_quantize_init(type); - auto encode=[&](uint32_t e) { - auto v=values; for(auto & x:v) x+=float(e)*0.015625f; - ds4_mix_detail::EncodedExpert bytes(ggml_row_size(type,256)*2); - if(ggml_quantize_chunk(type,v.data(),bytes.data(),0,2,256,importance.data())!=bytes.size()) - fail("IQ size mismatch"); - return bytes; - }; - std::vector serial,parallel,parallel16; - auto writer=[](std::vector& out){return [&out](uint32_t,const auto & b){out.insert(out.end(),b.begin(),b.end());};}; - ds4_mix_detail::ordered_expert_batches(17,1,ggml_row_size(type,256)*2,encode,writer(serial)); - ds4_mix_detail::ordered_expert_batches(17,8,ggml_row_size(type,256)*2,encode,writer(parallel)); - ds4_mix_detail::ordered_expert_batches(17,16,ggml_row_size(type,256)*2,encode,writer(parallel16)); - if(serial!=parallel || serial!=parallel16) fail("IQ parallel output differs"); - } - must_fail([&]{ds4_mix_detail::checked_encoded_size(UINT64_MAX,2,16);}); - must_fail([&]{ds4_mix_detail::checked_encoded_size(1,1,17);}); - std::vector args={"converter","--input","/unused","--output","/unused/new.gguf", - "--recipe","iq85","--imatrix","/unused/importance.dat","--imatrix-provenance","uniform-unvalidated"}; - auto parse=[&] {std::vector argv;for(auto & arg:args)argv.push_back(arg.data());return parse_options(argv.size(),argv.data());}; - if(parse().encode_threads!=1) fail("default encode thread count changed"); - args.push_back("--encode-threads");args.push_back("16"); - if(parse().encode_threads!=16) fail("CLI rejects sixteen encoder workers"); - args.back()="17";must_fail([&]{parse();}); - unsigned writes=0; - must_fail([&]{ds4_mix_detail::ordered_expert_batches(17,8,1, - [](uint32_t e){if(e==3) fail("injected worker failure");return ds4_mix_detail::EncodedExpert(1);}, - [&](uint32_t,const auto&){++writes;});}); - if(writes!=3) fail("publication continued after worker failure"); - test_fp8_dense_analytic(); - test_source_artifact(); - std::cout<<"PASS: dense preservation/canonical encoding, imatrix provenance, IQ parallel determinism, failure bounds\n"; - return 0; - } catch(const std::exception& e) {std::cerr<<"FAIL: "< plan.json -``` - -Encoding uses canonical ggml IQ/Q8 encoders. `--encode-threads 1..16` (default 1) -parallelizes independent experts in ordered batches; lookup tables initialize -before workers launch. Each worker owns one encoded expert plus row scratch and -two source descriptors. With the actual 4096x2048 expert dimensions, encoded -payloads are 2.0625 MiB gate/up and 2.3125 MiB down, at most 37 MiB for sixteen -results. This excludes canonical IQ lookup/encoder scratch, source headers, -imatrix and thread stacks. Dense encoding uses one row plus its small FP8 scale -grid. No entire expert is expanded to F32. Worker failures drain launched jobs -before propagation. A fresh `.partial` path is exclusively created; complete -bytes/header and raw preserved tensors are verified before atomic no-overwrite -publication. Failed partial files remain for inspection and are never reused. - -Build/test on the authorized remote Linux CPU host only: - -```sh -cmake -S server/tools/ds4_mix_converter -B /absolute/fresh-build -DCMAKE_BUILD_TYPE=Release -cmake --build /absolute/fresh-build -j2 -ctest --test-dir /absolute/fresh-build --output-on-failure -``` - -`test_ds4_iq_converter` covers dense preservation, canonical source-to-Q8 bytes, -an independent analytical FP8/scaling fixture crossing 128-row/column boundaries, -importance rejection/provenance, canonical IQ serial/8/16-worker identity with -nonuniform weights and a batch tail, and allocation/worker failure bounds. -`prove_iq85.py` runs exactly one layer and 1–17 experts (default 8), using 1, 8, -then 8 workers by default. `--reference-threads 1|8` and -`--parallel-threads 8|16` allow a bounded 8/16/repeat16 comparison after the -serial/8 qualification. It uses fresh output paths, timeouts, exact plan-size checks, hashes -and whole-file identity. It records binary/imatrix/source-index hashes and does -not build, run GPU code or convert a full model. Synthetic and bounded source -tests do not establish text/image quality or long-context performance; those -require separate runtime qualification against the existing model. diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index e28d7691c..d3245ae03 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -1,7 +1,6 @@ #include "ggml.h" #include "gguf.h" #include "rocmfpx.h" -#include "expert_batches.h" #include @@ -552,10 +551,6 @@ struct LayerCalibration { }; struct Options { - std::string recipe = "mix"; - std::string imatrix_provenance; - bool plan_only = false; - unsigned encode_threads = 1; fs::path input; fs::path output; std::optional imatrix; @@ -571,9 +566,6 @@ struct Options { void usage(const char * argv0) { std::cerr << "Usage: " << argv0 << " --input DIR --output FILE (--imatrix FILE | --absmax-only)\n" << " [--layer-start N] [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force]\n"; - std::cerr << "IQ85: --recipe iq85 --imatrix FILE --imatrix-provenance " - << "uniform-unvalidated|activation-derived|transferred-text-calibration " - << "[--plan-only] [--encode-threads 1..16]; fresh output only\n"; } int parse_nonnegative(const char * value, const std::string & option, bool allow_zero = true) { @@ -595,10 +587,6 @@ Options parse_options(int argc, char ** argv) { return argv[i]; }; if (arg == "--input") out.input = value(); - else if (arg == "--recipe") out.recipe = value(); - else if (arg == "--imatrix-provenance") out.imatrix_provenance = value(); - else if (arg == "--plan-only") out.plan_only = true; - else if (arg == "--encode-threads") out.encode_threads = parse_nonnegative(value(), arg, false); else if (arg == "--output") out.output = value(); else if (arg == "--imatrix") out.imatrix = fs::path(value()); else if (arg == "--absmax-only") out.absmax_only = true; @@ -612,17 +600,6 @@ Options parse_options(int argc, char ** argv) { else fail("unknown option " + arg); } if (out.input.empty() || out.output.empty()) fail("--input and --output are required"); - if (out.recipe != "mix" && out.recipe != "iq85") fail("--recipe must be mix or iq85"); - if (out.encode_threads > 16) fail("--encode-threads must be in 1..16"); - if (out.recipe == "iq85") { - if (out.force) fail("iq85 never overwrites artifacts; use a fresh output path"); - if (out.absmax_only || !out.imatrix) fail("iq85 requires --imatrix; --absmax-only is unsupported"); - if (out.imatrix_provenance != "uniform-unvalidated" && out.imatrix_provenance != "activation-derived" && - out.imatrix_provenance != "transferred-text-calibration") - fail("iq85 requires --imatrix-provenance uniform-unvalidated|activation-derived|transferred-text-calibration"); - } else if (out.plan_only || out.encode_threads != 1 || !out.imatrix_provenance.empty()) { - fail("--plan-only, --encode-threads and --imatrix-provenance require --recipe iq85"); - } if (out.absmax_only == out.imatrix.has_value()) { fail("choose exactly one of --imatrix FILE or --absmax-only"); } @@ -1377,8 +1354,6 @@ void write_gguf(const Options & options, const SafeTensorSet & source, verify_artifact(options.output, gumix_path, plan, p4, gumix); } -#include "iq85_converter.inc" - } // namespace int main(int argc, char ** argv) { @@ -1398,10 +1373,6 @@ int main(int argc, char ** argv) { if (!options.experts_only && (layers != source_layers || experts != source_experts)) { fail("layer/expert limits are permitted only with --experts-only smoke artifacts"); } - if (options.recipe == "iq85") { - run_iq85(options, source, layers, experts); - return 0; - } if (options.validate_input_only) { const auto layout = validate_input_layout(source, layers, experts); const auto plan = make_plan(source, layout, experts, options.experts_only); diff --git a/server/tools/ds4_mix_converter/expert_batches.h b/server/tools/ds4_mix_converter/expert_batches.h deleted file mode 100644 index f17542595..000000000 --- a/server/tools/ds4_mix_converter/expert_batches.h +++ /dev/null @@ -1,64 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include -#include -#include - -// Internal scheduling only: no source, calibration, codec or FILE state lives here. -namespace ds4_mix_detail { -using EncodedExpert = std::vector; -inline size_t checked_encoded_size(uint64_t row_bytes, uint64_t rows, unsigned workers) { - if (workers < 1 || workers > 16) throw std::runtime_error("encode threads must be in 1..16"); - if (!row_bytes || !rows || row_bytes > std::numeric_limits::max()/rows) - throw std::runtime_error("invalid or overflowing encoded expert size"); - const uint64_t bytes = row_bytes*rows; - if (bytes > std::numeric_limits::max()/workers || - bytes*workers > std::numeric_limits::max()) - throw std::runtime_error("overflowing encoded batch size"); - return static_cast(bytes); -} -struct AsyncExpert { - template auto operator()(Task task) const { - return std::async(std::launch::async, std::move(task)); - } -}; -// Launch is injectable only to test failure after some tasks have started. -// All launched futures are explicitly consumed before any captured owner can die. -template -void ordered_expert_batches(uint32_t count, unsigned workers, size_t expert_bytes, - Encode encode, Write write, Launch launch = {}) { - checked_encoded_size(expert_bytes, 1, workers); - auto publish = [&](uint32_t expert, const EncodedExpert & bytes) { - if (bytes.size() != expert_bytes) throw std::runtime_error("encoded expert byte count mismatch"); - write(expert, bytes); - }; - if (workers == 1) { - for (uint32_t expert = 0; expert < count; ++expert) publish(expert, encode(expert)); - return; - } - for (uint32_t first = 0; first < count;) { - const unsigned batch = std::min(workers, count - first); - std::vector> futures; - futures.reserve(batch); // Allocation precedes all launches. - std::exception_ptr failure; - try { - for (unsigned i = 0; i < batch; ++i) { - const uint32_t expert = first + i; - futures.push_back(launch([&encode, expert] { return encode(expert); })); - } - } catch (...) { failure = std::current_exception(); } - for (unsigned i = 0; i < futures.size(); ++i) { - try { - EncodedExpert bytes = futures[i].get(); - if (!failure) publish(first + i, bytes); - } catch (...) { if (!failure) failure = std::current_exception(); } - } - if (failure) std::rethrow_exception(failure); - first += batch; // No next batch until every future was drained. - } -} -} // namespace ds4_mix_detail diff --git a/server/tools/ds4_mix_converter/iq85_converter.inc b/server/tools/ds4_mix_converter/iq85_converter.inc deleted file mode 100644 index c9e8a1b2e..000000000 --- a/server/tools/ds4_mix_converter/iq85_converter.inc +++ /dev/null @@ -1,214 +0,0 @@ -// Included inside the converter's anonymous namespace. Reuses only source decoding -// and metadata helpers; the existing MIX calibration/encoding path is unchanged. -bool iq85_dense(const TensorSpec & spec) { - if (!spec.source || spec.type != GGML_TYPE_BF16 || spec.source->shape.size() != 2 || - spec.ne.size() < 2 || spec.ne[0] % 32) return false; - if (spec.name == "token_embd.weight" || spec.name == "output.weight") return true; - const auto parsed = parse_layer_name(spec.source->name); - if (!parsed) return false; - static const std::set leaves = { - "attn_kv.weight", "attn_output_a.weight", "attn_output_b.weight", - "attn_q_a.weight", "attn_q_b.weight", "ffn_down_shexp.weight", - "ffn_gate_shexp.weight", "ffn_up_shexp.weight"}; - const size_t dot = spec.name.find('.', 4); - return dot != std::string::npos && leaves.count(spec.name.substr(dot + 1)); -} - -std::vector iq85_plan(const SafeTensorSet & source, - const std::vector & layout, uint32_t experts, bool smoke) { - auto plan = make_plan(source, layout, experts, smoke); - for (auto & spec : plan) { - if (spec.producer == Producer::Expert) { - spec.type = spec.recipe->surface == Surface::Down ? GGML_TYPE_IQ2_XS : GGML_TYPE_IQ2_XXS; - } else if (iq85_dense(spec)) { - spec.type = GGML_TYPE_Q8_0; - } - } - return plan; -} - -const float * iq85_importance(const std::optional & imatrix, - const TensorSpec & spec, uint32_t expert, uint32_t source_experts) { - if (!imatrix) fail("iq85 requires importance weights"); - const auto it = imatrix->find(spec.name); - if (it == imatrix->end()) fail("imatrix missing " + spec.name); - const size_t in = spec.ne[0]; - const size_t full = checked_mul(in, source_experts, "per-expert importance dimensions"); - const auto & values = it->second.values; - if (values.size() != in && values.size() != full) - fail("iq85 importance dimensions must be input width or input width * SOURCE expert count: " + spec.name); - if (expert >= source_experts) fail("iq85 importance expert out of range"); - return values.data() + (values.size() == in ? 0 : size_t(expert)*in); -} - -void iq85_validate_importance(const std::vector & plan, - const std::optional & imatrix, const std::string & provenance, uint32_t source_experts = 1) { - bool any_nonuniform = false; - for (const auto & spec : plan) { - if (spec.producer != Producer::Expert) continue; - for (uint32_t expert = 0; expert < uint32_t(spec.ne[2]); ++expert) { - const float * values = iq85_importance(imatrix, spec, expert, source_experts); - if (std::none_of(values, values + spec.ne[0], [](float x) { return x > 0; })) - fail("iq85 requires nonzero importance for every selected expert: " + spec.name); - any_nonuniform |= std::any_of(values, values + spec.ne[0], [&](float x) { return x != values[0]; }); - } - } - if (provenance != "uniform-unvalidated" && !any_nonuniform) - fail("all importance rows are uniform; label these uniform-unvalidated, not activation-derived"); - std::cerr << "[iq85] imatrix provenance=" << provenance - << "; this label is operator supplied, not a quality qualification\n"; -} - -void iq85_write_experts(FILE * out, const SafeTensorSet & source, const TensorSpec & spec, - uint32_t experts, const std::optional & imatrix, unsigned workers) { - const size_t row_bytes = ggml_row_size(spec.type, spec.ne[0]); - const size_t expert_bytes = ds4_mix_detail::checked_encoded_size(row_bytes, spec.ne[1], workers); - const uint32_t source_experts = config_u32(source.config(), "n_routed_experts"); - // Initialize shared, immutable IQ lookup tables before any worker launches. - ggml_quantize_init(spec.type); - ds4_mix_detail::ordered_expert_batches(experts, workers, expert_bytes, - [&](uint32_t expert) { - const float * importance = iq85_importance(imatrix, spec, expert, source_experts); - const auto shape = validate_expert_source(source, spec.layer, expert, *spec.recipe); - if (shape.in != spec.ne[0] || shape.out != spec.ne[1]) fail("iq85 expert shape drift"); - const auto & w = source.at(source_expert_name(spec.layer, expert, *spec.recipe, "weight")); - const auto & s = source.at(source_expert_name(spec.layer, expert, *spec.recipe, "scale")); - OpenTensorPair input(w, s); - std::vector packed, scales; - std::vector values; - ds4_mix_detail::EncodedExpert bytes(expert_bytes); - for (uint32_t row = 0; row < shape.out; ++row) { - decode_expert_row(input, row, shape.in, packed, scales, values); - const size_t n = ggml_quantize_chunk(spec.type, values.data(), bytes.data() + row*row_bytes, - 0, 1, shape.in, importance); - if (n != row_bytes) fail("iq85 encoder returned wrong row size"); - } - return bytes; - }, [&](uint32_t expert, const ds4_mix_detail::EncodedExpert & bytes) { - fwrite_exact(out, bytes.data(), bytes.size(), spec.name); - std::cerr << "[iq85 encode] " << spec.name << " expert " << expert + 1 << '/' << experts << '\n'; - }); -} - -void iq85_write_dense(FILE * out, const TensorSpec & spec) { - const auto & w = *spec.source; - const size_t cols = spec.ne[0], rows = spec.ne[1]; - FileDescriptor wf(w.path); - std::vector values(cols); - std::vector input(cols * (spec.producer == Producer::DenseFp8 ? 1 : 2)); - std::vector scales; - if (spec.producer == Producer::DenseFp8) { - FileDescriptor sf(spec.scale->path); - scales.resize(spec.scale->size); - pread_exact(sf.fd, scales.data(), scales.size(), spec.scale->offset, spec.scale->name); - } else if (w.dtype != "BF16") fail("iq85 unsupported dense source " + w.name); - const size_t row_bytes = ggml_row_size(GGML_TYPE_Q8_0, cols); - std::vector encoded(row_bytes); - for (size_t row = 0; row < rows; ++row) { - pread_exact(wf.fd, input.data(), input.size(), w.offset + row*input.size(), w.name); - for (size_t col = 0; col < cols; ++col) { - if (spec.producer == Producer::DenseFp8) { - values[col] = fp8_e4m3fn(input[col]) * fp8_e8m0(scales[(row/128)*spec.scale->shape[1] + col/128]); - } else { - uint16_t b; - std::memcpy(&b, input.data() + col*2, 2); - values[col] = bf16_to_float(b); - } - if (!std::isfinite(values[col])) fail("non-finite iq85 dense source " + w.name); - } - if (ggml_quantize_chunk(GGML_TYPE_Q8_0, values.data(), encoded.data(), 0, 1, cols, nullptr) != row_bytes) - fail("Q8 encoder returned wrong row size"); - fwrite_exact(out, encoded.data(), encoded.size(), spec.name); - } -} - -void run_iq85(const Options & options, const SafeTensorSet & source, uint32_t layers, uint32_t experts) { - const auto layout = validate_input_layout(source, layers, experts); - const auto plan = iq85_plan(source, layout, experts, options.experts_only); - const std::optional imatrix = load_imatrix(*options.imatrix); - iq85_validate_importance(plan, imatrix, options.imatrix_provenance, config_u32(source.config(), "n_routed_experts")); - std::unique_ptr ctx(gguf_init_empty(), gguf_free); - if (!ctx) fail("gguf_init_empty failed"); - set_model_metadata(ctx.get(), source, layers, experts, false, options.experts_only, {}); - for (const char * key : {"deepseek4.p4mix.sidecar", "deepseek4.mix.calibration", - "deepseek4.mix.lower_quality_absmax_only", "deepseek4.mix.experts_only_smoke_artifact"}) - gguf_remove_key(ctx.get(), key); - gguf_set_val_str(ctx.get(), "general.name", "DeepSeek-V4-Flash-Vision iq85 experimental"); - gguf_set_val_u32(ctx.get(), "general.file_type", GGML_FTYPE_MOSTLY_IQ2_XXS); - gguf_set_val_str(ctx.get(), "deepseek4.quant.recipe", "iq85-v1: gate/up IQ2_XXS; down IQ2_XS; selected dense Q8_0"); - gguf_set_val_str(ctx.get(), "deepseek4.quant.imatrix_provenance", options.imatrix_provenance.c_str()); - gguf_set_val_str(ctx.get(), "deepseek4.quant.imatrix_source", options.imatrix->filename().c_str()); - gguf_set_val_bool(ctx.get(), "deepseek4.quant.quality_validated", false); - gguf_set_val_bool(ctx.get(), "deepseek4.quant.experts_only_smoke_artifact", options.experts_only); - std::vector> descriptors; - uint64_t data_bytes = 0; - json rows = json::array(); - for (const auto & spec : plan) { - uint64_t n = 1; - for (auto dim : spec.ne) { - if (dim <= 0) fail("iq85 invalid tensor dimension"); - n = checked_mul(n, dim, "iq85 descriptor element count"); - } - if (n > size_t(-1)/ggml_type_size(spec.type)) fail("iq85 tensor exceeds host size bounds"); - descriptors.push_back(make_tensor_descriptor(spec)); - gguf_add_tensor(ctx.get(), descriptors.back().get()); - const int64_t id = gguf_find_tensor(ctx.get(), spec.name.c_str()); - const uint64_t offset = gguf_get_tensor_offset(ctx.get(), id), bytes = gguf_get_tensor_size(ctx.get(), id); - if (bytes > UINT64_MAX - offset) fail("iq85 plan size overflow"); - data_bytes = offset + bytes; - rows.push_back({{"name",spec.name},{"type",int(spec.type)},{"bytes",bytes}}); - } - const uint64_t meta_bytes = gguf_get_meta_size(ctx.get()); - if (data_bytes > UINT64_MAX - meta_bytes) fail("iq85 file size overflow"); - const uint64_t total = meta_bytes + data_bytes; - std::cout << json({{"recipe","iq85-v1"},{"file_bytes",total},{"decimal_GB",double(total)/1e9}, - {"metadata_bytes",meta_bytes},{"tensor_bytes_with_padding",data_bytes}, - {"imatrix_provenance",options.imatrix_provenance},{"quality_validated",false}, - {"experts_only",options.experts_only},{"tensors",rows}}).dump(2) << '\n'; - if (options.plan_only || options.validate_input_only) return; - if (fs::exists(options.output) || fs::exists(options.output.string()+".gumix.bin")) fail("iq85 output exists"); - if (!options.output.parent_path().empty()) fs::create_directories(options.output.parent_path()); - const fs::path temporary = options.output.string() + ".partial"; - const int fd = ::open(temporary.c_str(), O_CREAT | O_EXCL | O_WRONLY, 0600); - if (fd < 0) fail("cannot exclusively create " + temporary.string()); - FILE * raw = ::fdopen(fd, "wb"); - if (!raw) { ::close(fd); fail("fdopen failed"); } - std::unique_ptr out(raw, std::fclose); - std::vector metadata(meta_bytes); - gguf_get_meta_data(ctx.get(), metadata.data()); - fwrite_exact(out.get(), metadata.data(), metadata.size(), "iq85 metadata"); - std::array zero{}; - for (const auto & spec : plan) { - const int64_t id = gguf_find_tensor(ctx.get(), spec.name.c_str()); - const uint64_t expected = meta_bytes + gguf_get_tensor_offset(ctx.get(), id); - const off_t position = ::ftello(out.get()); - if (position < 0 || uint64_t(position) > expected || expected - position >= kAlignment) - fail("iq85 stream offset mismatch"); - fwrite_exact(out.get(), zero.data(), expected-position, "iq85 padding"); - if (spec.producer == Producer::Expert) iq85_write_experts(out.get(), source, spec, experts, imatrix, options.encode_threads); - else if (spec.type == GGML_TYPE_Q8_0) iq85_write_dense(out.get(), spec); - else if (spec.producer == Producer::Raw) copy_raw(out.get(), *spec.source); - else if (spec.producer == Producer::DenseFp8) write_dense_fp8(out.get(), *spec.source, *spec.scale); - else if (spec.producer == Producer::Int64ToInt32) write_int64_to_int32(out.get(), *spec.source); - else fail("iq85 unknown producer"); - const off_t after = ::ftello(out.get()); - if (after < 0 || uint64_t(after) != expected + gguf_get_tensor_size(ctx.get(), id)) fail("iq85 producer size mismatch"); - } - if (std::fflush(out.get()) || ::fsync(::fileno(out.get()))) fail("iq85 flush/fsync failed"); - if (std::fclose(out.release())) fail("iq85 close failed"); - // Validate the partial file BEFORE publication; link is atomic and refuses overwrite. - gguf_init_params params = {true, nullptr}; - std::unique_ptr parsed(gguf_init_from_file(temporary.c_str(), params), gguf_free); - if (!parsed || fs::file_size(temporary) != total || gguf_get_n_tensors(parsed.get()) != int64_t(plan.size())) - fail("iq85 completed file size/header verification failed"); - FileDescriptor input(temporary); - for (const auto & spec : plan) { - const int64_t id = gguf_find_tensor(parsed.get(), spec.name.c_str()); - if (id < 0 || gguf_get_tensor_type(parsed.get(), id) != spec.type) fail("iq85 verification type mismatch"); - if (spec.producer == Producer::Raw && spec.type != GGML_TYPE_Q8_0) - compare_raw_passthrough(input.fd, gguf_get_data_offset(parsed.get()) + gguf_get_tensor_offset(parsed.get(), id), *spec.source); - } - if (::link(temporary.c_str(), options.output.c_str())) fail("iq85 atomic no-overwrite publication failed"); - if (::unlink(temporary.c_str())) fail("iq85 published but partial link cleanup failed"); - std::cerr << "[iq85 done] verified/published " << total << " bytes (" << double(total)/1e9 << " decimal GB)\n"; -} diff --git a/server/tools/ds4_mix_converter/prove_iq85.py b/server/tools/ds4_mix_converter/prove_iq85.py deleted file mode 100644 index 659618cf5..000000000 --- a/server/tools/ds4_mix_converter/prove_iq85.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -"""Bounded CPU-only original-source IQ85 pilot; never converts a full model.""" -import argparse -import hashlib -import json -import pathlib -import subprocess -import sys -import time - - -def sha256(path): - h = hashlib.sha256() - with path.open("rb") as f: - for chunk in iter(lambda: f.read(8 * 1024 * 1024), b""): - h.update(chunk) - return h.hexdigest() - - -def main(): - p = argparse.ArgumentParser(description=__doc__) - p.add_argument("--binary", type=pathlib.Path, required=True) - p.add_argument("--input", type=pathlib.Path, required=True) - p.add_argument("--imatrix", type=pathlib.Path, required=True) - p.add_argument("--imatrix-provenance", choices=["uniform-unvalidated", "activation-derived", "transferred-text-calibration"], required=True) - p.add_argument("--output-dir", type=pathlib.Path, required=True) - p.add_argument("--experts", type=int, default=8, choices=range(1, 18)) - p.add_argument("--reference-threads", type=int, default=1, choices=[1, 8]) - p.add_argument("--parallel-threads", type=int, default=8, choices=[8, 16]) - p.add_argument("--timeout", type=int, default=14400) - a = p.parse_args() - if sys.platform != "linux": - p.error("run only on the authorized Linux CPU host") - if not 1 <= a.timeout <= 28800: - p.error("timeout must be 1..28800 seconds per lane") - for name in ("binary", "input", "imatrix", "output_dir"): - value = getattr(a, name) - if not value.is_absolute(): - p.error(f"--{name.replace('_','-')} must be absolute") - a.output_dir.mkdir(parents=False, exist_ok=False) - manifest = {"recipe": "iq85-v1", "quality_validated": False, - "imatrix_provenance": a.imatrix_provenance, - "binary_sha256": sha256(a.binary), "imatrix_sha256": sha256(a.imatrix), - "source_index_sha256": sha256(a.input / "model.safetensors.index.json"), "lanes": []} - common = [str(a.binary), "--input", str(a.input), "--recipe", "iq85", - "--imatrix", str(a.imatrix), "--imatrix-provenance", a.imatrix_provenance, - "--layer-count", "1", "--expert-limit", str(a.experts), "--experts-only"] - def save(): - (a.output_dir / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n") - save() - reference_label = "serial" if a.reference_threads == 1 else "reference8" - for label, threads in ((reference_label, a.reference_threads), - (f"parallel{a.parallel_threads}", a.parallel_threads), - (f"repeat{a.parallel_threads}", a.parallel_threads)): - artifact = a.output_dir / (label + ".gguf") - command = common + ["--output", str(artifact), "--encode-threads", str(threads)] - start = time.monotonic() - lane = {"label": label, "command": command} - manifest["lanes"].append(lane) - save() - try: - with (a.output_dir / (label + ".plan.json")).open("w") as out, (a.output_dir / (label + ".log")).open("w") as err: - result = subprocess.run(command, stdout=out, stderr=err, timeout=a.timeout, check=False) - lane.update(exit_code=result.returncode, seconds=time.monotonic()-start) - if result.returncode: - save() - raise RuntimeError(f"{label} failed: exit {result.returncode}") - lane.update(bytes=artifact.stat().st_size, sha256=sha256(artifact)) - plan = json.loads((a.output_dir / (label + ".plan.json")).read_text()) - if lane["bytes"] != plan["file_bytes"]: - raise RuntimeError("plan byte count differs from output") - finally: - save() - manifest["byte_identical"] = len({lane["sha256"] for lane in manifest["lanes"]}) == 1 - save() - if not manifest["byte_identical"]: - raise RuntimeError("reference/parallel/repeat whole-file hashes differ") - print("PASS: bounded pilot exact plan sizes and reference/parallel/repeat whole-file identity; quality remains unvalidated") - - -if __name__ == "__main__": - main() From 212b3a65867af769ad6b469f741c29564853ef48 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:47:19 +0200 Subject: [PATCH 085/123] test(ds4v): run the image unit tests in the main build, drop standalone probes The five CPU image tests were separate CMake projects that CI never built. They now live in server/test and register with the other unit tests. The PyTorch-comparison probes and fixture generators stay on the research branch. Co-Authored-By: Claude Fable 5.1 --- server/CMakeLists.txt | 17 +- .../Ds4vImageCodecs.NOTICES.md} | 0 server/cmake/Ds4vImageCodecs.cmake | 6 +- .../test_ds4v_image_assembly.cpp} | 0 .../test_ds4v_image_input.cpp} | 0 .../test_ds4v_image_integration.cpp} | 0 .../test_ds4v_image_policy.cpp} | 0 .../test_ds4v_image_prompt.cpp} | 0 server/tools/ds4_bf16_affine/CMakeLists.txt | 12 - server/tools/ds4_bf16_affine/probe.cpp | 63 --- .../tools/ds4v_image_assembly/CMakeLists.txt | 12 - server/tools/ds4v_image_input/CMakeLists.txt | 18 - server/tools/ds4v_image_input/README.md | 44 -- .../ds4v_image_integration/CMakeLists.txt | 11 - server/tools/ds4v_image_policy/CMakeLists.txt | 13 - server/tools/ds4v_image_policy/README.md | 68 --- server/tools/ds4v_image_policy/probe.cpp | 51 -- .../ds4v_image_policy/verify_fixtures.py | 123 ----- .../tools/ds4v_image_prepare/CMakeLists.txt | 47 -- server/tools/ds4v_image_prepare/README.md | 83 --- server/tools/ds4v_image_prepare/compose.cpp | 67 --- server/tools/ds4v_image_prepare/compose.h | 17 - server/tools/ds4v_image_prepare/test.cpp | 145 ----- server/tools/ds4v_image_prepare/verify.py | 36 -- server/tools/ds4v_image_prompt/CMakeLists.txt | 16 - server/tools/ds4v_image_prompt/README.md | 79 --- server/tools/ds4v_image_prompt/fixtures.cpp | 73 --- .../ds4v_image_prompt/verify_fixtures.py | 29 - .../ds4v_preprocess_probe/CMakeLists.txt | 25 - server/tools/ds4v_preprocess_probe/README.md | 30 - .../ds4v_preprocess_probe.cpp | 518 ------------------ .../generate_reference_fixtures.py | 204 ------- server/tools/ds4v_vision/CMakeLists.txt | 85 --- server/tools/ds4v_vision/README.md | 178 ------ .../tools/ds4v_vision/attention_contract.cpp | 87 --- .../tools/ds4v_vision/attention_dispatch.py | 18 - server/tools/ds4v_vision/attention_source.cpp | 165 ------ server/tools/ds4v_vision/compare.py | 45 -- server/tools/ds4v_vision/geometry.cpp | 114 ---- server/tools/ds4v_vision/linear_contract.cpp | 68 --- server/tools/ds4v_vision/linear_rounding.cpp | 142 ----- server/tools/ds4v_vision/linear_source.cpp | 103 ---- .../ds4v_vision/linear_unbiased_source.cpp | 151 ----- server/tools/ds4v_vision/loader_tests.py | 64 --- server/tools/ds4v_vision/norm_contract.cpp | 63 --- server/tools/ds4v_vision/norm_source.cpp | 117 ---- server/tools/ds4v_vision/probe.cpp | 123 ----- server/tools/ds4v_vision/reference_math.py | 63 --- server/tools/ds4v_vision/reference_stages.py | 127 ----- server/tools/ds4v_vision/rotary_contract.cpp | 33 -- server/tools/ds4v_vision/rotary_source.cpp | 69 --- 51 files changed, 18 insertions(+), 3604 deletions(-) rename server/{tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md => cmake/Ds4vImageCodecs.NOTICES.md} (100%) rename server/{tools/ds4v_image_assembly/test.cpp => test/test_ds4v_image_assembly.cpp} (100%) rename server/{tools/ds4v_image_input/test.cpp => test/test_ds4v_image_input.cpp} (100%) rename server/{tools/ds4v_image_integration/test.cpp => test/test_ds4v_image_integration.cpp} (100%) rename server/{tools/ds4v_image_policy/test.cpp => test/test_ds4v_image_policy.cpp} (100%) rename server/{tools/ds4v_image_prompt/test.cpp => test/test_ds4v_image_prompt.cpp} (100%) delete mode 100644 server/tools/ds4_bf16_affine/CMakeLists.txt delete mode 100644 server/tools/ds4_bf16_affine/probe.cpp delete mode 100644 server/tools/ds4v_image_assembly/CMakeLists.txt delete mode 100644 server/tools/ds4v_image_input/CMakeLists.txt delete mode 100644 server/tools/ds4v_image_input/README.md delete mode 100644 server/tools/ds4v_image_integration/CMakeLists.txt delete mode 100644 server/tools/ds4v_image_policy/CMakeLists.txt delete mode 100644 server/tools/ds4v_image_policy/README.md delete mode 100644 server/tools/ds4v_image_policy/probe.cpp delete mode 100644 server/tools/ds4v_image_policy/verify_fixtures.py delete mode 100644 server/tools/ds4v_image_prepare/CMakeLists.txt delete mode 100644 server/tools/ds4v_image_prepare/README.md delete mode 100644 server/tools/ds4v_image_prepare/compose.cpp delete mode 100644 server/tools/ds4v_image_prepare/compose.h delete mode 100644 server/tools/ds4v_image_prepare/test.cpp delete mode 100644 server/tools/ds4v_image_prepare/verify.py delete mode 100644 server/tools/ds4v_image_prompt/CMakeLists.txt delete mode 100644 server/tools/ds4v_image_prompt/README.md delete mode 100644 server/tools/ds4v_image_prompt/fixtures.cpp delete mode 100644 server/tools/ds4v_image_prompt/verify_fixtures.py delete mode 100644 server/tools/ds4v_preprocess_probe/CMakeLists.txt delete mode 100644 server/tools/ds4v_preprocess_probe/README.md delete mode 100644 server/tools/ds4v_preprocess_probe/ds4v_preprocess_probe.cpp delete mode 100644 server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py delete mode 100644 server/tools/ds4v_vision/CMakeLists.txt delete mode 100644 server/tools/ds4v_vision/README.md delete mode 100644 server/tools/ds4v_vision/attention_contract.cpp delete mode 100644 server/tools/ds4v_vision/attention_dispatch.py delete mode 100644 server/tools/ds4v_vision/attention_source.cpp delete mode 100644 server/tools/ds4v_vision/compare.py delete mode 100644 server/tools/ds4v_vision/geometry.cpp delete mode 100644 server/tools/ds4v_vision/linear_contract.cpp delete mode 100644 server/tools/ds4v_vision/linear_rounding.cpp delete mode 100644 server/tools/ds4v_vision/linear_source.cpp delete mode 100644 server/tools/ds4v_vision/linear_unbiased_source.cpp delete mode 100644 server/tools/ds4v_vision/loader_tests.py delete mode 100644 server/tools/ds4v_vision/norm_contract.cpp delete mode 100644 server/tools/ds4v_vision/norm_source.cpp delete mode 100644 server/tools/ds4v_vision/probe.cpp delete mode 100644 server/tools/ds4v_vision/reference_math.py delete mode 100644 server/tools/ds4v_vision/reference_stages.py delete mode 100644 server/tools/ds4v_vision/rotary_contract.cpp delete mode 100644 server/tools/ds4v_vision/rotary_source.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index ac8e79002..bea621dd6 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -900,7 +900,7 @@ endif() # Production uses the same pinned decoder sources and options as the accepted # preprocessing probe. The decoder itself has no codec feature macro. include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/Ds4vImageCodecs.cmake") -install(FILES tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md +install(FILES cmake/Ds4vImageCodecs.NOTICES.md DESTINATION share/licenses/ds4v RENAME THIRD_PARTY_NOTICES.md) install(FILES "${DS4V_JPEG_SOURCE_DIR}/LICENSE.md" @@ -1886,6 +1886,21 @@ if(DFLASH27B_TESTS) endif() # ─── Unit tests (no GPU, no model files) ──────────────────────────── + # DS4V image units: each test builds only the unit it covers. + foreach(_ds4v_unit assembly input integration policy prompt) + add_executable(test_ds4v_image_${_ds4v_unit} test/test_ds4v_image_${_ds4v_unit}.cpp) + target_include_directories(test_ds4v_image_${_ds4v_unit} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/deepseek4) + list(APPEND _raw_unit_test_targets test_ds4v_image_${_ds4v_unit}) + endforeach() + target_sources(test_ds4v_image_assembly PRIVATE src/deepseek4/deepseek4_image_assembly.cpp) + target_sources(test_ds4v_image_input PRIVATE src/server/image_input.cpp) + target_link_libraries(test_ds4v_image_input PRIVATE nlohmann_json::nlohmann_json) + target_sources(test_ds4v_image_policy PRIVATE src/deepseek4/deepseek4_image_policy.cpp) + target_sources(test_ds4v_image_prompt PRIVATE + src/deepseek4/deepseek4_image_prompt.cpp + src/deepseek4/deepseek4_vision_preprocess.cpp) + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_moe_source_page_range.cpp") add_executable(test_moe_source_page_range test/test_moe_source_page_range.cpp) list(APPEND _raw_unit_test_targets test_moe_source_page_range) diff --git a/server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md b/server/cmake/Ds4vImageCodecs.NOTICES.md similarity index 100% rename from server/tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md rename to server/cmake/Ds4vImageCodecs.NOTICES.md diff --git a/server/cmake/Ds4vImageCodecs.cmake b/server/cmake/Ds4vImageCodecs.cmake index 55065dd2d..40605fa67 100644 --- a/server/cmake/Ds4vImageCodecs.cmake +++ b/server/cmake/Ds4vImageCodecs.cmake @@ -1,7 +1,5 @@ -# Shared decoder dependencies used by production and the accepted preprocessing -# probe. Keep archive pins and codec options identical to the qualified build. -# License texts remain in tools/ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md -# and the unmodified upstream archives. +# Pinned JPEG and PNG decoders for DS4V image input. License texts are in +# Ds4vImageCodecs.NOTICES.md and the unmodified upstream archives. include_guard(GLOBAL) include(ExternalProject) diff --git a/server/tools/ds4v_image_assembly/test.cpp b/server/test/test_ds4v_image_assembly.cpp similarity index 100% rename from server/tools/ds4v_image_assembly/test.cpp rename to server/test/test_ds4v_image_assembly.cpp diff --git a/server/tools/ds4v_image_input/test.cpp b/server/test/test_ds4v_image_input.cpp similarity index 100% rename from server/tools/ds4v_image_input/test.cpp rename to server/test/test_ds4v_image_input.cpp diff --git a/server/tools/ds4v_image_integration/test.cpp b/server/test/test_ds4v_image_integration.cpp similarity index 100% rename from server/tools/ds4v_image_integration/test.cpp rename to server/test/test_ds4v_image_integration.cpp diff --git a/server/tools/ds4v_image_policy/test.cpp b/server/test/test_ds4v_image_policy.cpp similarity index 100% rename from server/tools/ds4v_image_policy/test.cpp rename to server/test/test_ds4v_image_policy.cpp diff --git a/server/tools/ds4v_image_prompt/test.cpp b/server/test/test_ds4v_image_prompt.cpp similarity index 100% rename from server/tools/ds4v_image_prompt/test.cpp rename to server/test/test_ds4v_image_prompt.cpp diff --git a/server/tools/ds4_bf16_affine/CMakeLists.txt b/server/tools/ds4_bf16_affine/CMakeLists.txt deleted file mode 100644 index 40b6f3c01..000000000 --- a/server/tools/ds4_bf16_affine/CMakeLists.txt +++ /dev/null @@ -1,12 +0,0 @@ -cmake_minimum_required(VERSION 3.21) -project(ds4_bf16_affine LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) -set(GGML_BUILD "" CACHE PATH "Immutable existing GGML build") -add_executable(ds4_bf16_affine probe.cpp) -target_include_directories(ds4_bf16_affine PRIVATE ../../src ../../deps/llama.cpp/ggml/include) -foreach(lib ggml/src/libggml-base.so.0 ggml/src/libggml-cpu.so.0 ggml/src/ggml-hip/libggml-hip.so.0) - if(NOT EXISTS "${GGML_BUILD}/${lib}") - message(FATAL_ERROR "Missing immutable library ${GGML_BUILD}/${lib}") - endif() - target_link_libraries(ds4_bf16_affine PRIVATE "${GGML_BUILD}/${lib}") -endforeach() diff --git a/server/tools/ds4_bf16_affine/probe.cpp b/server/tools/ds4_bf16_affine/probe.cpp deleted file mode 100644 index 9e906163f..000000000 --- a/server/tools/ds4_bf16_affine/probe.cpp +++ /dev/null @@ -1,63 +0,0 @@ -#include "deepseek4/deepseek4_norm.h" -#include "ggml-alloc.h" -#include "ggml-cpu.h" -#include "ggml-cuda.h" -#include -#include -#include -#include -#include -#include -#include -#include -static void check(bool ok,const char * msg) { if(!ok) throw std::runtime_error(msg); } -static float decode(uint16_t v) { uint32_t b=uint32_t(v)<<16;float f;std::memcpy(&f,&b,4);return f; } -int main(int argc,char ** argv) { - if(argc!=3) { std::cerr<<"usage: ds4_bf16_affine cpu|hip:0 contract|execute\n";return 2; } - const bool gpu=std::string(argv[1])=="hip:0",contract=std::string(argv[2])=="contract"; - if(!gpu && std::string(argv[1])!="cpu") return 2; - if(!contract && std::string(argv[2])!="execute") return 2; - ggml_backend_t backend=gpu?ggml_backend_cuda_init(0):ggml_backend_cpu_init(); - if(!backend) return 1; - if(!gpu) ggml_backend_cpu_set_n_threads(backend,2); - std::cout<<"backend="<src[1]==control && half_graph->src[1]==f16,"F32/F16 graph changed"); - const bool affine_f32=y->src[1]->type==GGML_TYPE_F32; - std::cout<<"n="<src[1])==4*k && y->src[1]->src[0]==w,"not vector-only cast"); - std::vector input(k*n),weight(k);std::vector raw(k),after(k); - for(int i=0;i a(k*n),b(k*n);ggml_backend_tensor_get(y,a.data(),0,a.size()*4);ggml_backend_tensor_get(z,b.data(),0,b.size()*4);ggml_backend_tensor_get(w,after.data(),0,after.size()*2); - check(raw==after,"BF16 payload mutated");double maxerr=0;size_t different=0; - for(int row=0;row` placeholder for each image. It returns owned -JPEG/PNG bytes separately. Defaults are16MiB per encoded image,32MiB aggregate, -and four images. These limits count image bytes after base64 decoding, before -pixel decoding. Output is cleared on failure and input JSON is unchanged. -Literal image placeholders in all message string values are rejected, including -tool-call fields and placeholders split across adjacent text parts. Message nesting -is limited to64levels by an iterative walk before copying JSON. Images in other message roles are rejected. -The standard `detail` values are accepted; source preprocessing uses its fixed -model recipe for all three values. - -Only base64 data URLs are supported in this first unit. Remote HTTP(S), filesystem -paths, other media types, and other APIs' image part schemas fail explicitly. -Text-only message arrays preserve their JSON representation. - -`redact_image_urls` iteratively replaces image_url fields before a caller serializes status -or diagnostic JSON. Request integration must invoke it before dumping messages -or raw bodies. This helper alone does not prove that a live server is redacted. - -The pure unit was tested on soulf: RED at0306a3f (`valid JPEG transport rejected`), -GREEN ate09f8da. Tests cover canonical JPEG/PNG bytes, malformed base64, per-image -and aggregate/count limits, exact limit boundaries, content order, input immutability, -placeholder injection, malformed descriptors, failure cleanup, and nested redaction. - -```sh -cmake -S server/tools/ds4v_image_input -B /tmp/ds4v-transport-build -DCMAKE_BUILD_TYPE=Release -cmake --build /tmp/ds4v-transport-build -j2 -ctest --test-dir /tmp/ds4v-transport-build --output-on-failure -``` - -The standalone CMake target uses the same pinned nlohmann/json commit as the server. -It needs no GPU SDK or GGML. All builds and tests ran on soulf in -`~/lucebox-ds4v-transport`; evidence is in `artifacts/transport/` there. diff --git a/server/tools/ds4v_image_integration/CMakeLists.txt b/server/tools/ds4v_image_integration/CMakeLists.txt deleted file mode 100644 index dc3b65f76..000000000 --- a/server/tools/ds4v_image_integration/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -cmake_minimum_required(VERSION 3.21) -project(ds4v_image_integration LANGUAGES CXX) - -add_executable(ds4v_image_integration_test test.cpp) -target_include_directories(ds4v_image_integration_test PRIVATE ../../src/deepseek4) -target_compile_features(ds4v_image_integration_test PRIVATE cxx_std_17) -if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") - target_compile_options(ds4v_image_integration_test PRIVATE -Wall -Wextra -Wpedantic -Werror) -endif() -enable_testing() -add_test(NAME ds4v_image_integration COMMAND ds4v_image_integration_test) diff --git a/server/tools/ds4v_image_policy/CMakeLists.txt b/server/tools/ds4v_image_policy/CMakeLists.txt deleted file mode 100644 index 0f2c4de08..000000000 --- a/server/tools/ds4v_image_policy/CMakeLists.txt +++ /dev/null @@ -1,13 +0,0 @@ -cmake_minimum_required(VERSION 3.21) -project(ds4v_image_policy LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -add_library(ds4v_image_policy STATIC ../../src/deepseek4/deepseek4_image_policy.cpp) -target_include_directories(ds4v_image_policy PUBLIC ../../src) -target_compile_options(ds4v_image_policy PRIVATE -Wall -Wextra -Werror) -add_executable(test_ds4v_image_policy test.cpp) -target_link_libraries(test_ds4v_image_policy PRIVATE ds4v_image_policy) -add_executable(ds4v_image_policy_probe probe.cpp) -target_link_libraries(ds4v_image_policy_probe PRIVATE ds4v_image_policy) -enable_testing() -add_test(NAME ds4v_image_policy COMMAND test_ds4v_image_policy) diff --git a/server/tools/ds4v_image_policy/README.md b/server/tools/ds4v_image_policy/README.md deleted file mode 100644 index f9c2ae807..000000000 --- a/server/tools/ds4v_image_policy/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# DS4V image selection and raw visibility policy - -This standalone C++17 unit is independent of the native tower. It adds no decoder, -loader, graph, image transport, or server wiring. - -`select_image_experts` consumes finite, nonnegative F32 unbiased scores from -sqrt(softplus(router logits)) and finite image biases. It ranks score+bias, then -normalizes the selected **unbiased** scores in F32 and applies route scale 1.5. -The result owns bounded arrays for at most 256 experts. Invalid counts, pointers, -nonfinite values, overflowed corrected scores/sums, and zero selected sums fail -with a cleared result and an error. Input pointers must address `experts` readable -floats and must not alias the output. No router matmul or token/hash dispatch is -performed here. Equal corrected scores select lower indices first; this is an -explicit deterministic rule, not a claim of equal-score ordering parity with -Torch or every GGML kernel. - -`raw_key_visible` tests one absolute query/key pair. An image range is -[IMAGE_START, IMAGE_END+1), excluding leading compression padding. Queries in that -range see the union of the complete range and ordinary causal sliding-window -positions. Queries outside it retain ordinary visibility. Pass (-1,-1) when no -range applies. Nonnegative query/key, positive window, and an absent or ordered -nonnegative range are required; invalid arguments return false with `visible` -cleared. The helper uses differences rather than query+1, including at INT64_MAX. -It owns no image block, allocates no mask, and changes no compressed-row policy. -The eventual request boundary must supply validated sequence/image spans; this -scalar predicate has no sequence length or source image-token budget to validate. - -## Verification on soulf - -Tests were committed first at e30c1b0. The remote Release build succeeded and -CTest failed with `valid selection rejected` before implementation. At 60ce979, -the same tests passed, covering unbiased weighting, selection bias, deterministic -ties, malformed contracts, causal/image boundaries, and near-INT64_MAX positions. -All build and fixture execution took place on soulf with at most two threads. - -From the isolated `~/lucebox-ds4v-policy` checkout: - -```sh -cmake -S server/tools/ds4v_image_policy -B /tmp/ds4v-image-policy-build -DCMAKE_BUILD_TYPE=Release -cmake --build /tmp/ds4v-image-policy-build -j2 -ctest --test-dir /tmp/ds4v-image-policy-build --output-on-failure -OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2 ~/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python server/tools/ds4v_image_policy/verify_fixtures.py /tmp/ds4v-image-policy-build/ds4v_image_policy_probe ~/lucebox-ds4v-mix-fix/artifacts/routing-mask-reference ~/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored artifacts/image-policy/fixtures -``` - -The fixture verifier checks every original fixture hash and the parent model.py -hash before use. It executes only the exact source `linear` function and the -score-producing AST statements from `Gate.forward`, using the saved original -F32 input/router fixtures. New scores and native outputs go to a separate -evidence directory; neither source fixtures nor the Python environment changes. -Production code does not depend on Python, Torch, NumPy, or GGML. - -All 120 selected image expert indices match exactly across layers 0, 2, 3, 42 -and all five image token kinds. Maximum absolute weight errors are 2.9802322e-08, -5.9604645e-08, 0, and 0 respectively, against the unchanged 1e-6 limit. First -three-layer fixtures demonstrate learned image selection differs from their -original text hash rows. This proves calling the image policy for all five kinds; -it does not prove integrated hash bypass. - -Raw visibility matches all 433,562 query/key booleans exactly: 72,361 for the -one-image fixture and 361,201 for two images. This includes leading padding, -text before/between/after images, future-image isolation, and an image span longer -than the sliding window. The native probe writes full masks only for qualification; -the production predicate remains scalar. - -Evidence: `~/lucebox-ds4v-policy/artifacts/image-policy/{red.log,green.log,fixtures.log,fixtures/verdict.json}`. -Verdict: **PASS for this unintegrated CPU policy unit.** GPU graph execution, -bias_vl loading, text routing preservation in the integrated backend, HTTP image -behavior, and full-model output remain outside this unit. diff --git a/server/tools/ds4v_image_policy/probe.cpp b/server/tools/ds4v_image_policy/probe.cpp deleted file mode 100644 index 6e7458f22..000000000 --- a/server/tools/ds4v_image_policy/probe.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "deepseek4/deepseek4_image_policy.h" -#include -#include -#include -#include - -using namespace dflash::vision; -template static std::vector read(const char * path,size_t count) { - std::ifstream file(path,std::ios::binary|std::ios::ate); - if (!file || file.tellg()!=std::streamoff(count*sizeof(T))) throw std::runtime_error("input byte size mismatch"); - std::vector data(count); - file.seekg(0); file.read(reinterpret_cast(data.data()),count*sizeof(T)); - if (!file) throw std::runtime_error("input read failed"); - return data; -} -template static void save(const std::string & path,const std::vector & values) { - std::ofstream file(path,std::ios::binary); - file.write(reinterpret_cast(values.data()),values.size()*sizeof(T)); - if (!file) throw std::runtime_error("output write failed"); -} -int main(int argc,char ** argv) { - try { - if (argc==8 && std::string(argv[1])=="route") { - size_t rows=std::stoul(argv[5]),experts=std::stoul(argv[6]),topk=std::stoul(argv[7]); - if (rows>1024 || experts>256 || topk>experts) throw std::runtime_error("probe dimensions out of range"); - auto scores=read(argv[2],rows*experts),bias=read(argv[3],experts); - std::vector indices; - std::vector weights; - for (size_t row=0;row4096) throw std::runtime_error("probe mask too large"); - auto ranges=read(argv[2],count*2); - std::vector mask(count*count); - for (size_t q=0;q= config['vocab_size']) - assert np.array_equal(ids[image_rows], np.arange(config['vocab_size'], config['vocab_size'] + 5)) - expected_ids, expected_weights = read(entry['indices']), read(entry['weights']) - assert np.array_equal(actual_ids[image_rows], expected_ids[image_rows]), ('indices', layer) - difference = float(np.max(np.abs(actual_weights[image_rows] - expected_weights[image_rows]))) - assert np.isfinite(actual_weights).all() and difference <= 1e-6, ('weights', layer, difference) - assert np.max(np.abs(actual_weights[image_rows].sum(1) - 1.5)) <= 1e-6 - assert np.array_equal(actual_ids[image_rows], np.repeat(actual_ids[image_rows[:1]], 5, axis=0)) - hash_difference = None - if 'hash_rows' in entry: - hash_rows = read(entry['hash_rows']) - assert np.array_equal(expected_ids[:3], hash_rows), ('original text hash fixture', layer) - hash_difference = bool(np.all(np.any(hash_rows != actual_ids[image_rows[0]], axis=1))) - assert hash_difference, ('fixture must distinguish learned image selection from text hash', layer) - report['routing'][layer] = {'all_five_image_kinds_exact_indices': True, - 'max_weight_absolute_error': difference, 'image_selection_differs_from_text_hash_rows': hash_difference, - 'scores_sha256': hashlib.sha256(scores_path.read_bytes()).hexdigest()} - print('routing', layer, 'PASS', difference, flush=True) - -for name, entry in manifest['masks'].items(): - ids = read(entry['ids']).reshape(-1) - count = ids.size - ranges = np.full((count, 2), -1, np.int64) - starts = np.flatnonzero(ids == config['vocab_size']) - ends = np.flatnonzero(ids == config['vocab_size'] + 4) - assert len(starts) == len(ends) - for begin, end in zip(starts, ends): - assert begin <= end and (ranges[begin:end+1] == -1).all() - ranges[begin:end+1] = [begin, end+1] - ranges_path = a.output / f'{name}-ranges.i64' - ranges.tofile(ranges_path) - output = a.output / f'{name}-mask.u8' - subprocess.run([str(a.probe), 'mask', str(ranges_path), str(count), str(config['window_size']), str(output)], check=True) - actual = np.fromfile(output, np.uint8).reshape(count, count) - expected = np.zeros((count, count), np.uint8) - raw = read(entry['raw_indices']).reshape(count, -1) - for query, keys in enumerate(raw): - valid = keys[keys >= 0] - assert (valid < count).all() - expected[query, valid] = 1 - assert np.array_equal(actual, expected), ('raw visibility mismatch', name, np.count_nonzero(actual != expected)) - report['masks'][name] = {'all_key_query_pairs_exact': True, 'comparisons': count*count, - 'longer_than_window': bool(np.any(ends-starts+1 > config['window_size'])), - 'native_mask_sha256': hashlib.sha256(output.read_bytes()).hexdigest()} - print('mask', name, 'PASS', count*count, flush=True) - -report['verdict'] = 'PASS' -(a.output / 'verdict.json').write_text(json.dumps(report, indent=2) + '\n') diff --git a/server/tools/ds4v_image_prepare/CMakeLists.txt b/server/tools/ds4v_image_prepare/CMakeLists.txt deleted file mode 100644 index bdae0c65a..000000000 --- a/server/tools/ds4v_image_prepare/CMakeLists.txt +++ /dev/null @@ -1,47 +0,0 @@ -cmake_minimum_required(VERSION 3.21) -project(ds4v_image_prepare LANGUAGES C CXX) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) -foreach(backend CPU CUDA HIP METAL VULKAN SYCL OPENCL CANN MUSA RPC BLAS WEBGPU HEXAGON ZENDNN ZDNN OPENVINO VIRTGPU VIRTGPU_BACKEND ACCELERATE OPENMP) - set(GGML_${backend} OFF CACHE BOOL "" FORCE) -endforeach() -set(GGML_BACKEND_DL OFF CACHE BOOL "" FORCE) -set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -add_subdirectory(../../deps/llama.cpp/ggml ggml EXCLUDE_FROM_ALL) - -find_package(nlohmann_json CONFIG QUIET) -if(NOT nlohmann_json_FOUND) - include(FetchContent) - FetchContent_Declare(json - URL https://codeload.github.com/nlohmann/json/tar.gz/9cca280a4d0ccf0c08f47a99aa71d1b0e52f8d03 - URL_HASH SHA256=0dbc5e40a01ff142e7e68c03e85247a4dcede2f592d12d3677dee3664d17975a - DOWNLOAD_EXTRACT_TIMESTAMP TRUE) - FetchContent_MakeAvailable(json) -endif() - -# Reuse the accepted target's pinned codec downloads and two-job build rule. -set(DS4V_PREPROCESS_WITH_CODECS ON CACHE BOOL "" FORCE) -add_subdirectory(../ds4v_preprocess_probe codecs EXCLUDE_FROM_ALL) -set(DS4V_JINJA ../../deps/llama.cpp/common/jinja) -add_executable(ds4v_image_prepare - test.cpp compose.cpp - ../../src/server/image_input.cpp - ../../src/server/chat_template.cpp - ../../src/server/tokenizer.cpp - ../../src/deepseek4/deepseek4_vision_decode.cpp - ../../src/deepseek4/deepseek4_vision_preprocess.cpp - ../../src/deepseek4/deepseek4_image_prompt.cpp - ${DS4V_JINJA}/lexer.cpp ${DS4V_JINJA}/parser.cpp - ${DS4V_JINJA}/runtime.cpp ${DS4V_JINJA}/value.cpp - ${DS4V_JINJA}/string.cpp ${DS4V_JINJA}/caps.cpp - ../../deps/llama.cpp/common/unicode.cpp) -target_include_directories(ds4v_image_prepare PRIVATE ../../src ../../deps/llama.cpp/common) -target_link_libraries(ds4v_image_prepare PRIVATE ggml-base nlohmann_json::nlohmann_json ds4v_libjpeg ds4v_lodepng) -enable_testing() -set(DS4V_TOKENIZER_GGUF "" CACHE FILEPATH "Converter smoke GGUF; metadata only") -set(DS4V_SOURCE_FIXTURES "" CACHE PATH "Immutable preprocessing fixtures") -if(DS4V_TOKENIZER_GGUF AND DS4V_SOURCE_FIXTURES) - add_test(NAME ds4v_image_prepare_composition COMMAND ds4v_image_prepare ${DS4V_TOKENIZER_GGUF} ${DS4V_SOURCE_FIXTURES}) -endif() diff --git a/server/tools/ds4v_image_prepare/README.md b/server/tools/ds4v_image_prepare/README.md deleted file mode 100644 index 3a3bb6ce0..000000000 --- a/server/tools/ds4v_image_prepare/README.md +++ /dev/null @@ -1,83 +0,0 @@ -# CPU image preparation composition probe - -This qualification-only probe composes accepted data-URL extraction, JPEG/PNG -decode, source RGB preprocessing, the existing built-in DeepSeek4 renderer and -native tokenizer, and owned prompt expansion. It also executes the existing -Jinja renderer for marker-preserving, dropped-marker, and repeated-marker -controls. The tokenizer loads metadata from the converter smoke GGUF; no model -weights, GGML backend, HTTP server, or vision tower are initialized. - -`compose.cpp` contains an explicit tiny adapter from normalized text parts to -ChatMessage. It supports only the simple roles/string/text-part fixtures here. -It is **not HttpServer::normalize_chat_messages**, and does not implement its -ToolMemory replay, Responses or Anthropic conversion, request copies, queueing, -compression, cache, snapshot, usage serialization, or generation behavior. -Production component implementations and APIs are unchanged. - -The adapter counts markers after real rendering/tokenization, including zero-image -requests, before decoding. It constructs its result transactionally. Transport, -decode, preprocess, and expansion failures discard accumulated records and return -bounded category/index messages. Decoded RGB is retained only for this probe's -byte comparisons; the production prompt payload still contains plan/patches/layout. - -## Tests and limits of proof - -- Real source carrots/corn JPEG data URLs in both orders, with text before, - between, and after them. Decoded RGB, BF16 patches, shapes, layout kinds, - permutation, generated IDs, and absolute spans match immutable source fixtures. - All ordinary rendered tokens remain unchanged around expanded blocks. -- Both orders expand to 436 tokens in the fixture's built-in DS4 prompt. The first - block starts at 9; the second starts at 119 for corn then carrots, and 323 for - carrots then corn. These positions arise from actual rendering/tokenization. -- Exact expanded context fit succeeds and one-token overflow fails, using the - image-specific preparation helper. Existing text compression admission is not - executed or qualified here. -- Jinja preserves an image marker successfully, while dropped/repeated markers - fail. A Jinja-injected marker and a tool-schema object key containing the marker - fail with zero extracted images. This tests the final token boundary that an - earlier string-value scan can miss. -- A valid first JPEG followed by a truncated second JPEG fails transactionally. - Redaction occurs before JSON serialization and removes all data URLs. -- Two separately encoded 19x11 solid PNG requests have identical expanded IDs - but different patch bytes and independent mutable storage. They are synthetic - test inputs generated in memory with the pinned LodePNG encoder, not saved - user artifacts. -- Ordinary text follows the real built-in renderer/tokenizer unchanged. - -The fixture wrapper verifies all 28 original carrots/corn fixture hashes before -execution. Logs contain counts, shapes, bounds and hashes, never encoded images -or full prompt data. No source fixture or Python environment is modified. - -The initial test/stub commit was 1096fab. Linking the existing Jinja engine also -requires its existing common/unicode.cpp; the harness wiring correction a5aa8a3 -then built and recorded the intended RED `text composition failed`. The minimal -adapter at 15a10c5 passed the unchanged interaction cases. Final checks also -assert/report source dimensions and explicitly disable additional GGML providers. - -## Reproduce on soulf only - -From isolated `~/lucebox-ds4v-cpu`: - -```sh -cmake -S server/tools/ds4v_image_prepare -B /tmp/ds4v-image-prepare-build -DCMAKE_BUILD_TYPE=Release -DDS4V_TOKENIZER_GGUF=$HOME/lucebox-ds4v-mix-fix/artifacts/fitter-fix/smoke.gguf -DDS4V_SOURCE_FIXTURES=$HOME/ds4v-work/ds4v-preprocess-fixtures-final -cmake --build /tmp/ds4v-image-prepare-build --target ds4v_image_prepare -j2 -python3 server/tools/ds4v_image_prepare/verify.py /tmp/ds4v-image-prepare-build/ds4v_image_prepare ~/lucebox-ds4v-mix-fix/artifacts/fitter-fix/smoke.gguf ~/ds4v-work/ds4v-preprocess-fixtures-final artifacts/cpu-composition/verdict.json -``` - -The wrapper is the authoritative hash-checking invocation. CTest is also -registered as `ds4v_image_prepare_composition` when both fixture paths are set. -Builds use two jobs; the native composition execution is single-threaded. Only -ggml-base/gguf is linked, with compute backends disabled. The accepted preprocessing -CMake target supplies the pinned libjpeg-turbo and LodePNG dependencies and its -two-job external build. JSON fallback uses the root server's pinned 9cca280a -archive with its SHA256. No server root build is configured. - -Retain [the existing preprocessing third-party notices](../ds4v_preprocess_probe/THIRD_PARTY_NOTICES.md) -for Pillow/libjpeg-turbo/LodePNG, and the vendored llama.cpp license. This target -reuses those implementations and pins rather than copying codec code. - -Evidence: `~/lucebox-ds4v-cpu/artifacts/cpu-composition` contains RED/GREEN logs, -final source commit, source/binary hashes, dependency/backend configuration, and -fixture verdict. PASS means **CPU preparation composition with the explicit probe -adapter**, not integrated HTTP image input, image routing/attention execution, -vision semantics, tower parity, or paired-GPU performance. diff --git a/server/tools/ds4v_image_prepare/compose.cpp b/server/tools/ds4v_image_prepare/compose.cpp deleted file mode 100644 index 064af1eb5..000000000 --- a/server/tools/ds4v_image_prepare/compose.cpp +++ /dev/null @@ -1,67 +0,0 @@ -#include "compose.h" -#include "server/chat_template.h" -#include -#include - -using namespace dflash::common; -using namespace dflash::vision; -namespace { -Composition fail(const std::string & error) { - Composition result; - result.error=error; - return result; -} -std::vector probe_text_adapter(const nlohmann::json & normalized) { - std::vector result; - for (const auto & message:normalized) { - ChatMessage item; - item.role=message.value("role",std::string("user")); - const auto & content=message.at("content"); - if (content.is_string()) item.content=content.get(); - else if (content.is_array()) { - for (const auto & part:content) { - const auto type=part.value("type",std::string()); - if (type!="text" && type!="input_text" && type!="output_text") - throw std::runtime_error("unsupported probe part"); - item.content+=part.at("text").get(); - } - } else throw std::runtime_error("unsupported probe content"); - result.push_back(std::move(item)); - } - return result; -} -} -Composition compose(const nlohmann::json & messages,Tokenizer & tokenizer, - const ImagePromptLimits & limits,const std::string & tools,const std::string & jinja) { - try { - nlohmann::json normalized; - std::vector encoded; - std::string error; - if (!extract_chat_images(messages,normalized,encoded,error)) return fail("transport: "+error); - const auto chat=probe_text_adapter(normalized); - const auto rendered=jinja.empty() - ? render_chat_template(chat,ChatFormat::DEEPSEEK4,true,false,tools) - : render_chat_template_jinja(jinja,chat,"","",true,false,tools); - auto tokens=tokenizer.encode(rendered); - if (std::count(tokens.begin(),tokens.end(),129264)!=static_cast(encoded.size())) - return fail("cardinality: final image marker count differs from image count"); - Composition result; - std::vector patches; - for (size_t i=0;i(tokenizer.vocab_size()),tokenizer.token_to_id(DS4_IMAGE_PLACEHOLDER)}); - if (!prepared) return fail("prompt: "+prepared.message); - result.prepared=std::move(prepared); - result.rendered_tokens=std::move(tokens); - return result; - } catch (const std::exception &) { - return fail("probe adapter or rendering failure"); - } -} diff --git a/server/tools/ds4v_image_prepare/compose.h b/server/tools/ds4v_image_prepare/compose.h deleted file mode 100644 index 5fbfa3d44..000000000 --- a/server/tools/ds4v_image_prepare/compose.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once -#include "deepseek4/deepseek4_image_prompt.h" -#include "deepseek4/deepseek4_vision_decode.h" -#include "server/image_input.h" -#include "server/tokenizer.h" - -// Qualification-only adapter. This is not HttpServer normalization or serving. -struct Composition { - std::string error; - dflash::vision::PreparedImagePrompt prepared; - std::vector decoded; - std::vector rendered_tokens; - explicit operator bool() const { return error.empty(); } -}; -Composition compose(const nlohmann::json & messages, dflash::common::Tokenizer & tokenizer, - const dflash::vision::ImagePromptLimits & limits = {}, - const std::string & tools = "", const std::string & jinja = ""); diff --git a/server/tools/ds4v_image_prepare/test.cpp b/server/tools/ds4v_image_prepare/test.cpp deleted file mode 100644 index 1c33a895b..000000000 --- a/server/tools/ds4v_image_prepare/test.cpp +++ /dev/null @@ -1,145 +0,0 @@ -#include "compose.h" -#include "server/chat_template.h" -#include "lodepng.h" -#include -#include -#include -#include -#include -#include - -using namespace dflash::common; -using namespace dflash::vision; -using json=nlohmann::json; -namespace fs=std::filesystem; -static void check(bool ok,const char * why) { if (!ok) throw std::runtime_error(why); } -template static std::vector read(const fs::path & path) { - std::ifstream file(path,std::ios::binary|std::ios::ate); - check(bool(file),"fixture open failed"); - auto bytes=file.tellg(); - check(bytes>=0 && bytes<32*1024*1024 && bytes%sizeof(T)==0,"fixture size invalid"); - std::vector value(static_cast(bytes)/sizeof(T)); - file.seekg(0); file.read(reinterpret_cast(value.data()),bytes); - check(bool(file),"fixture read failed"); return value; -} -static std::string data_url(const std::vector & bytes,const char * mime="image/jpeg") { - constexpr char chars[]="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - std::string result=std::string("data:")+mime+";base64,"; - for (size_t i=0;i>18)&63]; result+=chars[(word>>12)&63]; - result+=i+1>6)&63]:'='; - result+=i+2 & urls) { - json parts=json::array({{{"type","text"},{"text","Describe these: "}}}); - for (const auto & url:urls) { - parts.push_back({{"type","image_url"},{"image_url",{{"url",url}}}}); - parts.push_back({{"type","text"},{"text"," then "}}); - } - parts.push_back({{"type","text"},{"text","Explain the difference."}}); - return json::array({{{"role","system"},{"content","Be concise."}},{{"role","user"},{"content",parts}}}); -} -static void compare(const Composition & result,size_t index,const fs::path & root,const std::string & label) { - const auto & item=result.prepared.images[index]; - check(result.decoded[index].pixels==read(root/label/"input.rgb"),"decoded RGB differs from source"); - check(item.input.patches_bf16==read(root/label/"patches.bf16"),"BF16 patches differ from source"); - check(result.decoded[index].width==(label=="corn"?450U:1024U) && - result.decoded[index].height==(label=="corn"?308U:701U),"source decoded dimensions"); - check(item.input.plan.vit_rows==(label=="corn"?23U:42U) && - item.input.plan.vit_cols==(label=="corn"?34U:61U) && - item.input.plan.aligner_rows==(label=="corn"?8U:14U) && - item.input.plan.aligner_cols==(label=="corn"?12U:21U),"source patch/aligner dimensions"); - const auto start=item.layout.span.block_begin; - const int residue=static_cast(start%4); - const auto types=read(root/label/("types-"+std::to_string(residue)+".i64")); - check(item.layout.types.size()==types.size(),"type count mismatch"); - check(item.layout.permutation==read(root/label/("permutation-"+std::to_string(residue)+".i64")),"permutation mismatch"); - for (size_t i=0;i(item.layout.types[i])==types[i],"source type mismatch"); - check(result.prepared.tokens[start+i]==129280+types[i],"generated ID mismatch"); - } - const auto first=std::find(types.begin(),types.end(),int64_t(0)); - const auto last=std::find(types.begin(),types.end(),int64_t(4)); - check(first!=types.end() && last!=types.end(),"source sentinels absent"); - check(item.layout.span.visible_begin==start+static_cast(first-types.begin()) && - item.layout.span.visible_end==start+static_cast(last-types.begin())+1 && - item.layout.span.block_end==start+types.size(),"source span mismatch"); - std::cout< rgb(19*11*3,pixel); - unsigned char * bytes=nullptr; size_t size=0; - check(lodepng_encode24(&bytes,&size,rgb.data(),19,11)==0,"synthetic PNG encode failed"); - std::vector data(bytes,bytes+size); std::free(bytes); - return data_url(data,"image/png"); -} -int main(int argc,char ** argv) { - try { - check(argc==3,"usage: composition_probe tokenizer_gguf fixtures"); - Tokenizer tokenizer; check(tokenizer.load_from_gguf(argv[1]),"tokenizer load failed"); - check(tokenizer.vocab_size()==129280 && tokenizer.token_to_id(DS4_IMAGE_PLACEHOLDER)==129264,"tokenizer contract"); - const auto text_messages=json::array({{{"role","user"},{"content","Hello."}}}); - auto text=compose(text_messages,tokenizer); - check(bool(text),"text composition failed"); - const auto expected=tokenizer.encode(render_chat_template({{"user","Hello."}},ChatFormat::DEEPSEEK4)); - check(text.prepared.tokens==expected && text.prepared.images.empty(),"text tokens changed"); - const fs::path root=argv[2]; - const auto corn=data_url(read(root/"corn/encoded.bin")); - const auto carrots=data_url(read(root/"carrots/encoded.bin")); - for (bool reverse:{false,true}) { - const std::string first=reverse?"carrots":"corn",second=reverse?"corn":"carrots"; - const auto input=messages(reverse?std::vector{carrots,corn}:std::vector{corn,carrots}); - auto result=compose(input,tokenizer); - check(bool(result) && result.prepared.images.size()==2,"real image composition failed"); - compare(result,0,root,first); compare(result,1,root,second); - size_t position=0,index=0; - for (int32_t token:result.rendered_tokens) { - if (token==129264) { - check(result.prepared.images[index].layout.span.block_begin==position,"ordered expanded image position"); - position+=result.prepared.images[index++].layout.types.size(); - } else check(result.prepared.tokens[position++]==token,"surrounding text changed"); - } - check(position==result.prepared.tokens.size() && index==2,"expanded coverage"); - const uint64_t total=result.prepared.tokens.size(); - check(bool(compose(input,tokenizer,{total+1,1,total})),"exact fit rejected"); - failure(compose(input,tokenizer,{total,1,total}),"prompt"); - std::cout<(root/"corn/encoded.bin"); malformed.resize(3); - failure(compose(messages({corn,data_url(malformed)}),tokenizer),"decode image 1"); - json redacted=messages({corn,carrots}); redact_image_urls(redacted); - const auto logged=redacted.dump(); - check(logged.find("base64,")==std::string::npos && logged.find(corn.substr(0,80))==std::string::npos && - logged.find("[image omitted]")!=std::string::npos,"redaction before dump failed"); - auto black=compose(messages({solid_png(0)}),tokenizer); - auto white=compose(messages({solid_png(255)}),tokenizer); - check(bool(black) && bool(white) && black.prepared.tokens==white.prepared.tokens,"same-layout PNG tokens"); - check(black.prepared.images[0].input.patches_bf16!=white.prepared.images[0].input.patches_bf16,"different pixels collapsed"); - const auto white_first=white.prepared.images[0].input.patches_bf16[0]; - black.prepared.images[0].input.patches_bf16[0]=0; - check(white.prepared.images[0].input.patches_bf16[0]==white_first,"image storage shared"); - std::cout<<"PASS text preservation, Jinja/cardinality, zero-image tool key, malformed second image, exact context, redaction, distinct PNG storage\n"; - return 0; - } catch (const std::exception & e) { std::cerr<<"FAIL: "< -#include -#include -#include -#include - -using namespace dflash::vision; -namespace fs=std::filesystem; -static void check(bool value,const char * why) { if (!value) throw std::runtime_error(why); } -template static std::vector read(const fs::path & path) { - std::ifstream stream(path,std::ios::binary|std::ios::ate); - check(bool(stream),"fixture open failed"); - const auto size=stream.tellg(); - check(size>=0 && size<16*1024*1024 && size%sizeof(T)==0,"fixture byte size invalid"); - std::vector values(static_cast(size)/sizeof(T)); - stream.seekg(0); stream.read(reinterpret_cast(values.data()),size); - check(bool(stream),"fixture read failed"); - return values; -} -static ImagePatchInput image(const fs::path & root,const std::string & label) { - ImagePatchInput value; - value.plan=label=="corn" ? ResizePlan{476,322,23,34,8,12,false} : ResizePlan{854,588,42,61,14,21,false}; - value.patches_bf16=read(root/label/"patches.bf16"); - return value; -} -static void compare(const PreparedImagePrompt & result,size_t index,const ImagePatchInput & input, - const fs::path & root,const std::string & label,uint64_t start,int fixture_start) { - const auto types=read(root/label/("types-"+std::to_string(fixture_start)+".i64")); - const auto permutation=read(root/label/("permutation-"+std::to_string(fixture_start)+".i64")); - check(bool(result) && result.images.size()>index,"fixture preparation failed"); - const auto & item=result.images[index]; - check(item.input.patches_bf16==input.patches_bf16,"patch bytes changed"); - check(item.layout.types.size()==types.size() && item.layout.permutation==permutation,"source layout size/permutation mismatch"); - for (size_t i=0;i(item.layout.types[i])==types[i],"source layout kind mismatch"); - check(result.tokens[start+i]==129280+types[i],"source expanded token mismatch"); - } - const auto first=std::find(types.begin(),types.end(),int64_t(ImageTokenType::Start)); - const auto last=std::find(types.begin(),types.end(),int64_t(ImageTokenType::End)); - check(first!=types.end() && last!=types.end(),"bad source sentinel fixture"); - check(item.layout.span.block_begin==start && item.layout.span.block_end==start+types.size() && - item.layout.span.visible_begin==start+static_cast(first-types.begin()) && - item.layout.span.visible_end==start+static_cast(last-types.begin())+1,"source absolute spans mismatch"); -} -int main(int argc,char ** argv) { - try { - check(argc==2,"usage: fixture_probe fixture_directory"); - const fs::path root=argv[1]; - for (const std::string label:{"corn","carrots"}) { - const auto source=image(root,label); - for (int start:{0,1,2,3,127}) { - std::vector tokens(start,42); - tokens.push_back(129264); tokens.push_back(77); - auto result=prepare_image_prompt(tokens,{source}); - compare(result,0,source,root,label,start,start); - check(result.tokens.back()==77,"text suffix changed"); - for (int i=0;i(root/first/"types-0.i64"); - const auto a=image(root,first),b=image(root,second); - auto result=prepare_image_prompt({129264,129264},{a,b}); - compare(result,0,a,root,first,0,0); - compare(result,1,b,root,second,first_types.size(),static_cast(first_types.size()%4)); - std::cout< -#include -#include -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fs = std::filesystem; -using dflash::vision::DecodedRgbView; -using dflash::vision::ImageLayout; -using dflash::vision::ImageTokenType; -using dflash::vision::PreprocessConfig; -using dflash::vision::PreprocessError; -using dflash::vision::PreprocessLimits; -using dflash::vision::PreprocessResult; -using dflash::vision::ResizePlan; -#ifdef DS4V_PREPROCESS_WITH_CODECS -using dflash::vision::DecodeError; -using dflash::vision::DecodeLimits; -using dflash::vision::EncodedImageView; -#endif - -namespace { - -struct FixtureCase { - std::string label; - std::uint32_t input_width = 0; - std::uint32_t input_height = 0; - std::uint32_t resized_width = 0; - std::uint32_t resized_height = 0; - std::uint32_t vit_rows = 0; - std::uint32_t vit_cols = 0; - std::uint32_t aligner_rows = 0; - std::uint32_t aligner_cols = 0; -}; - -void require(bool condition, const std::string & message) { - if (!condition) { - throw std::runtime_error(message); - } -} - -std::vector split_tabs(const std::string & line) { - std::vector fields; - std::size_t begin = 0; - while (true) { - const std::size_t end = line.find('\t', begin); - fields.push_back(line.substr(begin, end == std::string::npos ? end : end - begin)); - if (end == std::string::npos) { - return fields; - } - begin = end + 1; - } -} - -std::uint32_t parse_u32(const std::string & value, const std::string & field) { - std::size_t used = 0; - const unsigned long parsed = std::stoul(value, &used); - if (used != value.size() || parsed > std::numeric_limits::max()) { - throw std::runtime_error("invalid " + field + ": " + value); - } - return static_cast(parsed); -} - -std::vector read_manifest(const fs::path & root) { - std::ifstream input(root / "manifest.tsv"); - require(input.good(), "cannot open fixture manifest: " + (root / "manifest.tsv").string()); - std::string line; - require(static_cast(std::getline(input, line)), "fixture manifest is empty"); - require(line == - "label\tinput_width\tinput_height\tresized_width\tresized_height\tvit_rows\tvit_cols\taligner_rows\taligner_cols", - "fixture manifest header mismatch"); - std::vector cases; - while (std::getline(input, line)) { - if (line.empty()) { - continue; - } - const auto fields = split_tabs(line); - require(fields.size() == 9, "fixture manifest row must have 9 fields"); - require(!fields[0].empty() && - std::all_of(fields[0].begin(), fields[0].end(), [](unsigned char value) { - return (value >= 'a' && value <= 'z') || value == '-'; - }), - "fixture label contains unsupported characters"); - FixtureCase item; - item.label = fields[0]; - item.input_width = parse_u32(fields[1], "input_width"); - item.input_height = parse_u32(fields[2], "input_height"); - item.resized_width = parse_u32(fields[3], "resized_width"); - item.resized_height = parse_u32(fields[4], "resized_height"); - item.vit_rows = parse_u32(fields[5], "vit_rows"); - item.vit_cols = parse_u32(fields[6], "vit_cols"); - item.aligner_rows = parse_u32(fields[7], "aligner_rows"); - item.aligner_cols = parse_u32(fields[8], "aligner_cols"); - cases.push_back(std::move(item)); - } - require(!cases.empty(), "fixture manifest has no cases"); - return cases; -} - -template -std::vector read_binary(const fs::path & path) { - static_assert(std::is_trivially_copyable_v); - std::ifstream input(path, std::ios::binary | std::ios::ate); - require(input.good(), "cannot open fixture: " + path.string()); - const std::streampos end = input.tellg(); - require(end >= 0, "cannot determine fixture size: " + path.string()); - const auto bytes = static_cast(end); - require(bytes % sizeof(T) == 0, "fixture byte size is invalid: " + path.string()); - std::vector result(static_cast(bytes / sizeof(T))); - input.seekg(0); - if (!result.empty()) { - input.read(reinterpret_cast(result.data()), static_cast(bytes)); - require(input.good(), "cannot read fixture: " + path.string()); - } - return result; -} - -template -void require_equal( - const std::vector & actual, - const std::vector & expected, - const std::string & label) { - if (actual.size() != expected.size()) { - std::ostringstream message; - message << label << " size mismatch: actual=" << actual.size() - << " expected=" << expected.size(); - throw std::runtime_error(message.str()); - } - const auto mismatch = std::mismatch(actual.begin(), actual.end(), expected.begin()); - if (mismatch.first != actual.end()) { - const std::size_t offset = static_cast(mismatch.first - actual.begin()); - std::ostringstream message; - message << label << " mismatch at element " << offset - << ": actual=" << static_cast(*mismatch.first) - << " expected=" << static_cast(*mismatch.second); - throw std::runtime_error(message.str()); - } -} - -std::vector layout_types(const ImageLayout & layout) { - std::vector result; - result.reserve(layout.types.size()); - for (const auto type : layout.types) { - result.push_back(static_cast(type)); - } - return result; -} - -void require_plan(const ResizePlan & actual, const FixtureCase & expected) { - std::ostringstream details; - details << "actual resized=" << actual.resized_width << 'x' << actual.resized_height - << " vit=" << actual.vit_rows << 'x' << actual.vit_cols - << " aligner=" << actual.aligner_rows << 'x' << actual.aligner_cols; - require(actual.resized_width == expected.resized_width && - actual.resized_height == expected.resized_height && - actual.vit_rows == expected.vit_rows && actual.vit_cols == expected.vit_cols && - actual.aligner_rows == expected.aligner_rows && - actual.aligner_cols == expected.aligner_cols, - expected.label + " plan mismatch: " + details.str()); -} - -void require_same_result(const PreprocessResult & first, const PreprocessResult & second, - const std::string & label) { - require(static_cast(first) && static_cast(second), - label + " deterministic run failed"); - require(first.image.plan.resized_width == second.image.plan.resized_width && - first.image.plan.resized_height == second.image.plan.resized_height && - first.image.plan.vit_rows == second.image.plan.vit_rows && - first.image.plan.vit_cols == second.image.plan.vit_cols && - first.image.plan.aligner_rows == second.image.plan.aligner_rows && - first.image.plan.aligner_cols == second.image.plan.aligner_cols && - first.image.plan.direct_resize == second.image.plan.direct_resize, - label + " plan is not deterministic"); - require_equal(first.image.resized_rgb, second.image.resized_rgb, - label + " deterministic resized RGB"); - require_equal(first.image.patches_bf16, second.image.patches_bf16, - label + " deterministic patches"); - require_equal(layout_types(first.image.layout), layout_types(second.image.layout), - label + " deterministic layout types"); - require_equal(first.image.layout.permutation, second.image.layout.permutation, - label + " deterministic permutation"); - require(first.image.layout.span.block_begin == second.image.layout.span.block_begin && - first.image.layout.span.visible_begin == second.image.layout.span.visible_begin && - first.image.layout.span.visible_end == second.image.layout.span.visible_end && - first.image.layout.span.block_end == second.image.layout.span.block_end, - label + " span is not deterministic"); -} - -void verify_fixture(const fs::path & root, const FixtureCase & item) { - const fs::path case_dir = root / item.label; - const auto input = read_binary(case_dir / "input.rgb"); -#ifdef DS4V_PREPROCESS_WITH_CODECS - const auto encoded = read_binary(case_dir / "encoded.bin"); - const auto decoded = dflash::vision::decode_image({encoded.data(), encoded.size()}); - require(static_cast(decoded), - item.label + " decode failed: " + - dflash::vision::decode_error_name(decoded.status.code) + ": " + - decoded.status.message); - require(decoded.image.width == item.input_width && decoded.image.height == item.input_height, - item.label + " decoded dimensions mismatch"); - require_equal(decoded.image.pixels, input, item.label + " decoded RGB"); - const auto decoded_again = dflash::vision::decode_image({encoded.data(), encoded.size()}); - require(static_cast(decoded_again), item.label + " repeated decode failed"); - require_equal(decoded_again.image.pixels, decoded.image.pixels, - item.label + " deterministic decode"); - - DecodeLimits one_pixel; - one_pixel.decoded.max_decoded_pixels = 1; - const auto bounded = dflash::vision::decode_image( - {encoded.data(), encoded.size()}, one_pixel); - require(bounded.status.code == DecodeError::DecodedTooLarge && bounded.image.pixels.empty(), - item.label + " decoded pixel cap did not fail before output allocation"); -#endif - const DecodedRgbView view{item.input_width, item.input_height, input.data(), input.size()}; - const PreprocessResult result = dflash::vision::preprocess_rgb(view, 0); - require(static_cast(result), - item.label + " preprocess failed: " + - dflash::vision::preprocess_error_name(result.status.code) + ": " + - result.status.message); - require_plan(result.image.plan, item); - require_equal(result.image.resized_rgb, - read_binary(case_dir / "resized.rgb"), - item.label + " resized RGB"); - require_equal(result.image.patches_bf16, - read_binary(case_dir / "patches.bf16"), - item.label + " BF16 patches"); - - constexpr std::array starts = {0, 1, 2, 3, 127}; - for (const std::uint64_t start : starts) { - ImageLayout layout; - const auto status = dflash::vision::build_image_layout( - item.aligner_rows, item.aligner_cols, start, layout); - require(static_cast(status), - item.label + " layout failed at start " + std::to_string(start) + ": " + - status.message); - const std::string suffix = std::to_string(start) + ".i64"; - require_equal(layout_types(layout), - read_binary(case_dir / ("types-" + suffix)), - item.label + " types start=" + std::to_string(start)); - require_equal(layout.permutation, - read_binary(case_dir / ("permutation-" + suffix)), - item.label + " permutation start=" + std::to_string(start)); - const std::uint64_t leading = 3 - start % 4; - const std::uint64_t end = start + layout.types.size(); - require(layout.span.block_begin == start && layout.span.visible_begin == start + leading && - layout.span.visible_end == end && layout.span.block_end == end, - item.label + " span mismatch at start " + std::to_string(start)); - } - - const PreprocessResult repeated = dflash::vision::preprocess_rgb(view, 0); - require_same_result(result, repeated, item.label); - std::cout << item.label << " PASS resized=" << item.resized_width << 'x' - << item.resized_height << " vit=" << item.vit_rows << 'x' << item.vit_cols - << " aligner=" << item.aligner_rows << 'x' << item.aligner_cols - << " patch_words=" << result.image.patches_bf16.size() << '\n'; -} - -void expect_error(PreprocessError expected, PreprocessError actual, const std::string & label) { - require(actual == expected, - label + " returned " + dflash::vision::preprocess_error_name(actual) + - ", expected " + dflash::vision::preprocess_error_name(expected)); -} - -#ifdef DS4V_PREPROCESS_WITH_CODECS -void append_u32(std::vector & output, std::uint32_t value) { - output.push_back(static_cast(value >> 24)); - output.push_back(static_cast(value >> 16)); - output.push_back(static_cast(value >> 8)); - output.push_back(static_cast(value)); -} - -void append_png_chunk( - std::vector & output, - const std::array & type, - const std::vector & data) { - append_u32(output, static_cast(data.size())); - const std::size_t crc_begin = output.size(); - output.insert(output.end(), type.begin(), type.end()); - output.insert(output.end(), data.begin(), data.end()); - append_u32(output, lodepng_crc32(output.data() + crc_begin, 4 + data.size())); -} - -std::vector excessive_idat_png() { - std::vector inflated(4096, 0); - unsigned char * compressed = nullptr; - std::size_t compressed_size = 0; - const unsigned error = lodepng_zlib_compress( - &compressed, - &compressed_size, - inflated.data(), - inflated.size(), - &lodepng_default_compress_settings); - require(error == 0, "cannot create excessive-IDAT regression PNG"); - - std::vector result = {137, 80, 78, 71, 13, 10, 26, 10}; - const std::vector ihdr = { - 0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0, - }; - append_png_chunk(result, {'I', 'H', 'D', 'R'}, ihdr); - const std::vector compressed_bytes( - compressed, compressed + compressed_size); - append_png_chunk(result, {'I', 'D', 'A', 'T'}, compressed_bytes); - append_png_chunk(result, {'I', 'E', 'N', 'D'}, {}); - std::free(compressed); - return result; -} - -std::vector grey16_png() { - constexpr std::array values = { - 0, 1, 254, 255, 256, 257, 1024, 65535, - }; - std::vector pixels; - pixels.reserve(values.size() * 2 * 2); - for (int row = 0; row < 2; ++row) { - for (const std::uint16_t value : values) { - pixels.push_back(static_cast(value >> 8)); - pixels.push_back(static_cast(value)); - } - } - unsigned char * encoded = nullptr; - std::size_t encoded_size = 0; - const unsigned error = lodepng_encode_memory( - &encoded, &encoded_size, pixels.data(), 8, 2, LCT_GREY, 16); - require(error == 0, "cannot create GREY16 regression PNG"); - std::vector result(encoded, encoded + encoded_size); - std::free(encoded); - return result; -} - -std::vector cmyk_jpeg() { - jpeg_compress_struct encoder{}; - jpeg_error_mgr error{}; - encoder.err = jpeg_std_error(&error); - jpeg_create_compress(&encoder); - unsigned char * encoded = nullptr; - unsigned long encoded_size = 0; - jpeg_mem_dest(&encoder, &encoded, &encoded_size); - encoder.image_width = 2; - encoder.image_height = 1; - encoder.input_components = 4; - encoder.in_color_space = JCS_CMYK; - jpeg_set_defaults(&encoder); - jpeg_start_compress(&encoder, TRUE); - std::array pixels = {0, 64, 128, 16, 255, 192, 128, 32}; - JSAMPROW row = pixels.data(); - require(jpeg_write_scanlines(&encoder, &row, 1) == 1, - "cannot create CMYK regression JPEG"); - jpeg_finish_compress(&encoder); - std::vector result(encoded, encoded + encoded_size); - jpeg_destroy_compress(&encoder); - std::free(encoded); - return result; -} - -void decoder_regression_tests() { - std::vector failures; - const auto excessive = excessive_idat_png(); - const auto excessive_result = - dflash::vision::decode_image({excessive.data(), excessive.size()}); - if (excessive_result.status.code != DecodeError::MalformedImage || - excessive_result.status.message.find("IDAT exceeds decoded geometry bound") == - std::string::npos) { - failures.emplace_back("excess IDAT did not report the bounded-inflate outcome"); - } - - const auto grey = grey16_png(); - const auto grey_result = dflash::vision::decode_image({grey.data(), grey.size()}); - constexpr std::array expected_values = { - 0, 1, 254, 255, 255, 255, 255, 255, - }; - std::vector expected_rgb; - expected_rgb.reserve(expected_values.size() * 2 * 3); - for (int row = 0; row < 2; ++row) { - for (const std::uint8_t value : expected_values) { - expected_rgb.insert(expected_rgb.end(), 3, value); - } - } - if (!grey_result || grey_result.image.pixels != expected_rgb) { - failures.emplace_back("GREY16 did not match Pillow I;16 to RGB clamping"); - } - - const auto cmyk = cmyk_jpeg(); - const auto cmyk_result = dflash::vision::decode_image({cmyk.data(), cmyk.size()}); - if (cmyk_result.status.code != DecodeError::UnsupportedFormat) { - failures.emplace_back("CMYK JPEG was not classified as unsupported_format"); - } - - if (!failures.empty()) { - std::ostringstream message; - message << "decoder regression failures:"; - for (const auto & failure : failures) { - message << "\n- " << failure; - } - throw std::runtime_error(message.str()); - } -} -#endif - -void self_test() { - PreprocessConfig bad_config; - bad_config.patch_size = 16; - expect_error(PreprocessError::InvalidConfig, - dflash::vision::validate_config(bad_config).code, - "changed fixed config"); - - expect_error(PreprocessError::InvalidDimensions, - dflash::vision::validate_decoded_dimensions(0, 1).code, - "zero width"); - expect_error(PreprocessError::InputTooLarge, - dflash::vision::validate_decoded_dimensions(65'536, 1).code, - "axis cap"); - expect_error(PreprocessError::InputTooLarge, - dflash::vision::validate_decoded_dimensions(8192, 8193).code, - "pixel cap"); - - const std::array pixel = {0, 127, 255}; - DecodedRgbView wrong_size{1, 1, pixel.data(), 2}; - expect_error(PreprocessError::InputSizeMismatch, - dflash::vision::preprocess_rgb(wrong_size, 0).status.code, - "wrong RGB byte count"); - - PreprocessLimits tiny_output_limit; - tiny_output_limit.max_output_pixels = 1; - ResizePlan plan; - expect_error(PreprocessError::OutputTooLarge, - dflash::vision::plan_image(1, 1, plan, {}, tiny_output_limit).code, - "output cap"); - - ImageLayout layout; - expect_error(PreprocessError::TokenBudgetExceeded, - dflash::vision::build_image_layout(100, 100, 0, layout).code, - "layout budget"); - expect_error(PreprocessError::PositionOverflow, - dflash::vision::build_image_layout( - 2, 3, std::numeric_limits::max() - 5, layout).code, - "absolute span overflow"); - - const auto layout_status = dflash::vision::build_image_layout(2, 3, 0, layout); - require(static_cast(layout_status), "known layout failed"); - const std::vector expected_types = { - 1, 1, 1, 0, 2, 2, 2, 2, 2, 2, 3, 3, 4, - }; - const std::vector expected_permutation = {0, 3, 1, 4, 2, 5}; - require_equal(layout_types(layout), expected_types, "known layout types"); - require_equal(layout.permutation, expected_permutation, "known layout permutation"); - require(layout.span.block_begin == 0 && layout.span.visible_begin == 3 && - layout.span.visible_end == 13 && layout.span.block_end == 13, - "known layout span mismatch"); -#ifdef DS4V_PREPROCESS_WITH_CODECS - const std::array unsupported = {'G', 'I', 'F', '8', '9', 'a'}; - require(dflash::vision::decode_image({nullptr, 0}).status.code == DecodeError::EmptyInput, - "empty encoded image was accepted"); - require(dflash::vision::decode_image({unsupported.data(), unsupported.size()}).status.code == - DecodeError::UnsupportedFormat, - "unsupported encoded format was accepted"); - const std::array truncated_jpeg = {0xFF, 0xD8, 0xFF, 0xD9}; - require(dflash::vision::decode_image( - {truncated_jpeg.data(), truncated_jpeg.size()}).status.code == - DecodeError::MalformedImage, - "truncated JPEG was accepted"); - const std::array truncated_png = {137, 80, 78, 71, 13, 10, 26, 10}; - require(dflash::vision::decode_image( - {truncated_png.data(), truncated_png.size()}).status.code == - DecodeError::MalformedImage, - "truncated PNG was accepted"); - const std::uint8_t byte = 0; - DecodeLimits encoded_limit; - require(dflash::vision::decode_image( - {&byte, encoded_limit.max_encoded_bytes + 1}, encoded_limit).status.code == - DecodeError::EncodedTooLarge, - "oversized encoded input was inspected"); - decoder_regression_tests(); -#endif - std::cout << "self-test PASS\n"; -} - -void usage(const char * program) { - std::cerr << "Usage: " << program << " --self-test | --fixtures DIRECTORY\n"; -} - -} // namespace - -int main(int argc, char ** argv) { - try { - if (argc == 2 && std::string(argv[1]) == "--self-test") { - self_test(); - return 0; - } - if (argc == 3 && std::string(argv[1]) == "--fixtures") { - const fs::path root = argv[2]; - for (const auto & item : read_manifest(root)) { - verify_fixture(root, item); - } - return 0; - } - usage(argv[0]); - return 2; - } catch (const std::exception & error) { - std::cerr << "FAIL: " << error.what() << '\n'; - return 1; - } -} diff --git a/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py b/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py deleted file mode 100644 index 1de0161aa..000000000 --- a/server/tools/ds4v_preprocess_probe/generate_reference_fixtures.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python3 -"""Generate decoded-RGB preprocessing fixtures with the original Python source.""" - -import argparse -import hashlib -import io -import json -import math -from pathlib import Path -import shutil -import sys -from types import SimpleNamespace - -import numpy as np -import torch -from PIL import Image, ImageOps - - -START_POSITIONS = (0, 1, 2, 3, 127) - - -def pattern(width: int, height: int, seed: int) -> bytes: - y, x = np.indices((height, width), dtype=np.uint32) - channels = [] - for channel in range(3): - values = (x * 17 + y * 31 + (x * y) % 251 + channel * 73 + seed) % 256 - channels.append(values.astype(np.uint8)) - return np.stack(channels, axis=2).tobytes() - - -def source_resize(image, model_args, safe_resize): - patch = model_args.vision_patch_size - width, height = image.size - if model_args.vision_max_wh_ratio is not None and width > height * model_args.vision_max_wh_ratio: - width = height * model_args.vision_max_wh_ratio - if 0 < width * height < model_args.vision_min_pixels: - ratio = (model_args.vision_min_pixels / (width * height)) ** 0.5 - width = int(width * ratio) - height = int(height * ratio) - best_width = math.ceil(width / patch) * patch - best_height = math.ceil(height / patch) * patch - llm_h, llm_w, best_height, best_width = safe_resize( - height, - width, - best_height, - best_width, - patch, - model_args.vision_downsample_ratio, - model_args.vision_max_n_token, - ) - if image.width >= model_args.vision_max_wh_ratio * image.height: - resized = image.resize((best_width, best_height)) - else: - resized = ImageOps.pad(image, (best_width, best_height), color=(127, 127, 127)) - return resized, best_height // patch, best_width // patch, llm_h, llm_w - - -def save_bytes(path: Path, data: bytes, digests: dict[str, str], root: Path): - path.write_bytes(data) - digests[str(path.relative_to(root))] = hashlib.sha256(data).hexdigest() - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--source", required=True, type=Path, help="parent model directory") - parser.add_argument("--output", required=True, type=Path) - args = parser.parse_args() - - sys.path.insert(0, str(args.source / "inference")) - from image_processor import build_image_block, load_image, safe_resize - - config = json.loads((args.source / "config.json").read_text()) - config["dim"] = config["hidden_size"] - model_args = SimpleNamespace(**config) - torch.set_num_threads(2) - - if args.output.exists(): - shutil.rmtree(args.output) - args.output.mkdir(parents=True) - - cases: list[tuple[str, Image.Image, bytes]] = [] - synthetic = ( - ("tiny", 3, 5), - ("odd-padding", 37, 23), - ("portrait", 41, 113), - ("wide-direct", 257, 31), - ("very-tall", 10, 20000), - ("max-budget", 2048, 354), - ) - for seed, (label, width, height) in enumerate(synthetic, start=1): - raw = pattern(width, height, seed * 19) - image = Image.frombytes("RGB", (width, height), raw) - encoded = io.BytesIO() - image.save(encoded, format="PNG") - cases.append((label, image, encoded.getvalue())) - - rgba_rgb = np.frombuffer(pattern(29, 17, 211), dtype=np.uint8).reshape(17, 29, 3) - alpha = ((np.indices((17, 29), dtype=np.uint16).sum(axis=0) * 23) % 256).astype(np.uint8) - rgba = Image.fromarray(np.dstack((rgba_rgb, alpha))) - encoded_rgba = io.BytesIO() - rgba.save(encoded_rgba, format="PNG") - with Image.open(io.BytesIO(encoded_rgba.getvalue())) as opened: - expected_rgba_rgb = opened.convert("RGB") - cases.append(("png-rgba", expected_rgba_rgb, encoded_rgba.getvalue())) - - jpeg_source = Image.frombytes("RGB", (19, 11), pattern(19, 11, 233)) - exif = Image.Exif() - exif[274] = 6 - encoded_jpeg = io.BytesIO() - jpeg_source.save(encoded_jpeg, format="JPEG", quality=91, exif=exif) - with Image.open(io.BytesIO(encoded_jpeg.getvalue())) as opened: - expected_jpeg_rgb = opened.convert("RGB") - assert expected_jpeg_rgb.size == (19, 11), "source unexpectedly transposed EXIF orientation" - cases.append(("jpeg-exif", expected_jpeg_rgb, encoded_jpeg.getvalue())) - - for label in ("carrots", "corn"): - encoded = (args.source / "inference/examples/images" / f"{label}.jpeg").read_bytes() - with Image.open(io.BytesIO(encoded)) as opened: - image = opened.convert("RGB") - cases.append((label, image, encoded)) - - manifest_lines = [ - "label\tinput_width\tinput_height\tresized_width\tresized_height\tvit_rows\tvit_cols\taligner_rows\taligner_cols" - ] - digests: dict[str, str] = {} - for label, image, encoded in cases: - case_dir = args.output / label - case_dir.mkdir() - resized, vit_rows, vit_cols, aligner_rows, aligner_cols = source_resize( - image, model_args, safe_resize - ) - patches, source_vit_rows, source_vit_cols, source_aligner_rows, source_aligner_cols = load_image( - {"data": encoded}, model_args - ) - assert (vit_rows, vit_cols, aligner_rows, aligner_cols) == ( - source_vit_rows, - source_vit_cols, - source_aligner_rows, - source_aligner_cols, - ) - - # This checks that the separately materialized resized RGB image produces - # the same BF16 tensor as the original load_image implementation. - rebuilt = torch.from_numpy(np.asarray(resized, dtype=np.float32)).permute(2, 0, 1) / 255 - rebuilt = ((rebuilt - 0.5) / 0.5).to(torch.bfloat16) - rebuilt = ( - rebuilt.reshape(3, vit_rows, model_args.vision_patch_size, vit_cols, model_args.vision_patch_size) - .permute(1, 3, 0, 2, 4) - .reshape(vit_rows * vit_cols, 3, model_args.vision_patch_size, model_args.vision_patch_size) - ) - assert torch.equal(rebuilt, patches) - - save_bytes(case_dir / "input.rgb", image.tobytes(), digests, args.output) - save_bytes(case_dir / "encoded.bin", encoded, digests, args.output) - save_bytes(case_dir / "resized.rgb", resized.tobytes(), digests, args.output) - save_bytes( - case_dir / "patches.bf16", - patches.contiguous().view(torch.uint16).cpu().numpy().tobytes(), - digests, - args.output, - ) - for start in START_POSITIONS: - types, permutation = build_image_block(aligner_rows, aligner_cols, start) - save_bytes( - case_dir / f"types-{start}.i64", - types.contiguous().cpu().numpy().tobytes(), - digests, - args.output, - ) - save_bytes( - case_dir / f"permutation-{start}.i64", - permutation.contiguous().cpu().numpy().tobytes(), - digests, - args.output, - ) - manifest_lines.append( - "\t".join( - str(value) - for value in ( - label, - image.width, - image.height, - resized.width, - resized.height, - vit_rows, - vit_cols, - aligner_rows, - aligner_cols, - ) - ) - ) - print( - f"{label}: decoded={image.width}x{image.height} resized={resized.width}x{resized.height} " - f"vit={vit_rows}x{vit_cols} aligner={aligner_rows}x{aligner_cols}", - flush=True, - ) - - (args.output / "manifest.tsv").write_text("\n".join(manifest_lines) + "\n") - (args.output / "sha256.json").write_text(json.dumps(digests, indent=2, sort_keys=True) + "\n") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/server/tools/ds4v_vision/CMakeLists.txt b/server/tools/ds4v_vision/CMakeLists.txt deleted file mode 100644 index 168261cb5..000000000 --- a/server/tools/ds4v_vision/CMakeLists.txt +++ /dev/null @@ -1,85 +0,0 @@ -cmake_minimum_required(VERSION 3.21) -project(ds4v_vision LANGUAGES C CXX) -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -option(DS4V_VISION_HIP "Build the optional HIP qualification probe" OFF) -set(DS4V_VISION_PREBUILT_GGML "" CACHE PATH "Reuse an immutable GGML build for isolated runtime qualification") -set(GGML_CUDA OFF CACHE BOOL "" FORCE) -set(GGML_HIP ${DS4V_VISION_HIP} CACHE BOOL "" FORCE) -set(GGML_METAL OFF CACHE BOOL "" FORCE) -set(GGML_VULKAN OFF CACHE BOOL "" FORCE) -set(GGML_BLAS OFF CACHE BOOL "" FORCE) -set(GGML_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(GGML_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) -if(DS4V_VISION_PREBUILT_GGML) - add_library(ggml INTERFACE) - target_include_directories(ggml INTERFACE ../../deps/llama.cpp/ggml/include) - foreach(component base cpu) - set(library "${DS4V_VISION_PREBUILT_GGML}/ggml/src/libggml-${component}.so.0") - if(NOT EXISTS "${library}") - message(FATAL_ERROR "Missing prebuilt library: ${library}") - endif() - target_link_libraries(ggml INTERFACE "${library}") - endforeach() - if(DS4V_VISION_HIP) - set(library "${DS4V_VISION_PREBUILT_GGML}/ggml/src/ggml-hip/libggml-hip.so.0") - if(NOT EXISTS "${library}") - message(FATAL_ERROR "Missing prebuilt HIP library: ${library}") - endif() - target_link_libraries(ggml INTERFACE "${library}") - endif() -else() - add_subdirectory(../../deps/llama.cpp/ggml ggml) -endif() -if(DS4V_VISION_HIP AND NOT DS4V_VISION_PREBUILT_GGML) - target_compile_definitions(ggml-hip PRIVATE - cublasSgemmStridedBatched=hipblasSgemmStridedBatched - cudaStreamCaptureStatus=hipStreamCaptureStatus - cudaStreamCaptureStatusNone=hipStreamCaptureStatusNone - cudaStreamIsCapturing=hipStreamIsCapturing) - target_include_directories(ggml-hip BEFORE PRIVATE ../../src/hip_compat) - get_filename_component(DS4V_HIP_RUNTIME_DIR "${hip_DIR}/../.." ABSOLUTE) - target_link_options(ggml-hip PRIVATE "-L${DS4V_HIP_RUNTIME_DIR}") -endif() -add_library(ds4v_vision STATIC ../../src/deepseek4/deepseek4_vision.cpp) -target_include_directories(ds4v_vision PUBLIC ../../src) -target_link_libraries(ds4v_vision PUBLIC ggml) -add_executable(ds4v_vision_probe probe.cpp) -target_link_libraries(ds4v_vision_probe PRIVATE ds4v_vision) -add_executable(ds4v_linear_source linear_source.cpp) -target_link_libraries(ds4v_linear_source PRIVATE ds4v_vision) -add_executable(ds4v_linear_unbiased_source linear_unbiased_source.cpp) -target_link_libraries(ds4v_linear_unbiased_source PRIVATE ds4v_vision) -add_executable(ds4v_norm_source norm_source.cpp) -target_link_libraries(ds4v_norm_source PRIVATE ds4v_vision) -add_executable(ds4v_rotary_source rotary_source.cpp) -target_link_libraries(ds4v_rotary_source PRIVATE ds4v_vision) -add_executable(ds4v_attention_source attention_source.cpp) -target_link_libraries(ds4v_attention_source PRIVATE ds4v_vision) -add_executable(ds4v_linear_rounding linear_rounding.cpp) -target_link_libraries(ds4v_linear_rounding PRIVATE ds4v_vision) -if(DS4V_VISION_HIP) - target_compile_definitions(ds4v_vision_probe PRIVATE DS4V_VISION_HIP) - target_compile_definitions(ds4v_linear_rounding PRIVATE DS4V_VISION_HIP) - target_compile_definitions(ds4v_linear_source PRIVATE DS4V_VISION_HIP) - target_compile_definitions(ds4v_linear_unbiased_source PRIVATE DS4V_VISION_HIP) - target_compile_definitions(ds4v_norm_source PRIVATE DS4V_VISION_HIP) - target_compile_definitions(ds4v_rotary_source PRIVATE DS4V_VISION_HIP) - target_compile_definitions(ds4v_attention_source PRIVATE DS4V_VISION_HIP) -endif() -add_executable(ds4v_vision_geometry geometry.cpp) -target_link_libraries(ds4v_vision_geometry PRIVATE ds4v_vision) -add_executable(ds4v_linear_contract linear_contract.cpp) -target_link_libraries(ds4v_linear_contract PRIVATE ds4v_vision) -enable_testing() -add_test(NAME ds4v_linear_contract COMMAND ds4v_linear_contract) -add_test(NAME ds4v_vision_geometry COMMAND ds4v_vision_geometry) -add_executable(ds4v_norm_contract norm_contract.cpp) -target_link_libraries(ds4v_norm_contract PRIVATE ds4v_vision) -add_test(NAME ds4v_norm_contract COMMAND ds4v_norm_contract) -add_executable(ds4v_rotary_contract rotary_contract.cpp) -target_link_libraries(ds4v_rotary_contract PRIVATE ds4v_vision) -add_test(NAME ds4v_rotary_contract COMMAND ds4v_rotary_contract) -add_executable(ds4v_attention_contract attention_contract.cpp) -target_link_libraries(ds4v_attention_contract PRIVATE ds4v_vision) -add_test(NAME ds4v_attention_contract COMMAND ds4v_attention_contract) diff --git a/server/tools/ds4v_vision/README.md b/server/tools/ds4v_vision/README.md deleted file mode 100644 index 6c304e4f4..000000000 --- a/server/tools/ds4v_vision/README.md +++ /dev/null @@ -1,178 +0,0 @@ -# Native DS4V vision runtime - -Reusable, backend-owned projector/tower implementation. `VisionRuntime` borrows -one caller-selected backend and owns its validated BF16 weight buffer plus a -reusable graph allocator. Load is transactional: a rejected reload preserves the -prior runtime. No HTTP, decoder, image decode, sentinel placement, or N-layout -changes are included. `Sentinel` exposes the four learned delimiter vectors; -image rows are the `VisionOutput.embeddings` result. - -The loader checks all exporter semantic fields and all 267 names, BF16 dtypes, -shapes, alignment, and file bounds before backend allocation. It rejects unknown -schema, layout, activation, language dimension/vocabulary, missing/extra tensors, -and malformed GGUF metadata. Original source names and weight bytes are retained. -The accepted artifact has no source-repository or source-hash metadata; provenance -is verified externally against the accepted exporter hash, not invented by the -runtime. - -## Arithmetic and resource rationale - -F32 graph tensors hold BF16-rounded activations. `cast(BF16)` then `cast(F32)` -preserves every source BF16 boundary: biased linear, RMSNorm with F32 weights, -rotary Q/K, attention output, residual additions, SiLU, gated product, and each -aligner operation. Normalization and rotary arithmetic remain F32. GELU uses -`ggml_gelu_erf`. Q/K use explicit F32 cosine/sine tables with half-split channel -pairs and height frequencies before width frequencies. Im2col performs exact -channel-first, bottom/right padded 3x3 unfolding. - -Actual Torch 2.10 CPU tracing shows the source's **3D** SDPA dispatches -`aten::_scaled_dot_product_attention_math`. That implementation scales both F32 -Q and K by sqrt(1/sqrt(head_dimension)) before matrix multiplication, so this -runtime preserves the same operation order. Scaling scores afterwards changed -rounding enough to measurably worsen both full fixtures. See -[PyTorch Math SDPA source](https://github.com/pytorch/pytorch/blob/v2.10.0/aten/src/ATen/native/transformers/attention.cpp#L807-L891). -Explicit full softmax attention has no causal mask. CPUFlash is not the source -fixture path: forced Math reproduces both original fixtures bit for bit. - -One block graph at a time bounds quadratic scratch, with a hard 2 GiB graph -buffer limit measured before allocation. Input grids must fit the complete -384-token N-layout budget, including delimiters, row/odd-row/parity padding, -and three reserved leading alignment tokens. Diagnostic tensors are independent snapshots; a flag on -a view alone does not protect its backing allocation. No attention matrices are -retained across blocks. Caller diagnostics run synchronously. Each block currently -returns its F32 residual to host and uploads it into the following graph; HIP -transfer cost and GPU behavior are unqualified. `release_scratch()` frees graph -buffers while keeping weights/sentinels. Sequential calls only. - -Alternatives considered: built-in VISION RoPE has different frequency recurrence -rounding; adjacent-pair text RoPE is mathematically wrong. Flash attention would -add an unqualified kernel/dtype path. An all-F32 tower removes required source -rounding; F16 weights are not a lossless BF16 substitute. Keeping all 32 graphs -or attention diagnostics would multiply quadratic scratch. The chosen explicit -primitive graph makes each stage inspectable at the cost of host transfers. - -## Standalone CPU qualification - -Run these commands on `soulf`, from its isolated candidate worktree. The Mac is -for authorship only. CPU builds use two jobs; probe and reference execution use -two threads. Original fixtures and reference environment remain read-only. - -```sh -cmake -S server/tools/ds4v_vision -B /tmp/ds4v-tower-a-build -DCMAKE_BUILD_TYPE=Release -cmake --build /tmp/ds4v-tower-a-build -j2 -OMP_NUM_THREADS=2 ctest --test-dir /tmp/ds4v-tower-a-build --output-on-failure -python3 server/tools/ds4v_vision/loader_tests.py /tmp/ds4v-tower-a-build/ds4v_vision_probe /home/marcelorm/ds4v-work/ds4v-mmproj.gguf -``` - -Full probes (the last argument enables independent stage snapshots): - -```sh -OMP_NUM_THREADS=2 /usr/bin/time -v /tmp/ds4v-tower-a-build/ds4v_vision_probe /home/marcelorm/ds4v-work/ds4v-mmproj.gguf /home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference/corn-patches.f32 23 34 artifacts/vision-tower-a/native corn 1 -OMP_NUM_THREADS=2 /usr/bin/time -v /tmp/ds4v-tower-a-build/ds4v_vision_probe /home/marcelorm/ds4v-work/ds4v-mmproj.gguf /home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference/carrots-patches.f32 42 61 artifacts/vision-tower-a/native carrots 1 -``` - -Use `/home/marcelorm/lucebox-ds4v-mix-fix/.venv-vision-reference/bin/python` -with `OMP_NUM_THREADS=2 OPENBLAS_NUM_THREADS=2` for these tools: - -- `compare.py REFERENCE NATIVE --output comparison.json`: original fixture - hashes, raster shapes, finite values, max absolute error, RMSE, cosine. - It exits 3 when the unchanged Candidate B gates fail: feature maxabs <=0.25, - RMSE <=0.03, cosine >=0.9995; embedding maxabs <=0.75, RMSE <=0.08, - cosine >=0.9990. These gates were fixed before the candidate fixture runs. -- `reference_stages.py SOURCE REFERENCE NATIVE stages.json`: original source - hooks reproduce both fixture finals bitwise, then each original block consumes - the native incoming residual to isolate local arithmetic from accumulated drift. - Includes BF16 ULP distances; large max ULP distances across zero should be read - with absolute error and p99, not interpreted as uniform relative error. -- `reference_math.py SOURCE REFERENCE NATIVE OUTPUT_DIR`: labeled parent-only - Math sensitivity. It preserves original fixtures and is not a new tolerance. -- `attention_dispatch.py NATIVE`: records operator dispatch with source Q/K/V - shapes, strides, and BF16 dtype. - -`SOURCE` is `/home/marcelorm/lucebox-ds4v-2/models/DeepSeek-V4-Flash-Vision-Uncensored`. -`REFERENCE` is `/home/marcelorm/lucebox-ds4v-mix-fix/artifacts/vision-reference`. -Evidence is `/home/marcelorm/lucebox-ds4v-tower-a/artifacts/vision-tower-a`. -The projector SHA256 is -`58eb6b63243df2db21261ced5568b385b04991f38d45d39b781309497abd4b1c`. - -## Verdict: ISSUES (numerical acceptance remains open) - -Geometry checks, strict loader rejection, finite shapes, transactional reload, -sentinel access, and scratch release pass. Six deterministic arithmetic/geometry -checks are in CTest; sixteen malformed projector/language cases are in the loader -script. Corn observer-off and observer-on finals match bitwise. All graphs execute -on CPU without GPU, decoder, or HTTP changes. - -Final native graph versus **original** CPU fixtures: - -| Image/output | Shape | Max absolute | RMSE | Cosine | -|---|---|---:|---:|---:| -| carrots features | 2562 x 1024 | 0.1376953125 | 0.00259378329 | 0.99967582518 | -| carrots embeddings | 294 x 4096 | 0.02642822266 | 0.00142200006 | 0.99981156934 | -| corn features | 782 x 1024 | 1.484375 | 0.00629541949 | 0.99822935535 | -| corn embeddings | 96 x 4096 | 0.09423828125 | 0.00319258993 | 0.99907754975 | - -All four outputs are finite. Corn fails the fixed feature gate, so the comparison -command returns 3. Differences begin at patch embedding and accumulate through BF16 -residuals. Stage diagnostics isolate sharp amplification at blocks 12 and 31. -Same-input unfolding is bitwise exact for both fixtures, and same-input final -norm/aligner comparisons isolate small local kernel errors. The source Math -attention check does not explain away remaining end-to-end drift. No numerical -tolerance was widened. Full decoder logits, GPU transfer cost, and HIP arithmetic -remain unqualified. - -Final CPU resource measurements with stage snapshots enabled: weights 932,786,176 -bytes; corn scratch 77,774,592 bytes and 3.30829 s encode; carrots scratch -546,669,312 bytes and 17.6707 s encode. Peak process RSS was 1,827,056 KiB -(about 1.74 GiB), including the transient read-only mapped weight source during -load. The configured 2 GiB graph scratch cap is distinct from total process RSS. -Largest permitted grids are bounded analytically and by allocation measurement; -only the original 782/2562-patch grids have full numerical reference qualification. - -## Selected-base follow-up - -The full N-layout budget regression fails at5845ed5 and passes at5bf705e. -The largest grid permitted by this budget (6x561=3366patches) runs with finite -outputs and bitwise observer invariance. It is an allocation boundary fixture, -not a source resize-aspect fixture. Scratch is822488832bytes without diagnostics -and891424512bytes with them; encode times26.74/27.14seconds, peak RSS1881796KiB. -Failure/reload checks precede each successful encode; scratch release follows it. - -An optional HIP build can prepare the same probe for the later GPU window: - -```sh -ROCM_PATH=/opt/rocm-7.2.4 cmake -S server/tools/ds4v_vision -B /tmp/ds4v-runtime-hip-build -DCMAKE_BUILD_TYPE=Release -DDS4V_VISION_HIP=ON '-DCMAKE_HIP_ARCHITECTURES=gfx1100;gfx1151' -DCMAKE_HIP_COMPILER=/opt/rocm-7.2.4/lib/llvm/bin/clang++ -cmake --build /tmp/ds4v-runtime-hip-build -j2 -``` - -Append `hip:0` or `hip:1` to an encode/load-only probe command to request that -device explicitly. It fails if unavailable and never falls back to CPU. Building -the target is not GPU qualification. Do not run it on GPU before the private text -load proof and the operator's GPU window permit it. - - -## Explicit HIP fused-bias source comparison - -`ds4v_linear_source cpu|hip:0 tiny|patch|qkv FIXTURE_DIR REFERENCE_F32 NEW_OUTPUT_DIR` -runs the shared production vision-linear helper on exact BF16 values stored as -F32 fixture files. Frozen source fixtures, input/library hashes, actual device -identity, and GPU release must be checked by the external supervisor. The tool -never selects a different backend or modifies a reference. Exit 3 preserves a -numerical failure; it is not a successful qualification. HIP requires exactly -one explicit fused operation in the graph and exactly one actual Lt launch. -The unbiased lane separately retains its ordinary product and final BF16 cast. - -The historical `ds4v_linear_rounding` mathematical RNE probe continues to test -its explicitly requested old helper mode (no HIP capability argument). Its -biased RNE oracle differs from the frozen original HIP fused-source oracle; -it does not qualify the new runtime operation. Use `ds4v_linear_source` and -then the unchanged full-image gates for the actual runtime path. - -HIP biased linears now use a dedicated, source-configured BF16 hipBLASLt bias -epilogue operation. CPU/NVIDIA and unbiased graph construction retain the -previous implementation. The HIP backend retains one 76 MiB workspace outside -the graph allocator, charged against the 2 GiB scratch bound and included in -`scratch_bytes()` even after `release_scratch()`. It is freed when the borrowed -backend context is destroyed. Graphs containing the operation do not use HIP -graph capture. Only the pinned Radeon RX 7900 XT reference is the qualification -target; other HIP hardware has not been qualified. diff --git a/server/tools/ds4v_vision/attention_contract.cpp b/server/tools/ds4v_vision/attention_contract.cpp deleted file mode 100644 index 2d9d02e8d..000000000 --- a/server/tools/ds4v_vision/attention_contract.cpp +++ /dev/null @@ -1,87 +0,0 @@ -#include "deepseek4/deepseek4_vision.h" -#include "ggml-cpu.h" -#include -#include -#include -#include -#include -#include -#include - -using namespace dflash::vision; -static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && - GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && - GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed"); - -static void reject(bool av,int mode) { - const pid_t pid=fork(); check(pid>=0,"fork failed"); - if(pid==0) { - const rlimit limit={0,0}; setrlimit(RLIMIT_CORE,&limit); - auto c=ggml_init({1024*1024,nullptr,true}); - if(!av) { - auto x=ggml_new_tensor_2d(c,mode==1?GGML_TYPE_BF16:GGML_TYPE_F32,16,16); - if(mode==0) x=nullptr; - if(mode==2) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,15,16); - if(mode==3) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,4097,16); - if(mode==4) x=ggml_transpose(c,x); - if(mode==5) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,16,int64_t(INT_MAX)/64+1); - (void)ggml_soft_max_vision_f32(c,x); - } else { - int n=mode==5?15:mode==6?4097:16; - auto v=ggml_new_tensor_3d(c,mode==2?GGML_TYPE_BF16:GGML_TYPE_F32,mode==3?32:64,mode==4?8:16,n); - auto p=ggml_new_tensor_3d(c,mode==7?GGML_TYPE_BF16:GGML_TYPE_F32,n,n,16); - if(mode==0) v=nullptr; - if(mode==1) p=nullptr; - if(mode==8) p=ggml_new_tensor_3d(c,GGML_TYPE_F32,n,n+1,16); - if(mode==9) p=ggml_new_tensor_3d(c,GGML_TYPE_F32,n,n,8); - if(mode==10) p=ggml_transpose(c,p); - if(mode==11) v=ggml_permute(c,ggml_new_tensor_3d(c,GGML_TYPE_F32,16,64,n),1,0,2,3); - if(mode==12) v=ggml_new_tensor_4d(c,GGML_TYPE_F32,64,16,n,2); - if(mode==13) p=ggml_new_tensor_4d(c,GGML_TYPE_F32,n,n,16,2); - (void)ggml_mul_mat_vision_av_f32(c,v,p); - } - _exit(0); - } - int status=0; check(waitpid(pid,&status,0)==pid,"wait failed"); - check(WIFSIGNALED(status) && WTERMSIG(status)==SIGABRT,"invalid attention constructor accepted"); -} - -int main() { - auto backend=ggml_backend_cpu_init(); auto c=ggml_init({2*1024*1024,nullptr,true}); - try { - check(backend && c,"initialization failed"); - for(auto selected:{static_cast(nullptr),backend}) { - check(!detail::hip_softmax_capable(selected) && !detail::hip_av_capable(selected),"CPU/null advertised HIP attention"); - check(detail::hip_softmax_launches(selected)==0 && detail::hip_av_launches(selected)==0,"CPU/null reported HIP launches"); - auto q=ggml_new_tensor_3d(c,GGML_TYPE_F32,4,2,3); - auto g=ggml_new_graph(c); ggml_build_forward_expand(g,detail::attention(c,q,q,q,selected)); - int softmax=0,matmul=0; - for(int i=0;iop; - check(op!=GGML_OP_SOFT_MAX_VISION_F32 && op!=GGML_OP_MUL_MAT_VISION_AV_F32,"CPU generic attention changed"); - softmax+=op==GGML_OP_SOFT_MAX; matmul+=op==GGML_OP_MUL_MAT; - } - check(softmax==1 && matmul==2,"CPU attention graph operation count changed"); - } - for(int n:{16,128,782,2048,2049,2560,2562,4096}) { - auto v=ggml_new_tensor_3d(c,GGML_TYPE_F32,64,16,n); - auto x=ggml_new_tensor_3d(c,GGML_TYPE_F32,n,n,16); - auto p=ggml_soft_max_vision_f32(c,x); - auto y=ggml_mul_mat_vision_av_f32(c,v,p); - check(p->op==GGML_OP_SOFT_MAX_VISION_F32 && p->src[0]==x && p->type==GGML_TYPE_F32 && - ggml_are_same_shape(p,x),"softmax constructor shape changed"); - check(y->op==GGML_OP_MUL_MAT_VISION_AV_F32 && y->src[0]==v && y->src[1]==p && - y->type==GGML_TYPE_F32 && y->ne[0]==64 && y->ne[1]==n && y->ne[2]==16 && y->ne[3]==1, - "AV constructor layout changed"); - check(!ggml_backend_supports_op(backend,p) && !ggml_backend_supports_op(backend,y),"CPU advertised HIP operations"); - } - for(int mode=0;mode<6;++mode) reject(false,mode); - for(int mode=0;mode<14;++mode) reject(true,mode); - ggml_free(c); ggml_backend_free(backend); - std::cout<<"PASS: HIP attention ABI, bounded shapes, CPU/null preservation, invalid input rejection\n"; - return 0; - } catch(const std::exception &error) { - std::cerr<<"FAIL: "< -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fs=std::filesystem; -constexpr int ROWS=782, WIDTH=1024, HEADS=16, DIM=64; -constexpr size_t LIMIT=256ULL*1024*1024; -static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } -static uint32_t bits(float x) { uint32_t out; std::memcpy(&out,&x,4); return out; } -static std::vector load(const fs::path &path,size_t count,bool bf16=false) { - check(fs::is_regular_file(path) && fs::file_size(path)==count*4,"wrong fixture size"); - std::vector out(count); std::ifstream f(path,std::ios::binary); - f.read(reinterpret_cast(out.data()),count*4); check(bool(f),"fixture read failed"); - for(float x:out) check(std::isfinite(x) && (!bf16 || !(bits(x)&65535)),"invalid fixture values"); - return out; -} -static size_t compare(const std::vector &a,const std::vector &b) { - check(a.size()==b.size(),"comparison size mismatch"); size_t count=0; - for(size_t i=0;i &values) { - std::ofstream f(path,std::ios::binary); - f.write(reinterpret_cast(values.data()),values.size()*4); check(bool(f),"output write failed"); -} -struct Backend { - ggml_backend_t value=nullptr; - ~Backend() { if(value) ggml_backend_free(value); } -}; -struct Graph { - ggml_backend_t backend; - ggml_context *ctx=nullptr; ggml_cgraph *graph=nullptr; ggml_gallocr_t allocator=nullptr; - std::map outputs; - std::vector *>> inputs; - explicit Graph(ggml_backend_t b):backend(b) { - ctx=ggml_init({1024*1024,nullptr,true}); check(ctx,"metadata allocation failed"); - graph=ggml_new_graph(ctx); - } - ~Graph() { - ggml_backend_synchronize(backend); - if(allocator) ggml_gallocr_free(allocator); - if(ctx) ggml_free(ctx); - } - ggml_tensor *input(const std::vector &values,int64_t n0,int64_t n1,int64_t n2=1) { - check(values.size()==size_t(n0*n1*n2),"input shape mismatch"); - auto t=ggml_new_tensor_3d(ctx,GGML_TYPE_F32,n0,n1,n2); - ggml_set_input(t); inputs.emplace_back(t,&values); return t; - } - void output(const std::string &name,ggml_tensor *t) { - auto copy=ggml_dup(ctx,t); ggml_set_output(copy); - check(outputs.emplace(name,copy).second,"duplicate output"); - ggml_build_forward_expand(graph,copy); - } - std::map> execute(const fs::path &path,const std::string &prefix) { - for(int i=0;idata(),0,values->size()*4); - check(ggml_backend_graph_compute(backend,graph)==GGML_STATUS_SUCCESS,"graph execution failed"); - ggml_backend_synchronize(backend); - std::map> result; - for(auto &[name,t]:outputs) { - std::vector values(ggml_nelements(t)); - ggml_backend_tensor_get(t,values.data(),0,values.size()*4); - for(float x:values) check(std::isfinite(x),"nonfinite graph result"); - save(path/(prefix+name+".f32"),values); result.emplace(name,std::move(values)); - } - std::cout< cosine,sine; - detail::rotary_tables({23,34},cosine,sine,backend.value); - check(compare(cosine,load(source/"cos.f32",cosine.size()))==0 && - compare(sine,load(source/"sin.f32",sine.size()))==0,"rotary source differs"); - save(out/"cos.f32",cosine); save(out/"sin.f32",sine); - Graph g(backend.value); - auto input=g.input(qkv,3072,ROWS); - auto cos=g.input(cosine,32,1,ROWS),sin=g.input(sine,32,1,ROWS); - auto slice=[&](int offset) { - return ggml_cont(g.ctx,ggml_view_3d(g.ctx,input,DIM,HEADS,ROWS,DIM*4,3072*4,offset*WIDTH*4)); - }; - auto q=detail::rotate(g.ctx,slice(0),cos,sin),k=detail::rotate(g.ctx,slice(1),cos,sin),v=slice(2); - auto attention=detail::attention(g.ctx,q,k,v,backend.value); - g.output("attention",attention); g.output("q",q); g.output("k",k); g.output("v",v); - ggml_tensor *probabilities=nullptr,*precast=nullptr; - int softmax_count=0,av_count=0; - for(int i=0;iop==GGML_OP_SOFT_MAX_VISION_F32) { probabilities=node; ++softmax_count; } - if(node->op==GGML_OP_MUL_MAT_VISION_AV_F32) { precast=node; ++av_count; } - check(node->op!=GGML_OP_SOFT_MAX,"generic softmax in HIP attention"); - } - check(softmax_count==1 && av_count==1 && probabilities && precast,"attention operation count changed"); - g.output("scores",probabilities->src[0]); g.output("probabilities",probabilities); g.output("precast_av",precast); - const auto actual=g.execute(out,""); - for(const auto &[name,values]:actual) { - const size_t different=compare(values,load(source/(name+".f32"),values.size())); - std::cout<= gate['cosine']) - passed = passed and measured['pass'] -a.output.write_text(json.dumps(results, indent=2)+'\n') -print(json.dumps(results, indent=2)) -raise SystemExit(0 if passed else 3) diff --git a/server/tools/ds4v_vision/geometry.cpp b/server/tools/ds4v_vision/geometry.cpp deleted file mode 100644 index 5de8d343f..000000000 --- a/server/tools/ds4v_vision/geometry.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include "deepseek4/deepseek4_vision.h" -#include "ggml.h" -#include "ggml-alloc.h" -#include "ggml-cpu.h" -#include -#include -#include -#include - -using namespace dflash::vision; -static void check(bool value,const char * message) { if(!value) throw std::runtime_error(message); } -static float bf16(float value) { return ggml_bf16_to_fp32(ggml_fp32_to_bf16(value)); } -struct Test { - ggml_backend_t backend=ggml_backend_cpu_init(); - ggml_context * c=ggml_init({1024*1024,nullptr,true}); - ggml_gallocr_t alloc=ggml_gallocr_new(ggml_backend_cpu_buffer_type()); - std::vector>> inputs; - Test() { ggml_backend_cpu_set_n_threads(backend,2); } - ~Test() { ggml_gallocr_free(alloc); ggml_free(c); ggml_backend_free(backend); } - ggml_tensor * input(int a,int b,int d,std::vector data) { - auto t=ggml_new_tensor_3d(c,GGML_TYPE_F32,a,b,d); - ggml_set_input(t); inputs.emplace_back(t,std::move(data)); return t; - } - std::vector run(ggml_tensor * t) { - ggml_set_output(t); - auto g=ggml_new_graph(c); ggml_build_forward_expand(g,t); - check(ggml_gallocr_alloc_graph(alloc,g),"test graph allocation failed"); - for(auto & p:inputs) ggml_backend_tensor_set(p.first,p.second.data(),0,p.second.size()*4); - check(ggml_backend_graph_compute(backend,g)==GGML_STATUS_SUCCESS,"test compute failed"); - std::vector out(ggml_nelements(t)); ggml_backend_tensor_get(t,out.data(),0,out.size()*4); return out; - } -}; -int main() { - try { - { - Test t; - std::vector cosine,sine; detail::rotary_tables({2,3},cosine,sine); - std::vector x(64*2*6); - for(size_t i=0;i(6,0)); - auto k=t.input(2,1,3,std::vector(6,0)); - auto v=t.input(2,1,3,{1,2,4,5,10,11}); - auto out=t.run(detail::attention(t.c,q,k,v)); - for(int i=0;i<3;++i) { check(out[2*i]==5,"bidirectional attention mismatch"); check(out[2*i+1]==6,"attention channel mismatch"); } - } - { - Test t; - const int heads=2,n=3,d=4; - std::vector q(n*heads*d),k(q.size()),v(q.size()); - for(int p=0;p({1.f,1.015625f,-1.f,-1.015625f}),"BF16 halfway rounding mismatch"); - } - { - Test t; - const int h=4,w=5,c=2; - std::vector data(h*w*c); - for(int y=0;y x={-3.f,-1.f,-.1f,0.f,.1f,1.f,3.f}; - auto out=t.run(ggml_gelu_erf(t.c,t.input(7,1,1,x))); - for(int i=0;i<7;++i) check(std::abs(out[i]-.5f*x[i]*(1+std::erf(x[i]/std::sqrt(2.f))))<1e-6f,"exact erf GELU mismatch"); - } - std::cout<<"PASS: half-split 2D RoPE, full bidirectional/multihead attention, BF16 halfway rounding, padded channel-first unfold, exact erf GELU\n"; - return 0; - } catch(const std::exception & e) { std::cerr< -#include -#include -#include -#include -static void check(bool ok,const char *why) { if(!ok)throw std::runtime_error(why); } -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed"); -static void rejected(int mode,bool with_bias=true) { - pid_t pid=fork(); check(pid>=0,"fork failed"); - if(pid==0) { - auto c=ggml_init({1024*1024,nullptr,true}); - auto w=ggml_new_tensor_2d(c,mode==0?GGML_TYPE_F32:GGML_TYPE_BF16,64,32); - auto x=ggml_new_tensor_2d(c,mode==4?GGML_TYPE_F32:GGML_TYPE_BF16,mode==1?32:64,32); - auto b=ggml_new_tensor_1d(c,mode==10?GGML_TYPE_F32:GGML_TYPE_BF16,mode==2?16:32); - // Keep valid dimensions so these cases independently exercise contiguity. - if(mode==3) x=ggml_transpose(c,ggml_new_tensor_2d(c,GGML_TYPE_BF16,32,64)); - if(mode==5) w=ggml_transpose(c,ggml_new_tensor_2d(c,GGML_TYPE_BF16,32,64)); - if(mode==6) w=ggml_new_tensor_3d(c,GGML_TYPE_BF16,64,32,2); - if(mode==7) x=ggml_new_tensor_3d(c,GGML_TYPE_BF16,64,32,2); - if(mode==8) w=nullptr; - if(mode==9) x=nullptr; - if(mode==11) b=ggml_new_tensor_2d(c,GGML_TYPE_BF16,32,2); - (void)ggml_mul_mat_bias_bf16(c,w,x,with_bias?b:nullptr); - _exit(0); - } - int status=0; check(waitpid(pid,&status,0)==pid,"wait failed"); - check(WIFSIGNALED(status)&&WTERMSIG(status)==SIGABRT,"invalid constructor was not rejected"); -} -int main() { - auto backend=ggml_backend_cpu_init(); auto c=ggml_init({1024*1024,nullptr,true}); - try { - check(backend&&c,"initialization failed"); - check(dflash::vision::detail::hip_bias_workspace(nullptr)==0 && dflash::vision::detail::hip_bias_workspace(backend)==0,"CPU/null advertised HIP capability"); - auto w=ggml_new_tensor_2d(c,GGML_TYPE_BF16,64,32),x=ggml_new_tensor_2d(c,GGML_TYPE_BF16,64,32),b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,32); - auto y=ggml_mul_mat_bias_bf16(c,w,x,b); - check(y->type==GGML_TYPE_BF16 && y->src[0]==w && y->src[1]==x && y->src[2]==b && y->ne[0]==32 && y->ne[1]==32,"explicit op contract mismatch"); - check(!ggml_backend_supports_op(backend,y),"CPU advertised HIP op"); - auto unbiased_op=ggml_mul_mat_bias_bf16(c,w,x,nullptr); - check(unbiased_op->op==GGML_OP_MUL_MAT_BIAS_BF16 && unbiased_op->type==GGML_TYPE_BF16 && - unbiased_op->src[0]==w && unbiased_op->src[1]==x && unbiased_op->src[2]==nullptr && - unbiased_op->ne[0]==32 && unbiased_op->ne[1]==32,"optional-bias op contract mismatch"); - check(!ggml_backend_supports_op(backend,unbiased_op),"CPU advertised unbiased HIP op"); - auto xf=ggml_new_tensor_2d(c,GGML_TYPE_F32,64,32); - for(bool preserve:{false,true}) { - auto old=dflash::vision::detail::linear(c,w,xf,b,preserve); - auto selected=dflash::vision::detail::linear(c,w,xf,b,preserve,backend); - check(old->op==selected->op && selected->src[0]->op==old->src[0]->op,"non-HIP graph root changed"); - auto product=selected->src[0]->src[0]->src[0]; - check(product->op==GGML_OP_MUL_MAT && (product->src[0]->op==GGML_OP_CPY)==preserve,"non-HIP product dispatch changed"); - for(auto cpu_backend:{static_cast(nullptr),backend}) { - auto unbiased=dflash::vision::detail::linear(c,w,xf,nullptr,preserve,cpu_backend); - check(unbiased->op==GGML_OP_CPY && unbiased->type==GGML_TYPE_F32 && - unbiased->src[0]->op==GGML_OP_CPY && unbiased->src[0]->type==GGML_TYPE_BF16, - "non-HIP unbiased rounding boundary changed"); - auto raw=unbiased->src[0]->src[0]; - check(raw->op==GGML_OP_MUL_MAT && raw->src[0]==w && raw->src[1]==xf, - "non-HIP unbiased product dispatch changed"); - } - } - for(int mode=0;mode<4;++mode) rejected(mode); - for(int mode:{0,1,3,4,5,6,7,8,9}) rejected(mode,false); - for(int mode:{4,5,6,7,8,9,10,11}) rejected(mode); - ggml_free(c); ggml_backend_free(backend); std::cout<<"PASS: CPU/null capability, preserved op ABI/graph, invalid constructor rejection\n"; return 0; - } catch(const std::exception&e) { std::cerr< -#include -#include -#include -#include -#include -#include -#include - -static void check(bool ok,const char * message) { if(!ok) throw std::runtime_error(message); } -static uint32_t bits(float value) { uint32_t b; std::memcpy(&b,&value,4); return b; } -static float from_bits(uint32_t b) { float value; std::memcpy(&value,&b,4); return value; } -// Independent nearest-even BF16 oracle. The fixture is finite and every F64 -// dot/sum is exactly representable in F32, so accumulation order cannot affect it. -static float round_bf16(double exact) { - float value=static_cast(exact); - check(double(value)==exact,"oracle value is not exactly representable in F32"); - uint32_t b=bits(value), high=b>>16, low=b&65535; - if(low>32768 || (low==32768 && (high&1))) ++high; - return from_bits(high<<16); -} -static std::vector pack(const std::vector & values) { - std::vector out; - for(float value:values) { check((bits(value)&65535)==0,"fixture input is not exact BF16"); out.push_back({uint16_t(bits(value)>>16)}); } - return out; -} -static void save(const std::filesystem::path & path,const std::vector & values) { - std::ofstream file(path,std::ios::binary); file.write(reinterpret_cast(values.data()),values.size()*4); - check(bool(file),"output write failed"); -} -static std::vector read(ggml_tensor * tensor) { - std::vector values(ggml_nelements(tensor)); ggml_backend_tensor_get(tensor,values.data(),0,values.size()*4); return values; -} -struct Graph { - ggml_context * context=ggml_init({1024*1024,nullptr,true}); - ggml_gallocr_t allocator; - explicit Graph(ggml_backend_t backend):allocator(ggml_gallocr_new(ggml_backend_get_default_buffer_type(backend))) { - check(context && allocator,"graph creation failed"); - } - ~Graph() { ggml_gallocr_free(allocator); ggml_free(context); } -}; -int main(int argc,char ** argv) { - if(argc!=3) { std::cerr<<"usage: ds4v_linear_rounding cpu|hip:0 NEW_OUTPUT_DIR\n"; return 2; } - ggml_backend_t backend=nullptr; - const std::string device=argv[1]; - if(device=="cpu") { backend=ggml_backend_cpu_init(); if(backend) ggml_backend_cpu_set_n_threads(backend,2); } -#ifdef DS4V_VISION_HIP - else if(device=="hip:0" && ggml_backend_cuda_get_device_count()>0) backend=ggml_backend_cuda_init(0); -#endif - if(!backend) { std::cerr<<"requested backend unavailable\n"; return 1; } - int status=1; - try { - const std::filesystem::path directory=argv[2]; - check(!std::filesystem::exists(directory),"output directory already exists"); - std::filesystem::create_directories(directory); - std::cout<<"backend="<=256,"fixture fails to distinguish intermediate rounding"); - check(expected[m]==1.0078125f && premature[m]==1.f,"positive tie oracle changed"); - check(expected[m+1]==-1.0078125f && premature[m+1]==-1.f,"negative tie oracle changed"); - Graph owner(backend); auto c=owner.context; - auto w=ggml_new_tensor_2d(c,GGML_TYPE_BF16,k,m); - auto x=ggml_new_tensor_2d(c,GGML_TYPE_F32,k,n); - auto b=ggml_new_tensor_1d(c,GGML_TYPE_BF16,m); - for(auto t:{w,x,b}) ggml_set_input(t); - auto raw_dot=ggml_mul_mat(c,w,x); ggml_mul_mat_set_prec(raw_dot,GGML_PREC_F32); - auto actual=dflash::vision::detail::linear(c,w,x,b,preserve); - auto unbiased=dflash::vision::detail::linear(c,w,x,nullptr,preserve); - auto graph=ggml_new_graph(c); - for(auto t:{raw_dot,actual,unbiased}) { ggml_set_output(t); ggml_build_forward_expand(graph,t); } - size_t required=0; ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&required); - check(required<16*1024*1024,"tiny graph scratch unexpectedly large"); - for(int i=0;i *>>{ - {"weights.f32",&weights},{"inputs.f32",&inputs},{"bias.f32",&bias},{"expected.f32",&expected}, - {"premature.f32",&premature},{"actual.f32",&values},{"raw-dot.f32",&dots},{"exact-dot.f32",&dot}, - {"unbiased.f32",&no_bias}}) save(directory/item.first,*item.second); - std::cout<<"shape="< -#include -#include -#include -#include -#include -#include -#include -#include -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed unexpectedly"); -static void check(bool b,const char *s) { if(!b) throw std::runtime_error(s); } -static uint32_t bits(float v) { uint32_t b; std::memcpy(&b,&v,4); return b; } -static std::vector load(const std::filesystem::path&p,size_t n) { - check(std::filesystem::file_size(p)==n*4,"fixture size mismatch"); - std::vector v(n); std::ifstream f(p,std::ios::binary); f.read((char*)v.data(),n*4); - check(bool(f),"fixture read failed"); - for(float x:v) check(std::isfinite(x) && !(bits(x)&65535),"fixture not exact finite BF16"); - return v; -} -static std::vector pack(const std::vector&v) { - std::vector r; for(float x:v) r.push_back({uint16_t(bits(x)>>16)}); return r; -} -struct Graph { - ggml_context *c=ggml_init({1024*1024,nullptr,true}); - ggml_gallocr_t a; - explicit Graph(ggml_backend_t b):a(ggml_gallocr_new(ggml_backend_get_default_buffer_type(b))) { check(c&&a,"graph creation failed"); } - ~Graph() { ggml_gallocr_free(a); ggml_free(c); } -}; -int main(int argc,char**argv) { - std::cout<op==GGML_OP_MUL_MAT_BIAS_BF16; - check(ops==(dev=="hip:0"?2u:0u),"wrong actual fused-op graph dispatch"); - check((external!=0)==(dev=="hip:0"),"wrong HIP-only capability"); - const size_t before=dflash::vision::detail::hip_bias_launches(backend); - size_t scratch=0; ggml_gallocr_reserve_n_size(owner.a,g,nullptr,nullptr,&scratch); - constexpr size_t limit=128ULL*1024*1024; - check(external<=limit && scratch<=limit-external,"graph arena plus workspace exceeds fixed bound"); - for(int i=0;idata(),item.second->size()*4); check(bool(f),"output write failed"); } - std::cout<<"elements="< -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109, - "operation ABI changed unexpectedly"); -static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } -static size_t product(size_t a,size_t b) { - check(b==0 || a<=std::numeric_limits::max()/b,"size overflow"); - return a*b; -} -static uint32_t bits(float value) { uint32_t result; std::memcpy(&result,&value,4); return result; } -static std::vector load(const std::filesystem::path &path,size_t count) { - const size_t bytes=product(count,sizeof(float)); - check(bytes<=size_t(std::numeric_limits::max()),"fixture exceeds stream limit"); - check(std::filesystem::is_regular_file(path) && std::filesystem::file_size(path)==bytes,"fixture size mismatch"); - std::vector result(count); - std::ifstream file(path,std::ios::binary); - file.read(reinterpret_cast(result.data()),std::streamsize(bytes)); - check(bool(file),"fixture read failed"); - for(float value:result) - check(std::isfinite(value) && !(bits(value)&65535),"fixture must contain finite exact BF16 values"); - return result; -} -static std::vector pack(const std::vector &values) { - std::vector result; result.reserve(values.size()); - for(float value:values) result.push_back({uint16_t(bits(value)>>16)}); - return result; -} -struct Resources { - ggml_backend_t backend=nullptr; - ggml_context *context=nullptr; - ggml_gallocr_t allocator=nullptr; - ~Resources() { - if(backend) ggml_backend_synchronize(backend); - if(allocator) ggml_gallocr_free(allocator); - if(context) ggml_free(context); - if(backend) ggml_backend_free(backend); - } -}; -int main(int argc,char **argv) { - std::cout<type==GGML_TYPE_F32 && size_t(ggml_nelements(y))==elements,"unexpected output layout"); - ggml_set_output(y); - auto graph=ggml_new_graph(owner.context); - ggml_build_forward_expand(graph,y); - size_t explicit_ops=0; - for(int i=0;iop==GGML_OP_MUL_MAT_BIAS_BF16) { - ++explicit_ops; - check(node->src[2]==nullptr,"unbiased graph unexpectedly has a bias operand"); - } - } - check(explicit_ops==(device=="hip:0"?1u:0u),"wrong explicit unbiased graph dispatch"); - const size_t external=dflash::vision::detail::hip_bias_workspace(owner.backend); - check(external==(device=="hip:0"?76ULL*1024*1024:0),"wrong fixed HIP workspace capability"); - owner.allocator=ggml_gallocr_new(ggml_backend_get_default_buffer_type(owner.backend)); - check(owner.allocator,"graph allocator creation failed"); - size_t arena=0; - ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&arena); - constexpr size_t limit=128ULL*1024*1024; - check(external<=limit && arena<=limit-external,"graph arena plus workspace exceeds 128 MiB"); - check(ggml_gallocr_reserve(owner.allocator,graph),"graph reservation failed"); - check(ggml_gallocr_alloc_graph(owner.allocator,graph),"graph allocation failed"); - check(ggml_gallocr_get_buffer_size(owner.allocator,0)<=arena,"actual arena exceeds reservation estimate"); - ggml_backend_tensor_set(w,weights.data(),0,product(weights.size(),sizeof(ggml_bf16_t))); - ggml_backend_tensor_set(x,inputs.data(),0,product(inputs.size(),sizeof(float))); - const size_t before=dflash::vision::detail::hip_bias_launches(owner.backend); - check(ggml_backend_graph_compute(owner.backend,graph)==GGML_STATUS_SUCCESS,"graph execution failed"); - ggml_backend_synchronize(owner.backend); - const size_t after=dflash::vision::detail::hip_bias_launches(owner.backend); - check(after>=before && after-before==explicit_ops,"wrong actual Lt submission count"); - const size_t norm_launches=dflash::vision::detail::hip_norm_launches(owner.backend); - check(norm_launches==0,"linear graph unexpectedly submitted vision normalization"); - std::vector actual(elements); - ggml_backend_tensor_get(y,actual.data(),0,output_bytes); - size_t mismatches=0; double max_abs=0; - for(size_t i=0;i(actual.data()),std::streamsize(output_bytes)); - file.close(); check(bool(file),"output write failed"); - std::cout<<"explicit_unbiased_ops="< -#include -#include -#include -#include -#include -#include -#include - -static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } -static_assert(GGML_OP_PAGED_ATTN==104 && GGML_OP_MUL_MAT_BIAS_BF16==105 && - GGML_OP_RMS_NORM_VISION_F32==106 && GGML_OP_SOFT_MAX_VISION_F32==107 && GGML_OP_MUL_MAT_VISION_AV_F32==108 && GGML_OP_COUNT==109,"operation ABI changed"); -static void rejected(int mode) { - const pid_t pid=fork(); check(pid>=0,"fork failed"); - if(pid==0) { - auto c=ggml_init({1024*1024,nullptr,true}); - auto x=ggml_new_tensor_2d(c,mode==1?GGML_TYPE_BF16:GGML_TYPE_F32,mode==2?512:1024,mode==3?15:16); - if(mode==0) x=nullptr; - if(mode==4) x=ggml_transpose(c,ggml_new_tensor_2d(c,GGML_TYPE_F32,16,1024)); - if(mode==5) x=ggml_new_tensor_3d(c,GGML_TYPE_F32,1024,16,2); - if(mode==9) x=ggml_new_tensor_2d(c,GGML_TYPE_F32,1024,int64_t(INT_MAX)/1024+1); - const float eps=mode==6?-1.f:mode==7?std::numeric_limits::infinity(): - mode==8?std::numeric_limits::quiet_NaN():1e-6f; - (void)ggml_rms_norm_vision_f32(c,x,eps); - _exit(0); - } - int status=0; check(waitpid(pid,&status,0)==pid,"wait failed"); - check(WIFSIGNALED(status) && WTERMSIG(status)==SIGABRT,"invalid source-order norm was accepted"); -} -int main() { - auto backend=ggml_backend_cpu_init(); auto c=ggml_init({1024*1024,nullptr,true}); - try { - check(backend && c,"initialization failed"); - check(!dflash::vision::detail::hip_norm_capable(nullptr) && !dflash::vision::detail::hip_norm_capable(backend), - "CPU/null advertised HIP source normalization"); - check(dflash::vision::detail::hip_norm_launches(nullptr)==0 && dflash::vision::detail::hip_norm_launches(backend)==0, - "CPU/null reported HIP normalization launches"); - for(int rows:{16,782,2562}) { - auto x=ggml_new_tensor_2d(c,GGML_TYPE_F32,1024,rows); - auto y=ggml_rms_norm_vision_f32(c,x,1e-6f); - check(y->op==GGML_OP_RMS_NORM_VISION_F32 && y->type==GGML_TYPE_F32 && - y->src[0]==x && y->src[1]==nullptr && y->ne[0]==1024 && y->ne[1]==rows, - "source normalization constructor contract changed"); - check(!ggml_backend_supports_op(backend,y),"CPU advertised source-order HIP operation"); - } - for(int rows:{1,16,782}) { - auto x=ggml_new_tensor_2d(c,GGML_TYPE_F32,1024,rows); - for(auto selected:{static_cast(nullptr),backend}) { - auto y=dflash::vision::detail::rms_norm(c,x,1e-6f,selected); - check(y->op==GGML_OP_RMS_NORM && y->src[0]==x && y->type==GGML_TYPE_F32, - "generic CPU/null normalization path changed"); - } - } - for(int mode=0;mode<10;++mode) rejected(mode); - ggml_free(c); ggml_backend_free(backend); - std::cout<<"PASS: explicit HIP norm contract, CPU/null preservation, invalid input rejection\n"; - return 0; - } catch(const std::exception &e) { - std::cerr< -#include -#include -#include -#include -#include -#include -#include -#include - -namespace fs=std::filesystem; -constexpr int columns=1024,source_rows=782; -static void check(bool ok,const char *why) { if(!ok) throw std::runtime_error(why); } -static uint32_t bits(float value) { uint32_t out; std::memcpy(&out,&value,4); return out; } -static std::vector read(const fs::path &path,size_t count,bool bf16=false) { - check(fs::is_regular_file(path) && fs::file_size(path)==count*4,"wrong fixture size"); - std::vector values(count); std::ifstream file(path,std::ios::binary); - file.read(reinterpret_cast(values.data()),values.size()*4); check(bool(file),"fixture read failed"); - for(float value:values) check(std::isfinite(value) && (!bf16 || !(bits(value)&65535)),"invalid fixture values"); - return values; -} -static std::vector tile(const std::vector &source,int rows) { - check(source.size()==size_t(source_rows)*columns,"wrong fixed source shape"); - std::vector result(size_t(rows)*columns); - for(int row=0;row packed; for(float x:weights) packed.push_back({uint16_t(bits(x)>>16)}); - std::vector> expected; - for(const char *name:{"scaled","weighted","output"}) expected.push_back(tile(read(fixtures/(std::string(name)+".f32"),size_t(source_rows)*columns),rows)); - Resources owner; -#ifdef DS4V_VISION_HIP - check(ggml_backend_cuda_get_device_count()==1,"exactly one visible GPU required"); - owner.backend=ggml_backend_cuda_init(0); -#endif - check(owner.backend && dflash::vision::detail::hip_norm_capable(owner.backend),"source-order HIP normalization unavailable"); - std::cout<<"backend="<op==GGML_OP_RMS_NORM_VISION_F32; - check(node->op!=GGML_OP_RMS_NORM,"generic norm appeared in source graph"); - } - check(explicit_ops==1,"one explicit normalization operation required"); - owner.allocator=ggml_gallocr_new(ggml_backend_get_default_buffer_type(owner.backend)); - check(owner.allocator,"allocator unavailable"); size_t arena=0; - ggml_gallocr_reserve_n_size(owner.allocator,graph,nullptr,nullptr,&arena); - check(arena<=128ULL*1024*1024,"normalization graph exceeds 128 MiB"); - check(ggml_gallocr_reserve(owner.allocator,graph) && ggml_gallocr_alloc_graph(owner.allocator,graph),"allocation failed"); - check(ggml_gallocr_get_buffer_size(owner.allocator,0)<=arena,"allocation exceeds reservation"); - ggml_backend_tensor_set(x,inputs.data(),0,inputs.size()*4); - ggml_backend_tensor_set(w,packed.data(),0,packed.size()*2); - const size_t before=dflash::vision::detail::hip_norm_launches(owner.backend); - check(ggml_backend_graph_compute(owner.backend,graph)==GGML_STATUS_SUCCESS,"normalization graph failed"); - ggml_backend_synchronize(owner.backend); - const size_t after=dflash::vision::detail::hip_norm_launches(owner.backend); - check(after>=before && after-before==1,"actual source normalization dispatch mismatch"); - const size_t lt_launches=dflash::vision::detail::hip_bias_launches(owner.backend); - check(lt_launches==0,"normalization unexpectedly submitted Lt"); - fs::create_directory(out); size_t total=0; - const char *names[]={"scaled","weighted","output"}; - for(int field=0;field<3;++field) { - std::vector actual(inputs.size()); - ggml_backend_tensor_get(outputs[field],actual.data(),0,actual.size()*4); - size_t mismatches=0; - for(size_t i=0;i(actual.data()),actual.size()*4); check(bool(file),"output write failed"); - std::cout< -#include -#include -#include -#include - -using namespace dflash::vision; -static std::vector read_file(const std::string & path) { - std::ifstream f(path,std::ios::binary|std::ios::ate); - if(!f || f.tellg()<0 || size_t(f.tellg())%4) throw std::runtime_error("bad patch file"); - std::vector out(size_t(f.tellg())/4); - f.seekg(0); f.read(reinterpret_cast(out.data()),out.size()*4); - if(!f) throw std::runtime_error("patch file read failed"); - return out; -} -static void save(const std::string & path,const std::vector & values) { - std::ofstream f(path,std::ios::binary); - f.write(reinterpret_cast(values.data()),values.size()*4); - if(!f) throw std::runtime_error("output write failed: "+path); -} -int main(int argc,char ** argv) { - const bool load_only=argc>=3 && std::string(argv[2])=="--load-only"; - if((load_only && argc!=5 && argc!=6) || (!load_only && argc!=8 && argc!=9)) { - std::cerr<<"usage: ds4v_vision_probe mmproj patches.f32 height width output-dir label stages(0|1) [cpu|hip:0|hip:1]\n" - <<" ds4v_vision_probe mmproj --load-only dimension vocabulary [cpu|hip:0|hip:1]\n"; return 2; - } - const std::string device=(load_only?argc==6:argc==9)?argv[argc-1]:"cpu"; - ggml_backend_t backend=nullptr; - if(device=="cpu") { - backend=ggml_backend_cpu_init(); - if(backend) ggml_backend_cpu_set_n_threads(backend,2); - } -#ifdef DS4V_VISION_HIP - else if(device=="hip:0" || device=="hip:1") { - const int index=device.back()-'0'; - if(index>{ - {{48,72},false}, {{3,564},false}, {{6,564},false}, - {{3,3},true}, {{3,561},true}, {{6,561},true}, {{564,3},true}}) { - VisionOutput rejected; - if(runtime.encode({},check.first,rejected,error)) throw std::runtime_error("empty patches accepted"); - const auto expected=check.second?"patch count/shape mismatch":"patch grid exceeds image token budget"; - if(error!=expected) throw std::runtime_error("grid budget check: expected "+std::string(expected)+", got "+error); - } - if(runtime.load(argv[1],backend,4095,129280,error)) throw std::runtime_error("incompatible reload accepted"); - if(!runtime.config() || runtime.weight_bytes()==0) throw std::runtime_error("failed reload destroyed runtime"); - std::vector sentinel_after_reload; - if(!runtime.sentinel(Sentinel::Start,sentinel_after_reload,error)) throw std::runtime_error("failed reload lost sentinels"); - if(runtime.sentinel(static_cast(99),sentinel_after_reload,error)) throw std::runtime_error("invalid sentinel accepted"); - const auto patches=read_file(argv[2]); - PatchGrid grid{std::stoi(argv[3]),std::stoi(argv[4])}; - const std::string output_dir=argv[5],label=argv[6]; - const bool stages=std::stoi(argv[7])!=0; - std::filesystem::create_directories(output_dir); - StageObserver observer; - if(stages) observer=[&](const std::string & name,const std::vector & shape,const std::vector & values) { - save(output_dir+"/"+label+"-"+name+".f32",values); - std::cout<<"stage="< sentinel; - if(!runtime.sentinel(identity,sentinel,error) || sentinel.size()!=4096) throw std::runtime_error("sentinel failure"); - } - std::cout<<"output_shape="<> 16 - return np.where(bits & 0x8000, 0x8000 - (bits & 0x7fff), 0x8000 + bits).astype(np.int32) - ulps = np.abs(ordered_bf16(x)-ordered_bf16(y)) - metrics.update(bf16_ulp_max=int(ulps.max()), bf16_ulp_p99=float(np.percentile(ulps,99)), bf16_within_one_ulp=float(np.mean(ulps<=1))) - results[label][stage if mode == 'original' else mode + '.' + stage] = metrics - print(label, mode, stage, json.dumps(metrics), flush=True) - a.output.write_text(json.dumps(results, indent=2)+'\n') - -hooks=[] -for module, name in [(vit.patch_embed,'patch_embed'), (vit.blocks[0].norm1,'block0.norm1'), - (vit.blocks[0].attn.wqkv,'block0.qkv'), (vit.norm,'features'), - (aligner.w1,'aligner.w1'), (aligner.w2,'embeddings')]: - hooks.append(module.register_forward_hook(lambda m, inp, out, name=name: compare(name,out))) -for i, block in enumerate(vit.blocks): - hooks.append(block.register_forward_hook(lambda m, inp, out, i=i: compare(f'block{i}',out))) -hooks.append(aligner.w1.register_forward_pre_hook(lambda m, inp: compare('unfold',inp[0]))) -hooks.append(aligner.w2.register_forward_pre_hook(lambda m, inp: compare('aligner.gelu',inp[0]))) -original_rotary = vision.apply_rotary -original_sdpa = vision.F.scaled_dot_product_attention -rotary_count = 0 -attention_count = 0 - -def rotary(*args, **kwargs): - global rotary_count - out = original_rotary(*args, **kwargs) - if rotary_count < 2: - compare('block0.q' if rotary_count == 0 else 'block0.k', out) - rotary_count += 1 - return out - -def sdpa(*args, **kwargs): - global attention_count - out = original_sdpa(*args, **kwargs) - if attention_count == 0: - compare('block0.attention',out.transpose(0,1).reshape(out.shape[1],-1)) - attention_count += 1 - return out - -vision.apply_rotary = rotary -vision.F.scaled_dot_product_attention = sdpa -for label in ('corn','carrots'): - results[label] = {} - mode = 'original' - rotary_count = attention_count = 0 - meta = manifest['images'][label] - patches = torch.from_numpy(np.fromfile(a.reference / meta['patches']['file'],np.float32).reshape(meta['patches']['shape'])).to(torch.bfloat16) - with torch.inference_mode(): - features = vit(patches,*meta['vit_grid']) - embeddings = aligner(features,*meta['vit_grid']) - for name,value in [('features',features),('embeddings',embeddings)]: - original = np.fromfile(a.reference / meta[name]['file'],np.float32).reshape(meta[name]['shape']) - assert np.array_equal(original,value.float().numpy()), f'{label} instrumented parent {name} changed' - results[label]['original_fixture_bitwise_match'] = True - a.output.write_text(json.dumps(results,indent=2)+'\n') - - # Isolate kernel/rounding effects: every parent block receives the exact - # native preceding residual, avoiding cumulative differences from earlier blocks. - mode = 'same_input' - cos, sin = vision.get_vision_cos_sin(*meta['vit_grid'], vit.rope_dim, vit.rope_theta) - with torch.inference_mode(): - for i, block in enumerate(vit.blocks): - previous = 'patch_embed' if i == 0 else f'block{i-1}' - native_input = torch.from_numpy(np.fromfile(a.native / f'{label}-{previous}.f32', np.float32).reshape(-1,1024)).to(torch.bfloat16) - rotary_count = attention_count = 0 if i == 0 else 100 - block(native_input,cos,sin) - native_last = torch.from_numpy(np.fromfile(a.native / f'{label}-block31.f32',np.float32).reshape(-1,1024)).to(torch.bfloat16) - vit.norm(native_last) - native_features = torch.from_numpy(np.fromfile(a.native / f'{label}-features.f32',np.float32).reshape(-1,1024)).to(torch.bfloat16) - aligner(native_features,*meta['vit_grid']) - a.output.write_text(json.dumps(results,indent=2)+'\n') diff --git a/server/tools/ds4v_vision/rotary_contract.cpp b/server/tools/ds4v_vision/rotary_contract.cpp deleted file mode 100644 index 1b8d2c9e2..000000000 --- a/server/tools/ds4v_vision/rotary_contract.cpp +++ /dev/null @@ -1,33 +0,0 @@ -#include "deepseek4/deepseek4_vision.h" -#include "ggml-cpu.h" -#include -#include - -using namespace dflash::vision; -static void check(bool ok,const char *message) { if(!ok) throw std::runtime_error(message); } -int main() { - auto backend=ggml_backend_cpu_init(); - try { - check(backend,"CPU backend unavailable"); - check(!detail::hip_rotary_capable(nullptr) && !detail::hip_rotary_capable(backend),"CPU reports HIP rotary capability"); - check(detail::hip_rotary_launches(nullptr)==0 && detail::hip_rotary_launches(backend)==0,"CPU reports HIP rotary dispatch"); - std::vector generic_cos,generic_sin,cpu_cos,cpu_sin; - detail::rotary_tables({23,34},generic_cos,generic_sin); - detail::rotary_tables({23,34},cpu_cos,cpu_sin,backend); - check(generic_cos==cpu_cos && generic_sin==cpu_sin,"CPU backend changed generic rotary tables"); - check(cpu_cos.size()==782*32 && cpu_sin.size()==cpu_cos.size(),"wrong rotary table size"); - for(int i=0;i<32;++i) check(cpu_cos[i]==1.f && cpu_sin[i]==0.f,"zero-position rotary changed"); - for(PatchGrid grid:std::vector{{0,1},{1,0},{-1,1},{1,-1},{1153,1},{1,1153}}) { - bool rejected=false; - try { detail::rotary_tables(grid,cpu_cos,cpu_sin,backend); } - catch(const std::runtime_error &) { rejected=true; } - check(rejected,"invalid rotary grid accepted"); - } - ggml_backend_free(backend); - std::cout<<"PASS: rotary CPU portability and grid contract\n"; - return 0; - } catch(const std::exception &error) { - if(backend) ggml_backend_free(backend); - std::cerr< -#include -#include -#include -#include -#include -#include - -using namespace dflash::vision; -namespace fs=std::filesystem; -static void check(bool ok,const char *message) { if(!ok) throw std::runtime_error(message); } -static std::vector read(const fs::path &path) { - constexpr size_t count=782*32; - check(fs::is_regular_file(path) && fs::file_size(path)==count*4,"invalid rotary source file"); - std::vector values(count); std::ifstream f(path,std::ios::binary); - f.read(reinterpret_cast(values.data()),count*4); check(bool(f),"rotary fixture read failed"); - for(float value:values) check(std::isfinite(value),"nonfinite rotary source"); - return values; -} -static void save(const fs::path &path,const std::vector &values) { - std::ofstream f(path,std::ios::binary); - f.write(reinterpret_cast(values.data()),values.size()*4); - check(bool(f),"rotary output write failed"); -} -int main(int argc,char **argv) { - std::cout<(ggml_backend_reg_get_proc_address(reg,"ggml_backend_hip_vision_rotary_f32")); - check(fill!=nullptr,"HIP rotary fill unavailable"); - float a=123.f,b=456.f; - check(!fill(nullptr,23,34,&a,&b),"null backend accepted"); - for(PatchGrid grid:std::vector{{0,1},{1,0},{-1,1},{1,-1},{1153,1},{1,1153},{1152,1152}}) - check(!fill(backend,grid.height,grid.width,&a,&b),"invalid or oversized direct rotary grid accepted"); - check(!fill(backend,23,34,nullptr,&b) && !fill(backend,23,34,&a,nullptr),"null rotary output accepted"); - check(!fill(backend,23,34,&a,&a),"aliased rotary outputs accepted"); - check(a==123.f && b==456.f && detail::hip_rotary_launches(backend)==0,"rejected rotary call had side effects"); - std::vector cosine,sine; - detail::rotary_tables({23,34},cosine,sine,backend); - check(cosine.size()==expected_cos.size() && sine.size()==expected_sin.size(),"rotary output shape changed"); - check(std::memcmp(cosine.data(),expected_cos.data(),cosine.size()*4)==0 - && std::memcmp(sine.data(),expected_sin.data(),sine.size()*4)==0,"rotary source differs bitwise"); - check(detail::hip_rotary_launches(backend)==1,"wrong rotary preparation count"); - check(detail::hip_bias_launches(backend)==0 && detail::hip_norm_launches(backend)==0,"unexpected graph operation"); - fs::create_directory(out); save(out/"cos.f32",cosine); save(out/"sin.f32",sine); - std::cout<<"source_bitwise_mismatches=0 actual_rotary_launches=1 actual_lt_launches=0 actual_norm_launches=0\n"; - std::cout<<"PASS: HIP rotary tables match original source\n"; - ggml_backend_free(backend); return 0; - } catch(const std::exception &error) { - if(backend) ggml_backend_free(backend); - std::cerr<<"FAIL: "< Date: Mon, 21 Sep 2026 10:48:17 +0200 Subject: [PATCH 086/123] fix(ggml): correct the RPC op-count guard and shorten the vendor note Co-Authored-By: Claude Fable 5.1 --- server/deps/llama.cpp/VENDOR.md | 47 +++++++------------ server/deps/llama.cpp/ggml/include/ggml-rpc.h | 2 +- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/server/deps/llama.cpp/VENDOR.md b/server/deps/llama.cpp/VENDOR.md index cf1b606d8..d9488270d 100644 --- a/server/deps/llama.cpp/VENDOR.md +++ b/server/deps/llama.cpp/VENDOR.md @@ -28,34 +28,19 @@ These changes are limited to `ggml/`. Keep their public declarations in `ggml/include`, avoid DeepSeek-specific policy in generic kernels, and update this provenance when the patch set is moved to `lucebox-ggml`. -## Hub-local DS4V HIP fused-bias linear - -The local `GGML_OP_MUL_MAT_BIAS_BF16` operation is appended after `PAGED_ATTN`; -all existing op numeric values are preserved, while `GGML_OP_COUNT` grows from -105 to 106; the existing RPC header contract advances protocol patch 5 to 6. -The RPC registry does not expose the HIP capability, so vision never sends this -new operation through RPC; RPC supports_op also rejects it locally. Protocol -patch mismatches only warn, so this does not rely on a version handshake to -reject older peers. Rebuild ggml-base, CPU/HIP backends, and consumers together. Do not -mix old shared libraries with this header or serialize the new operation for -an older reader. This is an inference-only extension; CPU compute/backward -reject it, and HIP alone advertises the explicit registry capability. Other -backends are never selected by a generic unknown-op supports default. - -Only DS4V biased linears opt in. Ordinary text MUL_MAT/ADD fusion, unbiased -vision linears, and CPU/NVIDIA vision graph construction are unchanged. The -HIP-only CMake dependency is official hipBLASLt (`roc::hipblaslt`). The Lt -configuration follows the frozen PyTorch revision -`3d3aa833db84eed6b7f5595cb5f162c2f78300a4`: BF16 W/X/bias/output, F32 compute and -scalars, T/N, alpha=1, beta=0, bias epilogue, C=D, one first heuristic with a -76 MiB workspace maximum. There is no algorithm sweep or arithmetic fallback. - -One exact-size 76 MiB workspace and one Lt handle belong to each HIP backend -context that actually uses the operation. An event orders shared workspace -reuse on the actual execution stream; destruction waits for its last use. -The workspace is retained outside the ggml arena and is conservatively -included in VisionRuntime's scratch reservation/report even after arena -release. Graphs containing this operation are capture-ineligible; no global -text graph policy changes. Descriptors are per invocation, not cached. -Qualification is scoped to the Radeon RX 7900 XT and pinned ROCm/PyTorch -reference; availability on other HIP devices is not a qualification claim. +## Hub-local DS4V HIP vision ops + +Four inference-only ops are appended after the existing ones, so every earlier +op keeps its numeric value: `GGML_OP_MUL_MAT_BIAS_BF16`, +`GGML_OP_RMS_NORM_VISION_F32`, `GGML_OP_SOFT_MAX_VISION_F32` and +`GGML_OP_MUL_MAT_VISION_AV_F32`. Only the DS4V vision tower builds them. They +exist on the HIP backend alone; CPU, CUDA and RPC reject them in `supports_op`, +and graphs that contain them are not captured. + +They are compiled only when CMake finds hipBLASLt (`GGML_HIP_DS4V_VISION`). +Without it the HIP backend builds as before and the server refuses `--mmproj`. +The fused BF16 bias matmul keeps one hipBLASLt handle and one 76 MiB workspace +per backend context that uses it, released when the context is destroyed. + +Rebuild ggml-base, the backends and their consumers together; do not mix older +shared libraries with this header. diff --git a/server/deps/llama.cpp/ggml/include/ggml-rpc.h b/server/deps/llama.cpp/ggml/include/ggml-rpc.h index 20dc8a357..2ce9013a2 100644 --- a/server/deps/llama.cpp/ggml/include/ggml-rpc.h +++ b/server/deps/llama.cpp/ggml/include/ggml-rpc.h @@ -11,7 +11,7 @@ extern "C" { #define RPC_PROTO_PATCH_VERSION 8 #ifdef __cplusplus -static_assert(GGML_OP_COUNT == 109, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION"); +static_assert(GGML_OP_COUNT == 110, "GGML_OP_COUNT has changed - update RPC_PROTO_PATCH_VERSION"); #endif #define GGML_RPC_MAX_SERVERS 16 From fa7ff8fd2ba4150dbbfa5e46a09d9393a98c2e5a Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:48:36 +0200 Subject: [PATCH 087/123] docs(ds4v): state the real verification status of image serving Co-Authored-By: Claude Fable 5.1 --- docs/ds4v-image-serving.md | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/docs/ds4v-image-serving.md b/docs/ds4v-image-serving.md index f5b5debeb..f84758cc6 100644 --- a/docs/ds4v-image-serving.md +++ b/docs/ds4v-image-serving.md @@ -1,10 +1,16 @@ # DS4V image serving +**Status: experimental.** The request path works end to end, but the vision +tower has not met its numerical gate and no image-chat acceptance run exists. +See [Verification status](#verification-status) before relying on image output. + The DS4V integration accepts JPEG and PNG images through OpenAI chat -completions when the matching projector is supplied with `--mmproj`. -The current implementation has passed its remote HIP build and CPU integration -checks. Private paired-model HTTP qualification is still pending; a successful -projector export or standalone encoder check does not establish that result. +completions when the matching projector is supplied with `--mmproj`. Without +`--mmproj` nothing in the text serving path changes. + +The server must be built with hipBLASLt available (the `hipblaslt-dev` package +on ROCm). CMake reports `hipBLASLt found: building the DS4V vision ops`; a build +without it refuses `--mmproj` at startup. ## Supported configuration @@ -76,10 +82,18 @@ availability again. KV, saved snapshots, and draft weights remain reflected in that live measurement. Reservations are conservative policy, not a guarantee against unrelated concurrent allocations. -The remote checks include the server unit suite, decoder loader and image-batch -admission tests, synthetic allocation/UMA accounting, preprocessing/codec tests, -and standalone mixed embedding and cancellation tests. Native HIP encoder -comparisons for corn and carrots pass the unchanged feature/embedding gates; -corn also matches the source HIP output exactly and repeats byte for byte. -Full image HTTP behavior, paired runtime resource peaks, and performance require -their separate private serving proof. +## Verification status + +Covered by unit tests in the main build: image transport and request policy, +prompt expansion and ownership, embedding assembly and cancellation, image +spans and the expert budget, plus the decoder loader and image-batch admission +tests in `test_deepseek4_unit`. + +Not yet established: + +- The vision tower misses the fixed 0.9995 feature-cosine gate against the + reference implementation: 0.99906 on the Radeon RX 7900 XT it was developed + on, 0.99823 on CPU. Embeddings pass; features do not. +- No image-chat quality run, and no measurement of paired-GPU memory peaks or + throughput with a projector loaded. +- No other HIP device has been tried. From 633df96a617c117087bdd52264a991a0696a1c4b Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:50:47 +0200 Subject: [PATCH 088/123] fix(ds4v): scan for image markers only with a projector loaded; use model dimensions Text-only DeepSeek4 serving rejected any prompt containing token 129264. The scan now runs only when a projector is loaded. Decoder checks use the loaded model's layer and expert counts instead of fixed DS4V sizes. Co-Authored-By: Claude Fable 5.1 --- server/src/deepseek4/deepseek4_backend.cpp | 37 +++++++++++++--------- server/src/deepseek4/deepseek4_graph.cpp | 11 +++---- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index 6ac548ac3..aa6001224 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -1046,10 +1046,15 @@ bool DeepSeek4Backend::prepare_images( uint64_t context_capacity, uint64_t output_reserve, ImagePromptHandle & payload, std::string & error) const { if (images.empty()) { - for (int32_t token : tokens) { - if (token == 129264 || token < 0 || token >= 129280) { - error = "unbound image marker or invalid token in rendered prompt"; - return false; + // With a projector loaded, a marker left in a text prompt would be + // embedded as an ordinary token. Text-only backends skip the scan. + if (image_capable_) { + const int32_t marker = vision::ImageTokenizerContract{}.marker; + for (int32_t token : tokens) { + if (token == marker || token < 0 || token >= w_.n_vocab) { + error = "unbound image marker or invalid token in rendered prompt"; + return false; + } } } payload.reset(); @@ -1203,17 +1208,17 @@ bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, bool DeepSeek4Backend::load_vision() { if (cfg_.mmproj_path.empty()) return true; - if (w_.n_layer != 43 || w_.n_embd != 4096 || w_.n_vocab != 129280 || - w_.n_expert != 256 || w_.n_expert_used != 6 || w_.n_hash_layer != 3 || w_.n_swa != 128) { - std::fprintf(stderr, "[deepseek4] projector requires the supported DS4V decoder dimensions\n"); - return false; - } + // The projector checks the decoder's width and vocabulary when it loads. + // Here: every layer carries a finite F32[n_expert] image router bias. + std::vector values(size_t(w_.n_expert)); for (const auto & layer : w_.layers) { const auto bias = layer.ffn_gate_bias_vl; - if (!bias || bias->type != GGML_TYPE_F32 || bias->ne[0] != 256 || - ggml_nelements(bias) != 256) return false; - std::array values; - ggml_backend_tensor_get(bias, values.data(), 0, sizeof(values)); + if (!bias || bias->type != GGML_TYPE_F32 || bias->ne[0] != w_.n_expert || + ggml_nelements(bias) != w_.n_expert) { + std::fprintf(stderr, "[deepseek4] --mmproj requires one F32[n_expert] image router bias per layer\n"); + return false; + } + ggml_backend_tensor_get(bias, values.data(), 0, values.size() * sizeof(float)); if (!std::all_of(values.begin(), values.end(), [](float v) { return std::isfinite(v); })) { std::fprintf(stderr, "[deepseek4] nonfinite image router bias\n"); return false; @@ -3149,8 +3154,10 @@ GenerateResult DeepSeek4Backend::generate_from_state( result.fail(GenerateErrorCode::PrefillFailed, error.empty() ? "image materialization failed" : error); return result; } - } else if (std::any_of(req.prompt.begin(), req.prompt.end(), [&](int32_t token) { - return token < 0 || token >= w_.n_vocab || token == 129264; + } else if (image_capable_ && + std::any_of(req.prompt.begin(), req.prompt.end(), [&](int32_t token) { + return token < 0 || token >= w_.n_vocab || + token == vision::ImageTokenizerContract{}.marker; })) { result.fail(GenerateErrorCode::PrefillFailed, "unbound image marker or invalid prompt token"); return result; diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index 9bd978cc0..89d32eda7 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -7809,18 +7809,17 @@ bool deepseek4_validate_image_batch( if (!hybrid || !w.moe_hybrid || !hybrid->materialized_cold_experts || hybrid->cold_backend_kind != MoeHybridColdBackend::Gpu || !hybrid->cold_backend || cache.prefill_mode != PrefillAttentionMode::Sparse || count <= 4 || - count > DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS || w.n_layer != 43 || - w.layers.size() != 43 || cache.layers.size() != 43 || - w.compress_ratios.size() != 43 || hybrid->layers.size() != 43) + count > DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS || + w.layers.size() != size_t(w.n_layer) || cache.layers.size() != size_t(w.n_layer) || + w.compress_ratios.size() != size_t(w.n_layer) || hybrid->layers.size() != size_t(w.n_layer)) return fail("image batch requires the heterogeneous sparse decoder path"); for (int il = 0; il < w.n_layer; ++il) { const auto & layer = w.layers[size_t(il)]; const auto & state = cache.layers[size_t(il)]; const auto bias = layer.ffn_gate_bias_vl; const int ratio = int(w.compress_ratios[size_t(il)]); - if (!bias || bias->type != GGML_TYPE_F32 || bias->ne[0] != 256 || - ggml_nelements(bias) != 256 || !state.raw_kv || - ratio != (il < 2 ? 0 : il % 2 == 0 ? 4 : 128)) + if (!bias || bias->type != GGML_TYPE_F32 || bias->ne[0] != w.n_expert || + ggml_nelements(bias) != w.n_expert || !state.raw_kv) return fail("image decoder is missing a validated router bias or attention state"); if (ratio && (!layer.attn_compressor_ape || !layer.attn_compressor_kv || !layer.attn_compressor_gate || !layer.attn_compressor_norm || !state.comp_kv || From 5c81edc12c9dd8934a01b37d57e4d4588196571c Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:51:48 +0200 Subject: [PATCH 089/123] fix(server): leave image parts alone on backends without image input Every model started answering 400 to any request whose history held an image part. Only an image-capable backend enforces the image request policy now. Co-Authored-By: Claude Fable 5.1 --- server/src/server/image_input.cpp | 10 ++++++---- server/test/test_server_unit.cpp | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/server/src/server/image_input.cpp b/server/src/server/image_input.cpp index a5b4a2ce9..52770d52a 100644 --- a/server/src/server/image_input.cpp +++ b/server/src/server/image_input.cpp @@ -189,15 +189,17 @@ bool prepare_request_images(const nlohmann::json & messages, normalized = nullptr; images.clear(); error.clear(); + // A backend without image input keeps its existing handling of image + // parts: a client whose history holds an image must not start failing. + if (!policy.image_capable) { + normalized = messages; + return true; + } const bool has_images = contains_image_content(messages); if (has_images && !policy.chat_completions) { error = "image input is supported only through /v1/chat/completions image_url parts"; return false; } - if (has_images && !policy.image_capable) { - error = "image input is unavailable for this backend or serving mode; configure a supported --mmproj projector"; - return false; - } if (policy.chat_completions && (has_images || policy.reserve_placeholder)) { nlohmann::json prepared; std::vector extracted; diff --git a/server/test/test_server_unit.cpp b/server/test/test_server_unit.cpp index 79d81a8ab..f2d09e203 100644 --- a/server/test/test_server_unit.cpp +++ b/server/test/test_server_unit.cpp @@ -9135,16 +9135,23 @@ TEST_CASE(ServerUnitFixture, test_http_image_policy_rejects_unconsumed_images_in TEST_CASE(ServerUnitFixture, test_http_image_policy_requires_chat_endpoint_and_effective_capability) { const json messages = json::array({{{"role", "user"}, {"content", json::array({image_transport_part()})}}}); + { + json normalized = "stale"; + std::vector images{{"stale", {1}}}; + std::string error; + TEST_ASSERT(!prepare_request_images(messages, {false, true, true}, normalized, images, error)); + TEST_ASSERT(normalized.is_null() && images.empty()); + TEST_ASSERT(!error.empty()); + } + // Without image capability the messages pass through untouched. for (ImageRequestPolicy policy : { - ImageRequestPolicy{false, true, true}, ImageRequestPolicy{true, false, true}, ImageRequestPolicy{false, false, true}}) { json normalized = "stale"; std::vector images{{"stale", {1}}}; std::string error; - TEST_ASSERT(!prepare_request_images(messages, policy, normalized, images, error)); - TEST_ASSERT(normalized.is_null() && images.empty()); - TEST_ASSERT(!error.empty()); + TEST_ASSERT(prepare_request_images(messages, policy, normalized, images, error)); + TEST_ASSERT(normalized == messages && images.empty()); } json normalized; std::vector images; @@ -9172,6 +9179,6 @@ TEST_CASE(ServerUnitFixture, test_http_image_policy_preserves_text_without_image std::vector images; std::string error; const json forged = json::array({{{"role", "user"}, {"content", DS4_IMAGE_PLACEHOLDER}}}); - TEST_ASSERT(!prepare_request_images(forged, {true, false, true}, normalized, images, error)); + TEST_ASSERT(!prepare_request_images(forged, {true, true, true}, normalized, images, error)); TEST_ASSERT(normalized.is_null() && images.empty()); } From 02b510f48e3e11348dc5bfc17bdaa131422fc931 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:54:10 +0200 Subject: [PATCH 090/123] test(ds4): follow the generalised image-bias loader message Co-Authored-By: Claude Fable 5.1 --- server/tests/test_deepseek4_unit.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index c21e874e1..1b366d07e 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -2404,7 +2404,7 @@ static void test_image_bias_loader_opt_in_contract(ggml_backend_t backend) { else plan.layer_end = 42; DeepSeek4Weights weights; TEST_ASSERT(!load_deepseek4_gguf_partial(path, backend, plan, weights)); - TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find("exactly 43 F32[256]") != std::string::npos, + TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find("one F32[n_expert] image router bias per layer") != std::string::npos, dflash27b_last_error()); TEST_ASSERT(weights.ctx == nullptr && weights.buf == nullptr); free_deepseek4_weights(weights); @@ -2445,7 +2445,7 @@ static void test_image_bias_loader_opt_in_contract(ggml_backend_t backend) { plan.load_ds4_image_bias = true; DeepSeek4Weights weights; TEST_ASSERT(!load_deepseek4_gguf_partial(bad_path, backend, plan, weights)); - TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find("exactly 43 F32[256]") != std::string::npos, + TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find("one F32[n_expert] image router bias per layer") != std::string::npos, dflash27b_last_error()); TEST_ASSERT(weights.ctx == nullptr && weights.buf == nullptr && weights.dense_split_buf == nullptr); free_deepseek4_weights(weights); From 89fd666531bedda5d594220544cab0f9cad4a585 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:55:30 +0200 Subject: [PATCH 091/123] fix(server): run image extraction and redaction only for an image-capable backend Co-Authored-By: Claude Fable 5.1 --- server/src/server/http_server.cpp | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/server/src/server/http_server.cpp b/server/src/server/http_server.cpp index 3faa22b4a..8450f74a7 100644 --- a/server/src/server/http_server.cpp +++ b/server/src/server/http_server.cpp @@ -2433,21 +2433,25 @@ bool HttpServer::handle_model_request(SocketHandle fd, ParsedRequest & req, try { const json & body = req.raw_body; if (!parse_common_request_fields(fd, body, req)) return true; + // Image extraction and redaction apply only to an image-capable + // backend; every other backend sees the request exactly as before. std::vector encoded_images; - json normalized; - std::string extraction_error; - const ImageRequestPolicy image_policy{ - req.format == ApiFormat::OPENAI_CHAT, - config_.image_input_enabled, - config_.arch == "deepseek4"}; - if (!prepare_request_images(req.messages, image_policy, normalized, - encoded_images, extraction_error)) { - send_error(fd, 400, extraction_error); - return true; + if (config_.image_input_enabled) { + json normalized; + std::string extraction_error; + const ImageRequestPolicy image_policy{ + req.format == ApiFormat::OPENAI_CHAT, + config_.image_input_enabled, + config_.arch == "deepseek4"}; + if (!prepare_request_images(req.messages, image_policy, normalized, + encoded_images, extraction_error)) { + send_error(fd, 400, extraction_error); + return true; + } + req.messages = std::move(normalized); + redact_image_urls(req.raw_body); + redact_image_urls(req.messages); } - req.messages = std::move(normalized); - redact_image_urls(req.raw_body); - redact_image_urls(req.messages); const std::vector chat_messages = normalize_chat_messages(req.messages, req.format, tool_memory_); From f9cc9e30d7d6bf69714682946466b5b63e4a517b Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:56:30 +0200 Subject: [PATCH 092/123] fix(ds4v): refuse --mmproj at startup when the build has no vision ops Co-Authored-By: Claude Fable 5.1 --- server/src/deepseek4/deepseek4_backend.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index aa6001224..95435bb9e 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -1208,6 +1208,11 @@ bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, bool DeepSeek4Backend::load_vision() { if (cfg_.mmproj_path.empty()) return true; + if (!vision::detail::hip_bias_workspace(backend_)) { + std::fprintf(stderr, "[deepseek4] --mmproj needs the DS4V vision ops, which this build lacks " + "(hipBLASLt was not found when ggml-hip was configured)\n"); + return false; + } // The projector checks the decoder's width and vocabulary when it loads. // Here: every layer carries a finite F32[n_expert] image router bias. std::vector values(size_t(w_.n_expert)); From 37dfa4d42004bcfd7fb02cca84b5f292552d1fd8 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:58:23 +0200 Subject: [PATCH 093/123] fix(ds4v): ignore the MTP block's image bias in the loader check; drop the ratio-pattern test Co-Authored-By: Claude Fable 5.1 --- server/src/deepseek4/deepseek4_loader.cpp | 2 +- server/tests/test_deepseek4_unit.cpp | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index f1aba6c0f..4fd7aa400 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -1603,7 +1603,7 @@ bool load_deepseek4_gguf_partial(const std::string & path, const char * name = gguf_get_tensor_name(gctx, ti); const int layer = image_bias_layer(name); if (layer < 0) continue; - if (layer >= int(n_layer)) { valid = false; continue; } + if (layer >= int(n_layer)) continue; // the MTP block's bias is not loaded ++counts[size_t(layer)]; const ggml_tensor * tensor = find_tensor(meta_ctx, name); valid = valid && tensor && tensor->type == GGML_TYPE_F32 && diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index 1b366d07e..0dd8a3cda 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -2544,9 +2544,6 @@ static void test_image_batch_admission_before_execution(ggml_backend_t backend) TEST_ASSERT(!validate()); weights.layers[42].ffn_gate_bias_vl = bias; TEST_ASSERT(validate()); - weights.compress_ratios[42] = 128; - TEST_ASSERT(!validate()); - weights.compress_ratios[42] = 4; hybrid.materialized_cold_experts = false; TEST_ASSERT(!validate()); hybrid.materialized_cold_experts = true; From 7d4d7acb89822bfd5db876d84d06ccf6de8fbff6 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 10:59:33 +0200 Subject: [PATCH 094/123] test(ds4): the loader no longer pins the DS4V layer count and vocabulary Co-Authored-By: Claude Fable 5.1 --- server/tests/test_deepseek4_unit.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index 0dd8a3cda..505d366dc 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -2433,12 +2433,8 @@ static void test_image_bias_loader_opt_in_contract(ggml_backend_t backend) { matrix.image_bias_width = 128; matrix.image_bias_rows = 2; invalid.push_back(matrix); - auto wrong_layers = valid; - wrong_layers.block_count = 42; - invalid.push_back(wrong_layers); - auto wrong_vocab = valid; - wrong_vocab.vocab_size = 129279; - invalid.push_back(wrong_vocab); + // Decoder width and vocabulary are checked against the projector's own + // metadata when it loads, not here. for (const auto & options : invalid) { const std::string bad_path = write_deepseek4_loader_fixture(options); TargetLoadPlan plan; From 4196a706b4ab15a6d2a4a846409ac28f005abf12 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 11:13:03 +0200 Subject: [PATCH 095/123] fix(ds4v): check for the vision ops before loading weights; report the loader's reason Co-Authored-By: Claude Fable 5.1 --- server/src/deepseek4/deepseek4_backend.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index 95435bb9e..bca1499ab 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -1208,11 +1208,6 @@ bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, bool DeepSeek4Backend::load_vision() { if (cfg_.mmproj_path.empty()) return true; - if (!vision::detail::hip_bias_workspace(backend_)) { - std::fprintf(stderr, "[deepseek4] --mmproj needs the DS4V vision ops, which this build lacks " - "(hipBLASLt was not found when ggml-hip was configured)\n"); - return false; - } // The projector checks the decoder's width and vocabulary when it loads. // Here: every layer carries a finite F32[n_expert] image router bias. std::vector values(size_t(w_.n_expert)); @@ -2060,9 +2055,14 @@ bool DeepSeek4Backend::init_hybrid_model() { TargetLoadPlan plan; plan.skip_expert_tensors = true; plan.load_ds4_image_bias = !cfg_.mmproj_path.empty(); + if (plan.load_ds4_image_bias && !vision::detail::hip_bias_workspace(backend_)) { + std::fprintf(stderr, "[deepseek4] --mmproj needs the DS4V vision ops, which this build lacks " + "(hipBLASLt was not found when ggml-hip was configured)\n"); + return false; + } if (!load_deepseek4_gguf_partial(cfg_.model_path, backend_, plan, w_)) { - std::fprintf(stderr, "[deepseek4] failed to partially load model for hybrid mode: %s\n", - cfg_.model_path.c_str()); + std::fprintf(stderr, "[deepseek4] failed to partially load model for hybrid mode: %s (%s)\n", + cfg_.model_path.c_str(), dflash27b_last_error()); return false; } From 13a5dae9df30cef73be7cc83e56084a8ed21c301 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 11:24:50 +0200 Subject: [PATCH 096/123] build(ds4v): pass DOWNLOAD_EXTRACT_TIMESTAMP only on CMake >= 3.24 The Docker images configure with CMake 3.22, which read the keyword as part of URL_HASH and failed both the cuda12 and rocm prebuilds. Co-Authored-By: Claude Fable 5.1 --- server/cmake/Ds4vImageCodecs.cmake | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/server/cmake/Ds4vImageCodecs.cmake b/server/cmake/Ds4vImageCodecs.cmake index 40605fa67..960b177b1 100644 --- a/server/cmake/Ds4vImageCodecs.cmake +++ b/server/cmake/Ds4vImageCodecs.cmake @@ -5,6 +5,13 @@ include_guard(GLOBAL) include(ExternalProject) include(FetchContent) +# DOWNLOAD_EXTRACT_TIMESTAMP exists from CMake 3.24; older releases would read +# it as part of URL_HASH. +set(DS4V_EXTRACT_TIMESTAMP) +if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) + set(DS4V_EXTRACT_TIMESTAMP DOWNLOAD_EXTRACT_TIMESTAMP TRUE) +endif() + set(DS4V_JPEG_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/libjpeg-turbo-prefix) set(DS4V_JPEG_ARCHIVE_NAME jpeg) if(MSVC OR CMAKE_C_SIMULATE_ID STREQUAL "MSVC") @@ -16,7 +23,7 @@ file(MAKE_DIRECTORY ${DS4V_JPEG_PREFIX}/include) ExternalProject_Add(libjpeg_turbo_external URL https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.1.4.1/libjpeg-turbo-3.1.4.1.tar.gz URL_HASH SHA256=ecae8008e2cc9ade2f2c1bb9d5e6d4fb73e7c433866a056bd82980741571a022 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE + ${DS4V_EXTRACT_TIMESTAMP} CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${DS4V_JPEG_PREFIX} @@ -38,7 +45,7 @@ add_dependencies(ds4v_libjpeg libjpeg_turbo_external) FetchContent_Declare(lodepng URL https://github.com/lvandeve/lodepng/archive/ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a.tar.gz URL_HASH SHA256=c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803 - DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + ${DS4V_EXTRACT_TIMESTAMP}) FetchContent_MakeAvailable(lodepng) add_library(ds4v_lodepng STATIC ${lodepng_SOURCE_DIR}/lodepng.cpp) target_include_directories(ds4v_lodepng PUBLIC ${lodepng_SOURCE_DIR}) From 84591ebd4d9d159ad435ddd46dd7ad0514653bf0 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 11:37:49 +0200 Subject: [PATCH 097/123] build(ds4v): build the MIX converter only when its source is in the tree Dockerfile and Dockerfile.rocm copy a fixed list of server/ folders that does not include server/tools, so both image builds failed at configure. Co-Authored-By: Claude Fable 5.1 --- server/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index bea621dd6..05e8c62f2 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -384,7 +384,9 @@ add_subdirectory(deps/llama.cpp/ggml EXCLUDE_FROM_ALL) option(DFLASH27B_DS4_MIX_CONVERTER "Build the CPU-only DeepSeek-V4 safetensors to MIX GGUF converter" ON) -if(DFLASH27B_DS4_MIX_CONVERTER) +# The Docker build contexts do not copy server/tools. +if(DFLASH27B_DS4_MIX_CONVERTER AND + EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/tools/ds4_mix_converter/ds4_mix_converter.cpp") add_executable(ds4_mix_converter tools/ds4_mix_converter/ds4_mix_converter.cpp) target_include_directories(ds4_mix_converter PRIVATE From a6e4e729b504521f51b04a0604b368c626d2ff03 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 12:01:36 +0200 Subject: [PATCH 098/123] refactor(vision): move the model-independent image pieces out of deepseek4 JPEG/PNG decoding and the image span helpers move to common/vision with their own limits. The request transport takes the placeholder text from the backend (ModelBackend::image_placeholder) instead of a DeepSeek constant and an arch == "deepseek4" check. The codec build files lose their ds4v names. The decoder gets the unit test it lost with the standalone probe. Co-Authored-By: Claude Fable 5.1 --- docs/ds4v-image-serving.md | 22 +++ server/CMakeLists.txt | 34 +++-- ...decs.NOTICES.md => ImageCodecs.NOTICES.md} | 0 ...s4vImageCodecs.cmake => ImageCodecs.cmake} | 44 +++--- server/src/common/model_backend.h | 3 + .../vision/image_decode.cpp} | 13 +- .../vision/image_decode.h} | 16 +- server/src/common/vision/image_spans.h | 73 +++++++++ server/src/deepseek4/deepseek4_backend.cpp | 2 +- server/src/deepseek4/deepseek4_backend.h | 1 + server/src/deepseek4/deepseek4_image_prompt.h | 3 + server/src/deepseek4/deepseek4_image_spans.h | 52 +------ .../deepseek4/deepseek4_vision_preprocess.h | 18 +-- server/src/server/http_server.cpp | 2 +- server/src/server/image_input.cpp | 24 +-- server/src/server/image_input.h | 6 +- server/test/test_image_decode.cpp | 141 ++++++++++++++++++ ...v_image_input.cpp => test_image_input.cpp} | 31 ++-- server/test/test_server_unit.cpp | 31 ++-- 19 files changed, 368 insertions(+), 148 deletions(-) rename server/cmake/{Ds4vImageCodecs.NOTICES.md => ImageCodecs.NOTICES.md} (100%) rename server/cmake/{Ds4vImageCodecs.cmake => ImageCodecs.cmake} (50%) rename server/src/{deepseek4/deepseek4_vision_decode.cpp => common/vision/image_decode.cpp} (96%) rename server/src/{deepseek4/deepseek4_vision_decode.h => common/vision/image_decode.h} (70%) create mode 100644 server/src/common/vision/image_spans.h create mode 100644 server/test/test_image_decode.cpp rename server/test/{test_ds4v_image_input.cpp => test_image_input.cpp} (77%) diff --git a/docs/ds4v-image-serving.md b/docs/ds4v-image-serving.md index f84758cc6..d4ffaf2cb 100644 --- a/docs/ds4v-image-serving.md +++ b/docs/ds4v-image-serving.md @@ -97,3 +97,25 @@ Not yet established: - No image-chat quality run, and no measurement of paired-GPU memory peaks or throughput with a projector loaded. - No other HIP device has been tried. + +## Code layout + +Shared by every model: + +| Piece | Where | +| --- | --- | +| Reading images out of a request, limits, redaction | `server/src/server/image_input.*` | +| JPEG and PNG decoding to RGB | `server/src/common/vision/image_decode.*`, codecs in `server/cmake/ImageCodecs.cmake` | +| Image positions in a prompt, batches that keep an image whole | `server/src/common/vision/image_spans.h` | +| The backend contract | `supports_images`, `image_placeholder`, `prepare_images` in `server/src/common/model_backend.h`, and `GenerateRequest::images` | + +DS4V only, all under `server/src/deepseek4/`: resizing and patching +(`deepseek4_vision_preprocess`), the vision tower (`deepseek4_vision`), marker +expansion and embedding assembly (`deepseek4_image_prompt`, +`deepseek4_image_assembly`), attention visibility and expert routing for image +rows (`deepseek4_image_policy`), and memory admission +(`deepseek4_image_admission`). + +Another model needs its own preprocessing, tower and prompt expansion, and its +backend implements the three contract methods. Nothing in the HTTP server or in +`common/vision` names a model. diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 05e8c62f2..9f5dd1c86 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -518,7 +518,7 @@ add_library(dflash_common STATIC src/deepseek4/deepseek4_dspark.cpp src/deepseek4/deepseek4_dspark_spec.cpp src/deepseek4/deepseek4_vision.cpp - src/deepseek4/deepseek4_vision_decode.cpp + src/common/vision/image_decode.cpp src/deepseek4/deepseek4_vision_preprocess.cpp src/deepseek4/deepseek4_image_prompt.cpp src/deepseek4/deepseek4_image_assembly.cpp @@ -899,15 +899,14 @@ if(DFLASH27B_ENABLE_BSA) endif() endif() -# Production uses the same pinned decoder sources and options as the accepted -# preprocessing probe. The decoder itself has no codec feature macro. -include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/Ds4vImageCodecs.cmake") -install(FILES cmake/Ds4vImageCodecs.NOTICES.md - DESTINATION share/licenses/ds4v +# JPEG and PNG decoders for image input (common/vision/image_decode). +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ImageCodecs.cmake") +install(FILES cmake/ImageCodecs.NOTICES.md + DESTINATION share/licenses/image-codecs RENAME THIRD_PARTY_NOTICES.md) -install(FILES "${DS4V_JPEG_SOURCE_DIR}/LICENSE.md" - "${DS4V_JPEG_SOURCE_DIR}/README.ijg" - DESTINATION share/licenses/ds4v/libjpeg-turbo) +install(FILES "${IMAGE_CODEC_JPEG_SOURCE_DIR}/LICENSE.md" + "${IMAGE_CODEC_JPEG_SOURCE_DIR}/README.ijg" + DESTINATION share/licenses/image-codecs/libjpeg-turbo) target_link_libraries(dflash_common PUBLIC @@ -916,8 +915,8 @@ target_link_libraries(dflash_common ggml-base nlohmann_json::nlohmann_json PRIVATE - ds4v_libjpeg - ds4v_lodepng + image_codec_jpeg + image_codec_png ${CMAKE_DL_LIBS} ) # OpenMP for parallel MoE expert compute kernel (saturate memory bandwidth). @@ -1888,16 +1887,23 @@ if(DFLASH27B_TESTS) endif() # ─── Unit tests (no GPU, no model files) ──────────────────────────── + # Shared image input: request transport and JPEG/PNG decoding. + add_executable(test_image_input test/test_image_input.cpp src/server/image_input.cpp) + target_include_directories(test_image_input PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(test_image_input PRIVATE nlohmann_json::nlohmann_json) + add_executable(test_image_decode test/test_image_decode.cpp src/common/vision/image_decode.cpp) + target_include_directories(test_image_decode PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(test_image_decode PRIVATE image_codec_jpeg image_codec_png) + list(APPEND _raw_unit_test_targets test_image_input test_image_decode) + # DS4V image units: each test builds only the unit it covers. - foreach(_ds4v_unit assembly input integration policy prompt) + foreach(_ds4v_unit assembly integration policy prompt) add_executable(test_ds4v_image_${_ds4v_unit} test/test_ds4v_image_${_ds4v_unit}.cpp) target_include_directories(test_ds4v_image_${_ds4v_unit} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/deepseek4) list(APPEND _raw_unit_test_targets test_ds4v_image_${_ds4v_unit}) endforeach() target_sources(test_ds4v_image_assembly PRIVATE src/deepseek4/deepseek4_image_assembly.cpp) - target_sources(test_ds4v_image_input PRIVATE src/server/image_input.cpp) - target_link_libraries(test_ds4v_image_input PRIVATE nlohmann_json::nlohmann_json) target_sources(test_ds4v_image_policy PRIVATE src/deepseek4/deepseek4_image_policy.cpp) target_sources(test_ds4v_image_prompt PRIVATE src/deepseek4/deepseek4_image_prompt.cpp diff --git a/server/cmake/Ds4vImageCodecs.NOTICES.md b/server/cmake/ImageCodecs.NOTICES.md similarity index 100% rename from server/cmake/Ds4vImageCodecs.NOTICES.md rename to server/cmake/ImageCodecs.NOTICES.md diff --git a/server/cmake/Ds4vImageCodecs.cmake b/server/cmake/ImageCodecs.cmake similarity index 50% rename from server/cmake/Ds4vImageCodecs.cmake rename to server/cmake/ImageCodecs.cmake index 960b177b1..8a1973b58 100644 --- a/server/cmake/Ds4vImageCodecs.cmake +++ b/server/cmake/ImageCodecs.cmake @@ -1,5 +1,5 @@ -# Pinned JPEG and PNG decoders for DS4V image input. License texts are in -# Ds4vImageCodecs.NOTICES.md and the unmodified upstream archives. +# Pinned JPEG and PNG decoders behind common/vision/image_decode. License texts +# are in ImageCodecs.NOTICES.md and the unmodified upstream archives. include_guard(GLOBAL) include(ExternalProject) @@ -7,26 +7,26 @@ include(FetchContent) # DOWNLOAD_EXTRACT_TIMESTAMP exists from CMake 3.24; older releases would read # it as part of URL_HASH. -set(DS4V_EXTRACT_TIMESTAMP) +set(IMAGE_CODEC_EXTRACT_TIMESTAMP) if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24) - set(DS4V_EXTRACT_TIMESTAMP DOWNLOAD_EXTRACT_TIMESTAMP TRUE) + set(IMAGE_CODEC_EXTRACT_TIMESTAMP DOWNLOAD_EXTRACT_TIMESTAMP TRUE) endif() -set(DS4V_JPEG_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/libjpeg-turbo-prefix) -set(DS4V_JPEG_ARCHIVE_NAME jpeg) +set(IMAGE_CODEC_JPEG_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/libjpeg-turbo-prefix) +set(IMAGE_CODEC_JPEG_ARCHIVE_NAME jpeg) if(MSVC OR CMAKE_C_SIMULATE_ID STREQUAL "MSVC") - set(DS4V_JPEG_ARCHIVE_NAME jpeg-static) + set(IMAGE_CODEC_JPEG_ARCHIVE_NAME jpeg-static) endif() -set(DS4V_JPEG_ARCHIVE - ${DS4V_JPEG_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}${DS4V_JPEG_ARCHIVE_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}) -file(MAKE_DIRECTORY ${DS4V_JPEG_PREFIX}/include) +set(IMAGE_CODEC_JPEG_ARCHIVE + ${IMAGE_CODEC_JPEG_PREFIX}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}${IMAGE_CODEC_JPEG_ARCHIVE_NAME}${CMAKE_STATIC_LIBRARY_SUFFIX}) +file(MAKE_DIRECTORY ${IMAGE_CODEC_JPEG_PREFIX}/include) ExternalProject_Add(libjpeg_turbo_external URL https://github.com/libjpeg-turbo/libjpeg-turbo/releases/download/3.1.4.1/libjpeg-turbo-3.1.4.1.tar.gz URL_HASH SHA256=ecae8008e2cc9ade2f2c1bb9d5e6d4fb73e7c433866a056bd82980741571a022 - ${DS4V_EXTRACT_TIMESTAMP} + ${IMAGE_CODEC_EXTRACT_TIMESTAMP} CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release - -DCMAKE_INSTALL_PREFIX=${DS4V_JPEG_PREFIX} + -DCMAKE_INSTALL_PREFIX=${IMAGE_CODEC_JPEG_PREFIX} -DCMAKE_INSTALL_LIBDIR=lib -DENABLE_SHARED=OFF -DENABLE_STATIC=ON @@ -35,23 +35,23 @@ ExternalProject_Add(libjpeg_turbo_external -DWITH_SIMD=OFF -DWITH_TURBOJPEG=OFF BUILD_COMMAND ${CMAKE_COMMAND} --build --parallel 2 - BUILD_BYPRODUCTS ${DS4V_JPEG_ARCHIVE}) -add_library(ds4v_libjpeg STATIC IMPORTED GLOBAL) -set_target_properties(ds4v_libjpeg PROPERTIES - IMPORTED_LOCATION ${DS4V_JPEG_ARCHIVE} - INTERFACE_INCLUDE_DIRECTORIES ${DS4V_JPEG_PREFIX}/include) -add_dependencies(ds4v_libjpeg libjpeg_turbo_external) + BUILD_BYPRODUCTS ${IMAGE_CODEC_JPEG_ARCHIVE}) +add_library(image_codec_jpeg STATIC IMPORTED GLOBAL) +set_target_properties(image_codec_jpeg PROPERTIES + IMPORTED_LOCATION ${IMAGE_CODEC_JPEG_ARCHIVE} + INTERFACE_INCLUDE_DIRECTORIES ${IMAGE_CODEC_JPEG_PREFIX}/include) +add_dependencies(image_codec_jpeg libjpeg_turbo_external) FetchContent_Declare(lodepng URL https://github.com/lvandeve/lodepng/archive/ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a.tar.gz URL_HASH SHA256=c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803 - ${DS4V_EXTRACT_TIMESTAMP}) + ${IMAGE_CODEC_EXTRACT_TIMESTAMP}) FetchContent_MakeAvailable(lodepng) -add_library(ds4v_lodepng STATIC ${lodepng_SOURCE_DIR}/lodepng.cpp) -target_include_directories(ds4v_lodepng PUBLIC ${lodepng_SOURCE_DIR}) +add_library(image_codec_png STATIC ${lodepng_SOURCE_DIR}/lodepng.cpp) +target_include_directories(image_codec_png PUBLIC ${lodepng_SOURCE_DIR}) # Expose the original JPEG notices for production installation after the # dependency build has downloaded its hash-verified source archive. ExternalProject_Get_Property(libjpeg_turbo_external SOURCE_DIR) -set(DS4V_JPEG_SOURCE_DIR "${SOURCE_DIR}") +set(IMAGE_CODEC_JPEG_SOURCE_DIR "${SOURCE_DIR}") unset(SOURCE_DIR) diff --git a/server/src/common/model_backend.h b/server/src/common/model_backend.h index 52a1c08fa..af7f9fcd4 100644 --- a/server/src/common/model_backend.h +++ b/server/src/common/model_backend.h @@ -134,7 +134,10 @@ struct DaemonIO { struct ModelBackend { virtual ~ModelBackend() = default; + // Image input. A backend that supports it names the text its chat template + // turns into the image marker, and binds decoded images to a rendered prompt. virtual bool supports_images() const { return false; } + virtual std::string image_placeholder() const { return {}; } virtual bool prepare_images(std::vector & tokens, std::vector images, uint64_t context_capacity, diff --git a/server/src/deepseek4/deepseek4_vision_decode.cpp b/server/src/common/vision/image_decode.cpp similarity index 96% rename from server/src/deepseek4/deepseek4_vision_decode.cpp rename to server/src/common/vision/image_decode.cpp index b16920a9d..a391f59f6 100644 --- a/server/src/deepseek4/deepseek4_vision_decode.cpp +++ b/server/src/common/vision/image_decode.cpp @@ -1,4 +1,4 @@ -#include "deepseek4_vision_decode.h" +#include "image_decode.h" #include #include @@ -37,11 +37,16 @@ DecodeStatus validate_decoded( std::uint32_t height, const DecodeLimits & limits, std::size_t & output_bytes) { - const auto status = validate_decoded_dimensions(width, height, limits.decoded); - if (!status) { - return {DecodeError::DecodedTooLarge, status.message}; + if (width == 0 || height == 0) { + return {DecodeError::MalformedImage, "decoded image dimensions must be positive"}; + } + if (width > limits.max_dimension || height > limits.max_dimension) { + return {DecodeError::DecodedTooLarge, "decoded image dimension exceeds the limit"}; } const std::uint64_t pixels = static_cast(width) * height; + if (pixels > limits.max_decoded_pixels) { + return {DecodeError::DecodedTooLarge, "decoded image pixel count exceeds the limit"}; + } if (pixels > std::numeric_limits::max() / 3) { return {DecodeError::DecodedTooLarge, "decoded RGB byte count overflows size_t"}; } diff --git a/server/src/deepseek4/deepseek4_vision_decode.h b/server/src/common/vision/image_decode.h similarity index 70% rename from server/src/deepseek4/deepseek4_vision_decode.h rename to server/src/common/vision/image_decode.h index 4688669d2..9deb03a84 100644 --- a/server/src/deepseek4/deepseek4_vision_decode.h +++ b/server/src/common/vision/image_decode.h @@ -1,7 +1,7 @@ +// JPEG and PNG decoding to 8-bit RGB. Model independent: every vision model +// starts from these pixels and does its own resizing and patching. #pragma once -#include "deepseek4_vision_preprocess.h" - #include #include #include @@ -14,9 +14,19 @@ struct EncodedImageView { std::size_t size = 0; }; +// Checked before any decoded buffer is allocated. struct DecodeLimits { std::size_t max_encoded_bytes = 16ULL * 1024ULL * 1024ULL; - PreprocessLimits decoded; + std::uint64_t max_decoded_pixels = 64ULL * 1024ULL * 1024ULL; + std::uint32_t max_dimension = 65'535; +}; + +// Borrowed row-major RGB, three bytes per pixel. +struct DecodedRgbView { + std::uint32_t width = 0; + std::uint32_t height = 0; + const std::uint8_t * data = nullptr; + std::size_t size = 0; }; struct DecodedRgb { diff --git a/server/src/common/vision/image_spans.h b/server/src/common/vision/image_spans.h new file mode 100644 index 000000000..84b8dd9de --- /dev/null +++ b/server/src/common/vision/image_spans.h @@ -0,0 +1,73 @@ +// Where images sit inside a prompt. Model independent: a backend uses these to +// keep an image inside one prefill batch and to build its attention mask. +#pragma once + +#include +#include +#include +#include + +namespace dflash::vision { + +// Half-open absolute token positions. `block` covers every token the image +// expanded to; `visible` is the part that attends bidirectionally. +struct TokenSpan { + std::uint64_t block_begin = 0; + std::uint64_t visible_begin = 0; + std::uint64_t visible_end = 0; + std::uint64_t block_end = 0; +}; + +// Borrowed, sorted by position, non-overlapping. +struct ImageSpanView { + const TokenSpan * data = nullptr; + size_t size = 0; +}; + +inline const TokenSpan * image_block_at(ImageSpanView spans, uint64_t position) { + for (size_t i = 0; i < spans.size; ++i) { + const auto & span = spans.data[i]; + if (position < span.block_begin) break; + if (position < span.block_end) return &span; + } + return nullptr; +} + +inline bool valid_image_spans(ImageSpanView spans, uint64_t prompt_size, + size_t max_images, uint64_t max_block_tokens) { + if (spans.size > max_images || (spans.size && !spans.data)) return false; + uint64_t previous_end = 0; + for (size_t i = 0; i < spans.size; ++i) { + const auto & span = spans.data[i]; + if (span.block_begin < previous_end || span.block_begin > span.visible_begin || + span.visible_begin >= span.visible_end || span.visible_end > span.block_end || + span.block_end > prompt_size || span.block_end - span.block_begin > max_block_tokens) { + return false; + } + previous_end = span.block_end; + } + return true; +} + +// Largest batch starting at `position` that does not cut an image in two. +// Returns 0 when no such batch fits in `capacity`. +inline int atomic_image_chunk(ImageSpanView spans, uint64_t position, + int proposed, uint64_t remaining, int capacity) { + if (proposed <= 0 || capacity <= 0 || uint64_t(proposed) > remaining || + position > std::numeric_limits::max() - remaining) return 0; + uint64_t end = position + std::min(proposed, capacity); + for (size_t i = 0; i < spans.size; ++i) { + const auto & span = spans.data[i]; + if (span.block_end <= position) continue; + if (span.block_begin < position) return 0; + if (span.block_begin >= end) break; + if (end < span.block_end) { + end = span.block_begin == position ? span.block_end : span.block_begin; + break; + } + } + const uint64_t count = end - position; + return count && count <= remaining && count <= uint64_t(capacity) ? int(count) : 0; +} + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index bca1499ab..20910754c 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -8,7 +8,7 @@ #include "deepseek4_image_budget.h" #include "deepseek4_image_assembly.h" #include "deepseek4_image_admission.h" -#include "deepseek4_vision_decode.h" +#include "../common/vision/image_decode.h" #include "dflash27b.h" #include "deepseek4_snapshot.h" #include "deepseek4_page_layout.h" diff --git a/server/src/deepseek4/deepseek4_backend.h b/server/src/deepseek4/deepseek4_backend.h index ffe3bb51f..b1c7447aa 100644 --- a/server/src/deepseek4/deepseek4_backend.h +++ b/server/src/deepseek4/deepseek4_backend.h @@ -73,6 +73,7 @@ class DeepSeek4Backend : public ModelBackend { // ModelBackend interface void print_ready_banner() const override; bool supports_images() const override { return image_capable_; } + std::string image_placeholder() const override { return vision::DS4V_IMAGE_PLACEHOLDER; } bool prepare_images(std::vector & tokens, std::vector images, uint64_t context_capacity, diff --git a/server/src/deepseek4/deepseek4_image_prompt.h b/server/src/deepseek4/deepseek4_image_prompt.h index 7edb1ed9c..2a6cb8407 100644 --- a/server/src/deepseek4/deepseek4_image_prompt.h +++ b/server/src/deepseek4/deepseek4_image_prompt.h @@ -11,6 +11,9 @@ struct PromptImage { ImagePatchInput input; ImageLayout layout; }; +// Text the DS4V chat template renders for one image; it tokenizes to `marker`. +inline constexpr char DS4V_IMAGE_PLACEHOLDER[] = "<|deepseek_image|>"; + struct ImageTokenizerContract { std::uint32_t vocabulary = 129280; std::int32_t marker = 129264; diff --git a/server/src/deepseek4/deepseek4_image_spans.h b/server/src/deepseek4/deepseek4_image_spans.h index 8c2a04a95..63f307b76 100644 --- a/server/src/deepseek4/deepseek4_image_spans.h +++ b/server/src/deepseek4/deepseek4_image_spans.h @@ -1,57 +1,15 @@ +// DS4V limits for the shared image span helpers. #pragma once -#include "deepseek4_vision_preprocess.h" -#include -#include +#include "../common/vision/image_spans.h" namespace dflash::vision { -struct ImageSpanView { - const TokenSpan * data = nullptr; - size_t size = 0; -}; - -inline const TokenSpan * image_block_at(ImageSpanView spans, uint64_t position) { - for (size_t i = 0; i < spans.size; ++i) { - const auto & span = spans.data[i]; - if (position < span.block_begin) break; - if (position < span.block_end) return &span; - } - return nullptr; -} +inline constexpr size_t DS4V_MAX_IMAGES = 4; +inline constexpr uint64_t DS4V_MAX_IMAGE_BLOCK_TOKENS = 384; inline bool valid_image_spans(ImageSpanView spans, uint64_t prompt_size) { - if (spans.size > 4 || (spans.size && !spans.data)) return false; - uint64_t previous_end = 0; - for (size_t i = 0; i < spans.size; ++i) { - const auto & span = spans.data[i]; - if (span.block_begin < previous_end || span.block_begin > span.visible_begin || - span.visible_begin >= span.visible_end || span.visible_end > span.block_end || - span.block_end > prompt_size || span.block_end - span.block_begin > 384) { - return false; - } - previous_end = span.block_end; - } - return true; -} - -inline int atomic_image_chunk(ImageSpanView spans, uint64_t position, - int proposed, uint64_t remaining, int capacity) { - if (proposed <= 0 || capacity <= 0 || uint64_t(proposed) > remaining || - position > std::numeric_limits::max() - remaining) return 0; - uint64_t end = position + std::min(proposed, capacity); - for (size_t i = 0; i < spans.size; ++i) { - const auto & span = spans.data[i]; - if (span.block_end <= position) continue; - if (span.block_begin < position) return 0; - if (span.block_begin >= end) break; - if (end < span.block_end) { - end = span.block_begin == position ? span.block_end : span.block_begin; - break; - } - } - const uint64_t count = end - position; - return count && count <= remaining && count <= uint64_t(capacity) ? int(count) : 0; + return valid_image_spans(spans, prompt_size, DS4V_MAX_IMAGES, DS4V_MAX_IMAGE_BLOCK_TOKENS); } } // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_vision_preprocess.h b/server/src/deepseek4/deepseek4_vision_preprocess.h index bb24c5cf2..d0c526438 100644 --- a/server/src/deepseek4/deepseek4_vision_preprocess.h +++ b/server/src/deepseek4/deepseek4_vision_preprocess.h @@ -1,5 +1,8 @@ #pragma once +#include "../common/vision/image_decode.h" +#include "../common/vision/image_spans.h" + #include #include #include @@ -28,13 +31,6 @@ struct PreprocessLimits { std::uint64_t max_output_pixels = 16ULL * 1024ULL * 1024ULL; }; -struct DecodedRgbView { - std::uint32_t width = 0; - std::uint32_t height = 0; - const std::uint8_t * data = nullptr; - std::size_t size = 0; -}; - enum class ImageTokenType : std::int64_t { Start = 0, Pad = 1, @@ -43,14 +39,6 @@ enum class ImageTokenType : std::int64_t { End = 4, }; -struct TokenSpan { - // All intervals are half-open absolute token positions. - std::uint64_t block_begin = 0; - std::uint64_t visible_begin = 0; - std::uint64_t visible_end = 0; - std::uint64_t block_end = 0; -}; - struct ResizePlan { std::uint32_t resized_width = 0; std::uint32_t resized_height = 0; diff --git a/server/src/server/http_server.cpp b/server/src/server/http_server.cpp index 8450f74a7..eb10d1e30 100644 --- a/server/src/server/http_server.cpp +++ b/server/src/server/http_server.cpp @@ -2442,7 +2442,7 @@ bool HttpServer::handle_model_request(SocketHandle fd, ParsedRequest & req, const ImageRequestPolicy image_policy{ req.format == ApiFormat::OPENAI_CHAT, config_.image_input_enabled, - config_.arch == "deepseek4"}; + backend_.image_placeholder()}; if (!prepare_request_images(req.messages, image_policy, normalized, encoded_images, extraction_error)) { send_error(fd, 400, extraction_error); diff --git a/server/src/server/image_input.cpp b/server/src/server/image_input.cpp index 52770d52a..d832a6acc 100644 --- a/server/src/server/image_input.cpp +++ b/server/src/server/image_input.cpp @@ -14,20 +14,20 @@ int base64_value(char c) { if (c == '/') return 63; return -1; } -bool reserved_placeholder(std::string_view text) { - return text.find(DS4_IMAGE_PLACEHOLDER) != std::string_view::npos; +bool reserved_placeholder(std::string_view text, std::string_view placeholder) { + return text.find(placeholder) != std::string_view::npos; } void require(bool valid, const char * message) { if (!valid) throw std::invalid_argument(message); } -void validate_message_structure(const nlohmann::json & messages) { +void validate_message_structure(const nlohmann::json & messages, std::string_view placeholder) { std::vector> pending{{&messages, 0}}; while (!pending.empty()) { const auto [value, depth] = pending.back(); pending.pop_back(); require(depth <= 64, "image message nesting exceeds 64 levels"); if (value->is_string()) { - require(!reserved_placeholder(value->get_ref()), + require(!reserved_placeholder(value->get_ref(), placeholder), "text contains the reserved image placeholder"); } else if (value->is_structured()) { for (const auto & child : *value) pending.emplace_back(&child, depth + 1); @@ -95,15 +95,17 @@ bool parse_image_data_url(std::string_view url, EncodedImage & image, } } -bool extract_chat_images(const nlohmann::json & messages, nlohmann::json & normalized, +bool extract_chat_images(const nlohmann::json & messages, std::string_view placeholder, + nlohmann::json & normalized, std::vector & images, std::string & error, const ImageInputLimits & limits) { normalized = nullptr; images.clear(); error.clear(); try { + require(!placeholder.empty(), "the backend names no image placeholder"); require(messages.is_array(), "image messages must be an array"); - validate_message_structure(messages); + validate_message_structure(messages, placeholder); nlohmann::json result = messages; std::vector collected; size_t total_bytes = 0; @@ -122,7 +124,7 @@ bool extract_chat_images(const nlohmann::json & messages, nlohmann::json & norma } require(type != "image" && type != "input_image", "use image_url content parts for images"); if (type != "image_url") continue; - require(!reserved_placeholder(text_segment), "text contains the reserved image placeholder"); + require(!reserved_placeholder(text_segment, placeholder), "text contains the reserved image placeholder"); text_segment.clear(); require(message.value("role", std::string("user")) == "user", "images are supported only in user messages"); require(collected.size() < limits.image_count, "too many images in request"); @@ -145,9 +147,9 @@ bool extract_chat_images(const nlohmann::json & messages, nlohmann::json & norma } total_bytes += decoded.bytes.size(); collected.push_back(std::move(decoded)); - part = {{"type", "text"}, {"text", DS4_IMAGE_PLACEHOLDER}}; + part = {{"type", "text"}, {"text", std::string(placeholder)}}; } - require(!reserved_placeholder(text_segment), "text contains the reserved image placeholder"); + require(!reserved_placeholder(text_segment, placeholder), "text contains the reserved image placeholder"); } normalized = std::move(result); images = std::move(collected); @@ -200,10 +202,10 @@ bool prepare_request_images(const nlohmann::json & messages, error = "image input is supported only through /v1/chat/completions image_url parts"; return false; } - if (policy.chat_completions && (has_images || policy.reserve_placeholder)) { + if (policy.chat_completions) { nlohmann::json prepared; std::vector extracted; - if (!extract_chat_images(messages, prepared, extracted, error, limits)) return false; + if (!extract_chat_images(messages, policy.placeholder, prepared, extracted, error, limits)) return false; if (contains_image_content(prepared)) { error = "images must be user content-array image_url parts"; return false; diff --git a/server/src/server/image_input.h b/server/src/server/image_input.h index e62f313c8..909d5ebe1 100644 --- a/server/src/server/image_input.h +++ b/server/src/server/image_input.h @@ -11,7 +11,6 @@ namespace dflash::common { -inline constexpr char DS4_IMAGE_PLACEHOLDER[] = "<|deepseek_image|>"; struct ImageInputLimits { size_t image_bytes = 16 * 1024 * 1024; @@ -22,12 +21,15 @@ struct ImageInputLimits { struct ImageRequestPolicy { bool chat_completions = false; bool image_capable = false; - bool reserve_placeholder = false; + // Text that stands for one image in the rendered prompt; the backend's + // chat template maps it to the model's image marker. Users may not send it. + std::string placeholder; }; bool parse_image_data_url(std::string_view url, EncodedImage & image, std::string & error, size_t max_bytes = 16 * 1024 * 1024); bool extract_chat_images(const nlohmann::json & messages, + std::string_view placeholder, nlohmann::json & normalized, std::vector & images, std::string & error, diff --git a/server/test/test_image_decode.cpp b/server/test/test_image_decode.cpp new file mode 100644 index 000000000..de5b7dd89 --- /dev/null +++ b/server/test/test_image_decode.cpp @@ -0,0 +1,141 @@ +// JPEG/PNG decoding shared by every vision model. +#include "common/vision/image_decode.h" + +#include +#include +#include + +using namespace dflash::vision; + +static int failures = 0; +static void check(bool condition, const char * message) { + if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); ++failures; } +} + +// 3x2 PNG: red, green, blue / (10,20,30), (200,100,50), white. +const std::vector PNG_3X2 = { + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 3, + 0, 0, 0, 2, 8, 2, 0, 0, 0, 18, 22, 241, 77, 0, 0, 0, 24, 73, 68, 65, + 84, 120, 156, 99, 248, 207, 192, 192, 0, 193, 92, 34, 114, 39, 82, 140, 254, 255, 255, 15, + 0, 60, 25, 7, 149, 239, 194, 198, 216, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, + 130, +}; + +// 16x8 JPEG filled with (40,120,200). +const std::vector JPEG_16X8 = { + 255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, + 255, 219, 0, 67, 0, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 2, 2, 2, 2, + 4, 3, 2, 2, 2, 2, 5, 4, 4, 3, 4, 6, 5, 6, 6, 6, 5, 6, 6, 6, + 7, 9, 8, 6, 7, 9, 7, 6, 6, 8, 11, 8, 9, 10, 10, 10, 10, 10, 6, 8, + 11, 12, 11, 10, 12, 9, 10, 10, 10, 255, 219, 0, 67, 1, 2, 2, 2, 2, 2, 2, + 5, 3, 3, 5, 10, 7, 6, 7, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 255, 192, + 0, 17, 8, 0, 8, 0, 16, 3, 1, 34, 0, 2, 17, 1, 3, 17, 1, 255, 196, 0, + 31, 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 255, 196, 0, 181, 16, 0, 2, 1, 3, 3, + 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125, 1, 2, 3, 0, 4, 17, 5, 18, 33, + 49, 65, 6, 19, 81, 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, 35, 66, 177, 193, 21, + 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, 25, 26, 37, 38, 39, 40, 41, + 42, 52, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, + 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, + 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, + 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, + 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 225, 226, 227, 228, 229, 230, 231, + 232, 233, 234, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 255, 196, 0, 31, 1, 0, 3, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, + 6, 7, 8, 9, 10, 11, 255, 196, 0, 181, 17, 0, 2, 1, 2, 4, 4, 3, 4, 7, + 5, 4, 4, 0, 1, 2, 119, 0, 1, 2, 3, 17, 4, 5, 33, 49, 6, 18, 65, 81, + 7, 97, 113, 19, 34, 50, 129, 8, 20, 66, 145, 161, 177, 193, 9, 35, 51, 82, 240, 21, + 98, 114, 209, 10, 22, 36, 52, 225, 37, 241, 23, 24, 25, 26, 38, 39, 40, 41, 42, 53, + 54, 55, 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, 74, 83, 84, 85, 86, 87, 88, 89, + 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, 118, 119, 120, 121, 122, 130, 131, 132, + 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, 153, 154, 162, 163, 164, 165, 166, + 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, 194, 195, 196, 197, 198, 199, 200, + 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 226, 227, 228, 229, 230, 231, 232, 233, 234, + 242, 243, 244, 245, 246, 247, 248, 249, 250, 255, 218, 0, 12, 3, 1, 0, 2, 17, 3, 17, + 0, 63, 0, 242, 58, 40, 162, 191, 181, 15, 228, 51, 255, 217, +}; + +// 8x8 greyscale JPEG filled with 128. +const std::vector JPEG_GREY_8X8 = { + 255, 216, 255, 224, 0, 16, 74, 70, 73, 70, 0, 1, 1, 0, 0, 1, 0, 1, 0, 0, + 255, 219, 0, 67, 0, 2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 2, 2, 2, 2, + 4, 3, 2, 2, 2, 2, 5, 4, 4, 3, 4, 6, 5, 6, 6, 6, 5, 6, 6, 6, + 7, 9, 8, 6, 7, 9, 7, 6, 6, 8, 11, 8, 9, 10, 10, 10, 10, 10, 6, 8, + 11, 12, 11, 10, 12, 9, 10, 10, 10, 255, 192, 0, 11, 8, 0, 8, 0, 8, 1, 1, + 17, 0, 255, 196, 0, 31, 0, 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 255, 196, 0, 181, 16, + 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 125, 1, 2, 3, 0, + 4, 17, 5, 18, 33, 49, 65, 6, 19, 81, 97, 7, 34, 113, 20, 50, 129, 145, 161, 8, + 35, 66, 177, 193, 21, 82, 209, 240, 36, 51, 98, 114, 130, 9, 10, 22, 23, 24, 25, 26, + 37, 38, 39, 40, 41, 42, 52, 53, 54, 55, 56, 57, 58, 67, 68, 69, 70, 71, 72, 73, + 74, 83, 84, 85, 86, 87, 88, 89, 90, 99, 100, 101, 102, 103, 104, 105, 106, 115, 116, 117, + 118, 119, 120, 121, 122, 131, 132, 133, 134, 135, 136, 137, 138, 146, 147, 148, 149, 150, 151, 152, + 153, 154, 162, 163, 164, 165, 166, 167, 168, 169, 170, 178, 179, 180, 181, 182, 183, 184, 185, 186, + 194, 195, 196, 197, 198, 199, 200, 201, 202, 210, 211, 212, 213, 214, 215, 216, 217, 218, 225, 226, + 227, 228, 229, 230, 231, 232, 233, 234, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 255, 218, + 0, 8, 1, 1, 0, 0, 63, 0, 43, 255, 217, +}; + +static DecodeResult decode(const std::vector & bytes, const DecodeLimits & limits = {}) { + return decode_image({bytes.data(), bytes.size()}, limits); +} + +static bool near(int value, int expected) { return std::abs(value - expected) <= 3; } + +int main() { + { + const auto result = decode(PNG_3X2); + check(bool(result) && result.image.width == 3 && result.image.height == 2, "PNG dimensions"); + const std::vector expected = { + 255, 0, 0, 0, 255, 0, 0, 0, 255, 10, 20, 30, 200, 100, 50, 255, 255, 255}; + check(result.image.pixels == expected, "PNG pixels are exact row-major RGB"); + check(result.image.view().size == expected.size(), "view covers the pixels"); + } + { + const auto result = decode(JPEG_16X8); + check(bool(result) && result.image.width == 16 && result.image.height == 8, "JPEG dimensions"); + bool close = result.image.pixels.size() == 16u * 8u * 3u; + for (size_t i = 0; close && i < result.image.pixels.size(); i += 3) { + close = near(result.image.pixels[i], 40) && near(result.image.pixels[i + 1], 120) && + near(result.image.pixels[i + 2], 200); + } + check(close, "JPEG pixels match the source colour"); + } + { + const auto result = decode(JPEG_GREY_8X8); + bool grey = bool(result) && result.image.pixels.size() == 8u * 8u * 3u; + for (size_t i = 0; grey && i < result.image.pixels.size(); ++i) grey = near(result.image.pixels[i], 128); + check(grey, "greyscale JPEG expands to RGB"); + } + { + check(decode_image({nullptr, 0}).status.code == DecodeError::EmptyInput, "empty input"); + const std::vector text = {'n', 'o', 't', ' ', 'a', 'n', ' ', 'i', 'm', 'a', 'g', 'e'}; + check(decode(text).status.code == DecodeError::UnsupportedFormat, "unknown format"); + auto truncated = PNG_3X2; + truncated.resize(truncated.size() / 2); + const auto broken = decode(truncated); + check(!broken && broken.image.pixels.empty(), "truncated PNG fails without partial output"); + auto cut = JPEG_16X8; + cut.resize(cut.size() / 3); + check(!decode(cut), "truncated JPEG fails"); + } + { + DecodeLimits limits; + limits.max_encoded_bytes = PNG_3X2.size() - 1; + check(decode(PNG_3X2, limits).status.code == DecodeError::EncodedTooLarge, "encoded byte limit"); + limits = {}; + limits.max_decoded_pixels = 5; + check(decode(PNG_3X2, limits).status.code == DecodeError::DecodedTooLarge, "pixel count limit"); + limits = {}; + limits.max_dimension = 2; + check(decode(PNG_3X2, limits).status.code == DecodeError::DecodedTooLarge, "dimension limit"); + limits = {}; + limits.max_decoded_pixels = 6; + check(bool(decode(PNG_3X2, limits)), "exact pixel limit is accepted"); + } + if (failures) { std::fprintf(stderr, "%d failure(s)\n", failures); return 1; } + std::printf("OK\n"); + return 0; +} diff --git a/server/test/test_ds4v_image_input.cpp b/server/test/test_image_input.cpp similarity index 77% rename from server/test/test_ds4v_image_input.cpp rename to server/test/test_image_input.cpp index eb84d2433..b43197974 100644 --- a/server/test/test_ds4v_image_input.cpp +++ b/server/test/test_image_input.cpp @@ -6,6 +6,9 @@ using namespace dflash::common; using json = nlohmann::json; +// Any text works; the transport must not know a model's marker. +static constexpr char IMAGE_PLACEHOLDER[] = ""; + static void check(bool condition, const char * message) { if (!condition) throw std::runtime_error(message); } @@ -43,40 +46,40 @@ int main() { text_part("before"), image_part(jpeg), text_part("between"), image_part(png), text_part("after")})}}}); json normalized; std::vector images; - check(extract_chat_images(messages, normalized, images, error), "ordered images rejected"); + check(extract_chat_images(messages, IMAGE_PLACEHOLDER, normalized, images, error), "ordered images rejected"); check(images.size() == 2 && images[0].mime_type == "image/jpeg" && images[1].mime_type == "image/png", "image order differs"); std::string text; for (const auto & part : normalized[0]["content"]) text += part.at("text").get(); - check(text == "before" + std::string(DS4_IMAGE_PLACEHOLDER) + "between" + DS4_IMAGE_PLACEHOLDER + "after", "text/image placement differs"); + check(text == "before" + std::string(IMAGE_PLACEHOLDER) + "between" + IMAGE_PLACEHOLDER + "after", "text/image placement differs"); check(messages[0]["content"][1]["image_url"]["url"] == jpeg, "input JSON mutated"); check(normalized.dump().find("base64") == std::string::npos, "normalized messages retain bytes"); const json plain = json::array({{{"role", "user"}, {"content", "hello"}}, {{"role", "assistant"}, {"content", "hi"}, {"tool_calls", json::array()}}}); - check(extract_chat_images(plain, normalized, images, error) && normalized == plain && images.empty(), "text-only request changed"); + check(extract_chat_images(plain, IMAGE_PLACEHOLDER, normalized, images, error) && normalized == plain && images.empty(), "text-only request changed"); for (const auto & bad : std::vector{ - json::array({{{"role", "user"}, {"content", DS4_IMAGE_PLACEHOLDER}}}), - json::array({{{"role", "user"}, {"content", json::array({text_part("<|deepseek_"), text_part("image|>")})}}}), - json::array({{{"role", "assistant"}, {"reasoning_content", DS4_IMAGE_PLACEHOLDER}, {"content", "hi"}}}), - json::array({{{"type", "function_call_output"}, {"output", DS4_IMAGE_PLACEHOLDER}}}), - json::array({{{"type", "function_call"}, {"arguments", DS4_IMAGE_PLACEHOLDER}}}), - json::array({{{"role", "assistant"}, {"tool_calls", json::array({{{"function", {{"arguments", DS4_IMAGE_PLACEHOLDER}}}}})}}}), + json::array({{{"role", "user"}, {"content", IMAGE_PLACEHOLDER}}}), + json::array({{{"role", "user"}, {"content", json::array({text_part("")})}}}), + json::array({{{"role", "assistant"}, {"reasoning_content", IMAGE_PLACEHOLDER}, {"content", "hi"}}}), + json::array({{{"type", "function_call_output"}, {"output", IMAGE_PLACEHOLDER}}}), + json::array({{{"type", "function_call"}, {"arguments", IMAGE_PLACEHOLDER}}}), + json::array({{{"role", "assistant"}, {"tool_calls", json::array({{{"function", {{"arguments", IMAGE_PLACEHOLDER}}}}})}}}), json::array({{{"role", "assistant"}, {"content", json::array({image_part(png)})}}}), json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}, {"image_url", 42}}})}}}), json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}, {"image_url", {{"url", 42}}}}})}}}), json::array({{{"role", "user"}, {"content", json::array({{{"type", "image_url"}}})}}})}) { - check(!extract_chat_images(bad, normalized, images, error), "invalid image message accepted"); + check(!extract_chat_images(bad, IMAGE_PLACEHOLDER, normalized, images, error), "invalid image message accepted"); check(images.empty() && normalized.is_null(), "failed extraction retains partial state"); } ImageInputLimits limits; limits.image_count = 1; - check(!extract_chat_images(messages, normalized, images, error, limits), "image count cap ignored"); + check(!extract_chat_images(messages, IMAGE_PLACEHOLDER, normalized, images, error, limits), "image count cap ignored"); limits.image_count = 4; limits.request_bytes = 10; - check(!extract_chat_images(messages, normalized, images, error, limits), "aggregate byte cap ignored"); + check(!extract_chat_images(messages, IMAGE_PLACEHOLDER, normalized, images, error, limits), "aggregate byte cap ignored"); limits.request_bytes = 11; - check(extract_chat_images(messages, normalized, images, error, limits), "exact aggregate cap rejected"); + check(extract_chat_images(messages, IMAGE_PLACEHOLDER, normalized, images, error, limits), "exact aggregate cap rejected"); json deep = plain; json * nested = &deep[0]["metadata"]; @@ -85,7 +88,7 @@ int main() { nested = &(*nested)["nested"]; } *nested = image_part(png); - check(!extract_chat_images(deep, normalized, images, error), "deep metadata accepted before copy"); + check(!extract_chat_images(deep, IMAGE_PLACEHOLDER, normalized, images, error), "deep metadata accepted before copy"); check(error == "image message nesting exceeds 64 levels", "unexpected depth rejection"); redact_image_urls(deep); check((*nested)["image_url"] == "[image omitted]", "deep redaction failed"); diff --git a/server/test/test_server_unit.cpp b/server/test/test_server_unit.cpp index f2d09e203..a16ac51cf 100644 --- a/server/test/test_server_unit.cpp +++ b/server/test/test_server_unit.cpp @@ -8984,6 +8984,9 @@ TEST_CASE(ServerUnitFixture, } namespace { +// Any text works; the transport must not know a model's marker. +constexpr char IMAGE_PLACEHOLDER[] = ""; + json image_transport_part(const char * url = "data:image/png;base64,iVBORw0KGgo=") { return {{"type", "image_url"}, {"image_url", {{"url", url}}}}; } @@ -9021,7 +9024,7 @@ TEST_CASE(ServerUnitFixture, test_image_extraction_normalization_preserves_inter json normalized; std::vector images; std::string error; - TEST_ASSERT(prepare_request_images(messages, {true, true, true}, normalized, images, error)); + TEST_ASSERT(prepare_request_images(messages, {true, true, IMAGE_PLACEHOLDER}, normalized, images, error)); TEST_ASSERT(images.size() == 2); TEST_ASSERT(images[0].mime_type == "image/png"); TEST_ASSERT(images[0].bytes == std::vector({137, 80, 78, 71, 13, 10, 26, 10})); @@ -9031,8 +9034,8 @@ TEST_CASE(ServerUnitFixture, test_image_extraction_normalization_preserves_inter const auto chat = normalize_chat_messages(normalized, ApiFormat::OPENAI_CHAT, memory); TEST_ASSERT(chat.size() == 3); TEST_ASSERT(chat[0].role == "user"); - TEST_ASSERT(chat[0].content == std::string("before ") + DS4_IMAGE_PLACEHOLDER + - " between " + DS4_IMAGE_PLACEHOLDER + " after"); + TEST_ASSERT(chat[0].content == std::string("before ") + IMAGE_PLACEHOLDER + + " between " + IMAGE_PLACEHOLDER + " after"); TEST_ASSERT(chat[1].content == "acknowledged" && chat[2].content == "follow-up"); json retained = {{"messages", messages}, {"metadata", {{"image_url", "private-url"}, {"label", "keep"}}}}; redact_image_urls(retained); @@ -9050,7 +9053,7 @@ TEST_CASE(ServerUnitFixture, test_image_extraction_failure_does_not_publish_part json normalized = {{"stale", true}}; std::vector images{{"stale", {1}}}; std::string error; - TEST_ASSERT(!extract_chat_images(messages, normalized, images, error)); + TEST_ASSERT(!extract_chat_images(messages, IMAGE_PLACEHOLDER, normalized, images, error)); TEST_ASSERT(normalized.is_null()); TEST_ASSERT(images.empty()); TEST_ASSERT(!error.empty()); @@ -9125,7 +9128,7 @@ TEST_CASE(ServerUnitFixture, test_http_image_policy_rejects_unconsumed_images_in json normalized = "stale"; std::vector images{{"stale", {1}}}; std::string error; - TEST_ASSERT(!prepare_request_images(messages, {true, true, true}, normalized, images, error)); + TEST_ASSERT(!prepare_request_images(messages, {true, true, IMAGE_PLACEHOLDER}, normalized, images, error)); TEST_ASSERT(normalized.is_null() && images.empty()); TEST_ASSERT(!error.empty()); TEST_ASSERT(error.find("base64") == std::string::npos); @@ -9139,14 +9142,14 @@ TEST_CASE(ServerUnitFixture, test_http_image_policy_requires_chat_endpoint_and_e json normalized = "stale"; std::vector images{{"stale", {1}}}; std::string error; - TEST_ASSERT(!prepare_request_images(messages, {false, true, true}, normalized, images, error)); + TEST_ASSERT(!prepare_request_images(messages, {false, true, IMAGE_PLACEHOLDER}, normalized, images, error)); TEST_ASSERT(normalized.is_null() && images.empty()); TEST_ASSERT(!error.empty()); } // Without image capability the messages pass through untouched. for (ImageRequestPolicy policy : { - ImageRequestPolicy{true, false, true}, - ImageRequestPolicy{false, false, true}}) { + ImageRequestPolicy{true, false, IMAGE_PLACEHOLDER}, + ImageRequestPolicy{false, false, IMAGE_PLACEHOLDER}}) { json normalized = "stale"; std::vector images{{"stale", {1}}}; std::string error; @@ -9156,7 +9159,7 @@ TEST_CASE(ServerUnitFixture, test_http_image_policy_requires_chat_endpoint_and_e json normalized; std::vector images; std::string error; - TEST_ASSERT(prepare_request_images(messages, {true, true, true}, normalized, images, error)); + TEST_ASSERT(prepare_request_images(messages, {true, true, IMAGE_PLACEHOLDER}, normalized, images, error)); TEST_ASSERT(images.size() == 1); } @@ -9166,9 +9169,9 @@ TEST_CASE(ServerUnitFixture, test_http_image_policy_preserves_text_without_image {{"role", "user"}, {"content", json::array({{{"type", "text"}, {"text", "ordinary text"}}})}} }); for (ImageRequestPolicy policy : { - ImageRequestPolicy{true, false, true}, - ImageRequestPolicy{false, false, true}, - ImageRequestPolicy{true, true, true}}) { + ImageRequestPolicy{true, false, IMAGE_PLACEHOLDER}, + ImageRequestPolicy{false, false, IMAGE_PLACEHOLDER}, + ImageRequestPolicy{true, true, IMAGE_PLACEHOLDER}}) { json normalized; std::vector images; std::string error; @@ -9178,7 +9181,7 @@ TEST_CASE(ServerUnitFixture, test_http_image_policy_preserves_text_without_image json normalized; std::vector images; std::string error; - const json forged = json::array({{{"role", "user"}, {"content", DS4_IMAGE_PLACEHOLDER}}}); - TEST_ASSERT(!prepare_request_images(forged, {true, true, true}, normalized, images, error)); + const json forged = json::array({{{"role", "user"}, {"content", IMAGE_PLACEHOLDER}}}); + TEST_ASSERT(!prepare_request_images(forged, {true, true, IMAGE_PLACEHOLDER}, normalized, images, error)); TEST_ASSERT(normalized.is_null() && images.empty()); } From 57fe97d8881b00e8433af40f9e0cdd7fb4a4e70d Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 12:20:04 +0200 Subject: [PATCH 099/123] feat(ds4v): load the image router bias from published GGUFs llama.cpp conversions name it blk.N.exp_probs_b_vl.bias; the loader only knew the source checkpoint's layers.N.ffn.gate.bias_vl. Both spellings load now. Co-Authored-By: Claude Fable 5.1 --- server/src/deepseek4/deepseek4_loader.cpp | 29 +++++++++++++++-------- server/tests/test_deepseek4_unit.cpp | 15 ++++++++++-- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index 4fd7aa400..fe77cc319 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -29,6 +29,7 @@ #include // SIZE_MAX, used by the portable checked-size helpers below #include #include +#include #include #include #include @@ -236,17 +237,25 @@ static bool is_expert_tensor(const char * name) { std::strstr(name, "ffn_down_exps") != nullptr; } +// Layer index of an image router bias, or -1. Two spellings exist: the source +// checkpoint's "layers.N.ffn.gate.bias_vl" (kept by our converter) and +// llama.cpp's "blk.N.exp_probs_b_vl.bias" (published GGUFs). Leading zeros and +// trailing text are rejected. static int image_bias_layer(const char * name) { - constexpr const char * prefix = "layers."; - if (std::strncmp(name, prefix, 7) != 0) return -1; - const char * number = name + 7; - if (*number < '0' || *number > '9') return -1; - char * suffix = nullptr; - const long layer = std::strtol(number, &suffix, 10); - if (layer < 0 || layer > std::numeric_limits::max() || - std::strcmp(suffix, ".ffn.gate.bias_vl") != 0 || - std::string(name) != "layers." + std::to_string(layer) + ".ffn.gate.bias_vl") return -1; - return int(layer); + for (const auto & [prefix, suffix] : {std::pair + {"layers.", ".ffn.gate.bias_vl"}, {"blk.", ".exp_probs_b_vl.bias"}}) { + const size_t prefix_len = std::strlen(prefix); + if (std::strncmp(name, prefix, prefix_len) != 0) continue; + const char * number = name + prefix_len; + if (*number < '0' || *number > '9') return -1; + char * rest = nullptr; + const long layer = std::strtol(number, &rest, 10); + if (layer < 0 || layer > std::numeric_limits::max() || + std::strcmp(rest, suffix) != 0 || + std::string(name) != prefix + std::to_string(layer) + suffix) return -1; + return int(layer); + } + return -1; } static bool should_keep_ds4_tensor(const char * name, diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index 505d366dc..6a4fc29a6 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -244,6 +244,7 @@ struct DeepSeek4FixtureOptions { int image_bias_width = 256; int image_bias_rows = 1; bool add_mtp_image_bias = false; + bool llama_cpp_image_bias_names = false; // "blk.N.exp_probs_b_vl.bias" }; static std::string make_temp_gguf_path(const char * prefix) { @@ -324,7 +325,9 @@ static std::string write_deepseek4_loader_fixture(const DeepSeek4FixtureOptions malformed ? opts.image_bias_type : GGML_TYPE_F32, malformed ? opts.image_bias_width : 256, malformed ? opts.image_bias_rows : 1); - const std::string name = "layers." + std::to_string(layer) + ".ffn.gate.bias_vl"; + const std::string name = opts.llama_cpp_image_bias_names + ? "blk." + std::to_string(layer) + ".exp_probs_b_vl.bias" + : "layers." + std::to_string(layer) + ".ffn.gate.bias_vl"; ggml_set_name(bias, name.c_str()); std::memset(bias->data, 0, ggml_nbytes(bias)); if (bias->type == GGML_TYPE_F32) { @@ -2370,6 +2373,9 @@ static void test_image_bias_loader_opt_in_contract(ggml_backend_t backend) { valid.vocab_size = 129280; valid.image_biases = true; valid.add_mtp_image_bias = true; + // Both spellings load: the source checkpoint's and llama.cpp's. + for (bool llama_cpp_names : {false, true}) { + valid.llama_cpp_image_bias_names = llama_cpp_names; const std::string path = write_deepseek4_loader_fixture(valid); for (bool enabled : {false, true}) { TargetLoadPlan plan; @@ -2391,12 +2397,17 @@ static void test_image_bias_loader_opt_in_contract(ggml_backend_t backend) { } } if (weights.ctx) { - const auto mtp = ggml_get_tensor(weights.ctx, "layers.43.ffn.gate.bias_vl"); + const auto mtp = ggml_get_tensor(weights.ctx, llama_cpp_names + ? "blk.43.exp_probs_b_vl.bias" : "layers.43.ffn.gate.bias_vl"); TEST_ASSERT(mtp == nullptr || (mtp->buffer == nullptr && mtp->data == nullptr)); } } free_deepseek4_weights(weights); } + unlink(path.c_str()); + } + valid.llama_cpp_image_bias_names = false; + const std::string path = write_deepseek4_loader_fixture(valid); for (int boundary : {0, 1}) { TargetLoadPlan plan; plan.load_ds4_image_bias = true; From 8544d76af71d93b6e77eaa2fb731e6da9efb4c5c Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 12:55:38 +0200 Subject: [PATCH 100/123] feat(ds4): load llama.cpp-converted DeepSeek4 GGUFs without deepseek4.vocab_size Published conversions imply the vocabulary size from tokenizer.ggml.tokens. With this and the image-bias alias, ggml-org's DeepSeek-V4-Flash-Vision-Exp Q2_K_S loads and serves text and images on an R9700 + Strix Halo pair. Co-Authored-By: Claude Fable 5.1 --- server/src/deepseek4/deepseek4_loader.cpp | 12 +++++++++--- server/tests/test_deepseek4_unit.cpp | 5 +++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index fe77cc319..89d0ed693 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -1453,7 +1453,6 @@ bool load_deepseek4_gguf_partial(const std::string & path, static const char * kRequiredU32Keys[] = { "deepseek4.block_count", "deepseek4.embedding_length", - "deepseek4.vocab_size", "deepseek4.attention.head_count", "deepseek4.attention.head_count_kv", "deepseek4.attention.key_length", @@ -1485,7 +1484,14 @@ bool load_deepseek4_gguf_partial(const std::string & path, // ── Read hyperparameters ──────────────────────────────────────────── const uint32_t n_layer = get_u32_or(gctx, "deepseek4.block_count", 43); const uint32_t n_embd = get_u32_or(gctx, "deepseek4.embedding_length", 4096); - const uint32_t n_vocab = get_u32_or(gctx, "deepseek4.vocab_size", 129280); + // llama.cpp conversions carry no vocab_size key; the token list has the size. + uint32_t n_vocab = get_u32_or(gctx, "deepseek4.vocab_size", 0); + if (n_vocab == 0) { + const int64_t tokens_key = gguf_find_key(gctx, "tokenizer.ggml.tokens"); + if (tokens_key >= 0 && gguf_get_kv_type(gctx, tokens_key) == GGUF_TYPE_ARRAY) { + n_vocab = (uint32_t) gguf_get_arr_n(gctx, tokens_key); + } + } const uint32_t n_head = get_u32_or(gctx, "deepseek4.attention.head_count", 64); const uint32_t n_head_kv = get_u32_or(gctx, "deepseek4.attention.head_count_kv", 1); const uint32_t head_dim = get_u32_or(gctx, "deepseek4.attention.key_length", 512); @@ -1520,7 +1526,7 @@ bool load_deepseek4_gguf_partial(const std::string & path, const float swiglu_clamp = get_f32_or(gctx, "deepseek4.swiglu_clamp_exp", 10.0f); if (n_vocab == 0) { - set_last_error("deepseek4.vocab_size must be > 0"); + set_last_error("no vocabulary size: need deepseek4.vocab_size or tokenizer.ggml.tokens"); gguf_free(gctx); if (meta_ctx) ggml_free(meta_ctx); return false; diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index 6a4fc29a6..bf2a671db 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -2266,8 +2266,9 @@ static void test_loader_rejects_missing_required_metadata(ggml_backend_t backend DeepSeek4Weights weights; const bool ok = load_deepseek4_gguf(path, backend, weights); TEST_ASSERT(!ok); + // The fixture has no tokenizer token list either, so no size can be derived. TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find( - "missing required key: deepseek4.vocab_size") != std::string::npos, + "no vocabulary size") != std::string::npos, dflash27b_last_error()); free_deepseek4_weights(weights); unlink(path.c_str()); @@ -2304,7 +2305,7 @@ static void test_loader_rejects_zero_vocab_size(ggml_backend_t backend) { const bool ok = load_deepseek4_gguf(path, backend, weights); TEST_ASSERT(!ok); TEST_ASSERT_MSG(std::string(dflash27b_last_error()).find( - "deepseek4.vocab_size must be > 0") != std::string::npos, + "no vocabulary size") != std::string::npos, dflash27b_last_error()); free_deepseek4_weights(weights); unlink(path.c_str()); From b91eea9150db794a6e453972a52d5f591c892c19 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 14:06:18 +0200 Subject: [PATCH 101/123] fix(ds4v): count the GPU driver's page pool in the image memory admission After any GPU run the kernel's TTM pool keeps the freed GTT pages for reuse and /proc/meminfo leaves them out of MemAvailable. On a Strix Halo + R9700 box the pool held 61 GiB, so restarting the server with --mmproj failed admission (cold required/free 73.3/58.4 GiB) although the memory was there. The pool is only visible to root, so it is estimated from RAM that meminfo attributes to nothing minus the live GTT reported by amdgpu sysfs; it tracks the kernel counter within 0.8 GiB with the pool empty, full, and with 88 GiB of buffers live. Host-shared devices and the host figure are credited with it. Co-Authored-By: Claude Fable 5.1 --- server/CMakeLists.txt | 5 +- server/src/common/gpu_page_pool.cpp | 80 +++++++++++++++++++ server/src/common/gpu_page_pool.h | 28 +++++++ .../deepseek4/deepseek4_image_admission.cpp | 20 +++-- server/test/test_gpu_page_pool.cpp | 52 ++++++++++++ 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 server/src/common/gpu_page_pool.cpp create mode 100644 server/src/common/gpu_page_pool.h create mode 100644 server/test/test_gpu_page_pool.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 9f5dd1c86..765876b8b 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -518,6 +518,7 @@ add_library(dflash_common STATIC src/deepseek4/deepseek4_dspark.cpp src/deepseek4/deepseek4_dspark_spec.cpp src/deepseek4/deepseek4_vision.cpp + src/common/gpu_page_pool.cpp src/common/vision/image_decode.cpp src/deepseek4/deepseek4_vision_preprocess.cpp src/deepseek4/deepseek4_image_prompt.cpp @@ -1894,7 +1895,9 @@ if(DFLASH27B_TESTS) add_executable(test_image_decode test/test_image_decode.cpp src/common/vision/image_decode.cpp) target_include_directories(test_image_decode PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(test_image_decode PRIVATE image_codec_jpeg image_codec_png) - list(APPEND _raw_unit_test_targets test_image_input test_image_decode) + add_executable(test_gpu_page_pool test/test_gpu_page_pool.cpp src/common/gpu_page_pool.cpp) + target_include_directories(test_gpu_page_pool PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + list(APPEND _raw_unit_test_targets test_image_input test_image_decode test_gpu_page_pool) # DS4V image units: each test builds only the unit it covers. foreach(_ds4v_unit assembly integration policy prompt) diff --git a/server/src/common/gpu_page_pool.cpp b/server/src/common/gpu_page_pool.cpp new file mode 100644 index 000000000..653bc0a83 --- /dev/null +++ b/server/src/common/gpu_page_pool.cpp @@ -0,0 +1,80 @@ +#include "gpu_page_pool.h" + +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#endif + +namespace dflash::common { + +namespace { + +constexpr uint64_t OTHER_DRIVERS_MARGIN = 1ULL << 30; + +// Fields that together describe every page /proc/meminfo can attribute. +constexpr const char * ACCOUNTED[] = { + "MemFree", "Buffers", "Cached", "SwapCached", "AnonPages", "Slab", + "KernelStack", "PageTables", "SecPageTables", "Percpu", "Bounce", +}; + +#if defined(__linux__) +uint64_t live_gpu_host_bytes() { + uint64_t total = 0; + DIR * dir = opendir("/sys/class/drm"); + if (!dir) return 0; + while (const dirent * entry = readdir(dir)) { + const std::string name = entry->d_name; + // card0, card1, ... but not the connector entries such as card0-DP-1. + if (name.rfind("card", 0) != 0 || name.find('-') != std::string::npos) continue; + std::ifstream used("/sys/class/drm/" + name + "/device/mem_info_gtt_used"); + uint64_t bytes = 0; + if (used >> bytes) total += bytes; + } + closedir(dir); + return total; +} +#endif + +} // namespace + +uint64_t reclaimable_gpu_page_pool_bytes(const char * meminfo_text, uint64_t live_gpu_host_bytes) { + if (!meminfo_text) return 0; + uint64_t total = 0, accounted = 0, huge_pages = 0, huge_page_kb = 0; + std::istringstream lines(meminfo_text); + std::string line; + while (std::getline(lines, line)) { + const size_t colon = line.find(':'); + if (colon == std::string::npos) continue; + const std::string key = line.substr(0, colon); + const uint64_t value = std::strtoull(line.c_str() + colon + 1, nullptr, 10); + if (key == "MemTotal") total = value; + else if (key == "HugePages_Total") huge_pages = value; + else if (key == "Hugepagesize") huge_page_kb = value; + else for (const char * field : ACCOUNTED) if (key == field) accounted += value; + } + accounted += huge_pages * huge_page_kb; + if (total <= accounted) return 0; + const uint64_t unattributed = (total - accounted) * 1024; + const uint64_t in_use = live_gpu_host_bytes + OTHER_DRIVERS_MARGIN; + return unattributed > in_use ? unattributed - in_use : 0; +} + +uint64_t reclaimable_gpu_page_pool_bytes() { +#if defined(__linux__) + std::ifstream input("/proc/meminfo"); + if (!input) return 0; + std::stringstream text; + text << input.rdbuf(); + return reclaimable_gpu_page_pool_bytes(text.str().c_str(), live_gpu_host_bytes()); +#else + return 0; +#endif +} + +} // namespace dflash::common diff --git a/server/src/common/gpu_page_pool.h b/server/src/common/gpu_page_pool.h new file mode 100644 index 000000000..f036e5409 --- /dev/null +++ b/server/src/common/gpu_page_pool.h @@ -0,0 +1,28 @@ +// Memory the GPU driver is holding for reuse. +// +// When a process frees GPU buffers that live in system RAM (GTT, and every +// allocation on an integrated GPU), the kernel's TTM layer keeps the pages in a +// pool instead of returning them. The next GPU allocation is served from that +// pool first, and the kernel shrinks it under memory pressure. /proc/meminfo +// does not count the pool in MemAvailable, so after one large run MemAvailable +// can read tens of GiB low even though that memory is there for the taking. +// +// The pool size itself is only visible to root (debugfs). This estimates it from +// what any process can read: RAM that /proc/meminfo attributes to nothing, +// minus the GPU buffers that are live right now (amdgpu sysfs). Measured on a +// Strix Halo + R9700 box it tracks the kernel counter within 0.8 GiB with the +// pool empty, full (61 GiB), and while 88 GiB of GPU buffers are live. +#pragma once + +#include + +namespace dflash::common { + +// 0 when nothing can be read (non-Linux, no amdgpu). The result already has a +// 1 GiB margin taken off for pages owned by other drivers. +uint64_t reclaimable_gpu_page_pool_bytes(); + +// Same estimate from caller-supplied text, for tests. +uint64_t reclaimable_gpu_page_pool_bytes(const char * meminfo_text, uint64_t live_gpu_host_bytes); + +} // namespace dflash::common diff --git a/server/src/deepseek4/deepseek4_image_admission.cpp b/server/src/deepseek4/deepseek4_image_admission.cpp index d1daf4ebb..8734046cc 100644 --- a/server/src/deepseek4/deepseek4_image_admission.cpp +++ b/server/src/deepseek4/deepseek4_image_admission.cpp @@ -1,6 +1,7 @@ #include "deepseek4_image_admission.h" #include "deepseek4_internal.h" +#include "common/gpu_page_pool.h" #include "common/moe_hybrid_placement.h" #include "common/moe_hybrid_types.h" #include "ggml-backend.h" @@ -123,19 +124,26 @@ bool host_available(uint64_t & bytes, std::string & error) { !mul(kb, 1024, bytes)) return fail(error, "invalid host MemAvailable"); found = true; } - return found || fail(error, "host MemAvailable is missing"); + if (!found) return fail(error, "host MemAvailable is missing"); + // MemAvailable leaves out pages the GPU driver holds for reuse. + if (!add(bytes, common::reclaimable_gpu_page_pool_bytes())) return fail(error, "host availability overflow"); + return true; #else (void) bytes; return fail(error, "host admission currently requires Linux MemAvailable"); #endif } -bool device_free(ggml_backend_t backend, uint64_t & available, std::string & error) { +bool device_free(ggml_backend_t backend, ImageMemoryDomain domain, uint64_t & available, std::string & error) { size_t free = 0, total = 0; ggml_backend_dev_memory(ggml_backend_get_device(backend), &free, &total); if (!total) return fail(error, "device memory query returned no capacity"); // GGML's UMA query can legitimately report free RAM above dedicated total. available = free; + // A host-shared device reports MemAvailable, which leaves out the pages the + // GPU driver holds for reuse. Dedicated VRAM has no such pool. + if (domain == ImageMemoryDomain::HostShared && + !add(available, common::reclaimable_gpu_page_pool_bytes())) return fail(error, "device availability overflow"); return true; } } // namespace @@ -272,8 +280,8 @@ bool check_deepseek4_image_admission( reserves.cold_domain == ImageMemoryDomain::Unknown) { return fail(error, "actual owner host-memory sharing must be classified before admission"); } - if (!device_free(primary, out.primary_free_bytes, error) || - !device_free(cold, out.cold_free_bytes, error) || + if (!device_free(primary, reserves.primary_domain, out.primary_free_bytes, error) || + !device_free(cold, reserves.cold_domain, out.cold_free_bytes, error) || !host_available(out.host_available_bytes, error)) return false; const ImageStorageEstimate storage = out.storage; const uint64_t activation_bytes = out.cold_activation_estimate_bytes; @@ -307,8 +315,8 @@ bool check_deepseek4_image_runtime_admission( if (!cold_buft || !owner_activation_estimate(config, reserves.max_chunk_tokens, ggml_backend_buft_get_alignment(cold_buft), activation_bytes, error)) return false; ImageMemorySnapshot snapshot; - if (!device_free(primary, snapshot.primary_free_bytes, error) || - !device_free(cold, snapshot.cold_free_bytes, error) || + if (!device_free(primary, reserves.primary_domain, snapshot.primary_free_bytes, error) || + !device_free(cold, reserves.cold_domain, snapshot.cold_free_bytes, error) || !host_available(snapshot.host_available_bytes, error)) return false; ImageAdmissionReserves runtime_reserves = reserves; runtime_reserves.host_loader_overhead_bytes = 0; diff --git a/server/test/test_gpu_page_pool.cpp b/server/test/test_gpu_page_pool.cpp new file mode 100644 index 000000000..640dcee06 --- /dev/null +++ b/server/test/test_gpu_page_pool.cpp @@ -0,0 +1,52 @@ +// The estimate of GPU driver pages that MemAvailable does not count. +#include "common/gpu_page_pool.h" + +#include + +using dflash::common::reclaimable_gpu_page_pool_bytes; + +static int failures = 0; +static void check(bool condition, const char * message) { + if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); ++failures; } +} + +int main() { + constexpr uint64_t GiB = 1ULL << 30; + // 100,000,000 kB total, 31,500,000 kB attributed -> 68,500,000 kB unattributed. + const char * meminfo = + "MemTotal: 100000000 kB\n" + "MemFree: 10000000 kB\n" + "MemAvailable: 28000000 kB\n" + "Buffers: 0 kB\n" + "Cached: 20000000 kB\n" + "AnonPages: 1000000 kB\n" + "Shmem: 300000 kB\n" // already inside Cached, must not be counted twice + "Slab: 500000 kB\n" + "HugePages_Total: 0\n" + "Hugepagesize: 2048 kB\n"; + const uint64_t unattributed = 68500000ULL * 1024; + + check(reclaimable_gpu_page_pool_bytes(meminfo, 0) == unattributed - GiB, + "idle GPU: everything unattributed, less the margin for other drivers"); + check(reclaimable_gpu_page_pool_bytes(meminfo, 5 * GiB) == unattributed - 6 * GiB, + "live GPU buffers are in use, not reclaimable"); + check(reclaimable_gpu_page_pool_bytes(meminfo, unattributed) == 0, + "all unattributed memory is live GPU buffers"); + check(reclaimable_gpu_page_pool_bytes(meminfo, unattributed + 9 * GiB) == 0, "never negative"); + + const char * huge = + "MemTotal: 100000000 kB\n" + "MemFree: 10000000 kB\n" + "HugePages_Total: 20000\n" + "Hugepagesize: 2048 kB\n"; // 40,960,000 kB of huge pages are attributed + check(reclaimable_gpu_page_pool_bytes(huge, 0) == (100000000ULL - 10000000 - 40960000) * 1024 - GiB, + "huge pages count as attributed"); + + check(reclaimable_gpu_page_pool_bytes("MemFree: 5 kB\n", 0) == 0, "no MemTotal, no estimate"); + check(reclaimable_gpu_page_pool_bytes("", 0) == 0 && reclaimable_gpu_page_pool_bytes(nullptr, 0) == 0, + "empty input"); + + if (failures) { std::fprintf(stderr, "%d failure(s)\n", failures); return 1; } + std::printf("OK\n"); + return 0; +} From f023a8ab7d524d655609f2c26022387e232b1def Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 14:07:47 +0200 Subject: [PATCH 102/123] build(docker): build the DS4V vision ops in the ROCm image; keep the admission test machine independent Co-Authored-By: Claude Fable 5.1 --- Dockerfile.rocm | 5 ++++- server/tests/test_deepseek4_unit.cpp | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Dockerfile.rocm b/Dockerfile.rocm index c5afbdd2f..0b94d4898 100644 --- a/Dockerfile.rocm +++ b/Dockerfile.rocm @@ -46,7 +46,8 @@ ENV PATH=/opt/rocm/bin:/opt/rocm/lib/llvm/bin:${PATH} # hipCUB/rocPRIM: ggml's ROCm argsort/top-k kernels use their headers for the # large-N device-sort path. rocWMMA is also required by the DS4 indexed # prefill kernel. The ROCm apt repository ships these headers separately from -# the base HIP toolchain. +# the base HIP toolchain. hipBLASLt is optional for ggml-hip: with its -dev +# package present the DS4V vision ops are built, without it --mmproj is refused. # (deps/llama.cpp/ggml/src/ggml-hip/CMakeLists.txt does find_package(hipblas) # for its BLAS matmul path). The rocm/dev-ubuntu base ships the HIP toolchain # but NOT the math libs, so they are installed explicitly from the ROCm apt @@ -59,6 +60,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ git \ git-lfs \ hipblas-dev \ + hipblaslt-dev \ hipcub-dev \ libcurl4-openssl-dev \ ninja-build \ @@ -151,6 +153,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates \ curl \ hipblas \ + hipblaslt \ libgomp1 \ pciutils \ rocblas \ diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index bf2a671db..e78dd85ab 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -1743,10 +1743,12 @@ static void test_image_storage_admission_metadata() { config.materialize_cold_experts = true; // Actual wrapper sees the fake device's exhausted snapshot. It cannot pass - // regardless of the machine's MemAvailable; no positive case reads /proc. + // regardless of the machine's memory; no positive case reads /proc. The + // fake is dedicated memory: a host-shared device is also credited with the + // GPU driver's page pool, which depends on the machine running the test. ImageAdmissionReserves reserves; reserves.primary_domain = ImageMemoryDomain::Dedicated; - reserves.cold_domain = ImageMemoryDomain::HostShared; + reserves.cold_domain = ImageMemoryDomain::Dedicated; reserves.cold_runtime_reservation_bytes = 2ULL * 1024 * 1024 * 1024; reserves.host_request_bytes = 1024 * 1024; reserves.host_loader_overhead_bytes = 1024 * 1024; From 49cababf46ca289b4e3d13520cdc6044eccb1b49 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 15:08:28 +0200 Subject: [PATCH 103/123] feat(ds4v): image input on one GPU holding the whole model Images only worked with the experts split over two GPUs, where routing runs on the host. The single-GPU layer-major prefill now takes the image spans: attention gets the image visibility mask, and expert selection uses one per-token bias input (image rows: the image router bias; text rows: the layer's usual bias, or for a hash-routed layer a large bias on exactly the experts its table names). Image batches bypass the per-layer graph cache. The backend loads the image biases and the projector on the full-model load path and checks headroom on that one device. Strix Halo alone, ggml-org Q2_K_S: AI2D 85/100, ChartQA 55/60 and 43/60, the same as R9700 + Strix; 213 of 220 answers are identical between the two paths. Co-Authored-By: Claude Fable 5.1 --- docs/ds4v-image-serving.md | 36 ++++--- server/src/deepseek4/deepseek4_backend.cpp | 76 +++++++++++--- server/src/deepseek4/deepseek4_backend.h | 1 + server/src/deepseek4/deepseek4_graph.cpp | 98 +++++++++++++++---- .../deepseek4/deepseek4_image_admission.cpp | 12 +++ .../src/deepseek4/deepseek4_image_admission.h | 6 ++ server/tests/test_deepseek4_unit.cpp | 14 +++ 7 files changed, 197 insertions(+), 46 deletions(-) diff --git a/docs/ds4v-image-serving.md b/docs/ds4v-image-serving.md index d4ffaf2cb..99166408e 100644 --- a/docs/ds4v-image-serving.md +++ b/docs/ds4v-image-serving.md @@ -14,21 +14,27 @@ without it refuses `--mmproj` at startup. ## Supported configuration -The initial serving path requires Linux HIP, a DeepSeek4 decoder with the -supported DS4V dimensions, and two distinct local HIP devices. The decoder uses -sparse prefill with in-process heterogeneous expert ownership: -`DFLASH_DS4_MOE_TP=1`, `DFLASH_DS4_MOE_TP_INPROC=1`, and -`DFLASH_DS4_MOE_TP_GPU` selecting the secondary device. Set `--target-device` -to the primary device, `--ds4-prefill sparse`, and `--mmproj` to the -[exported projector](ds4v-mmproj.md). Device ordinals must match the host's -actual topology. - -Layer splitting, remote expert IPC, all-on-secondary placement, dense prefill, -concurrent sequence scheduling, and upstream forwarding do not support images. -`/props` reports the effective capability in -`capabilities.image_input_supported` after backend initialization. -Without `--mmproj`, text serving follows its existing path and image requests -are rejected. +Image input needs Linux HIP, a DeepSeek4 decoder whose GGUF carries the image +router biases, `--ds4-prefill sparse`, and `--mmproj` pointing at the +[exported projector](ds4v-mmproj.md). Two layouts work: + +- **One GPU holding the whole model** (for example a Strix Halo): nothing else + to set. The projector is loaded after the weights and must fit beside them. +- **Two GPUs splitting the experts in process** (for example R9700 + Strix + Halo): `DFLASH_DS4_MOE_TP=1`, `DFLASH_DS4_MOE_TP_INPROC=1`, and + `DFLASH_DS4_MOE_TP_GPU` selecting the second device, with `--target-device` + on the first. Device ordinals must match the host's actual topology. + +Layer splitting, remote expert IPC, all-on-secondary placement, experts kept +on the CPU, dense prefill, concurrent sequence scheduling, and upstream +forwarding do not support images. `/props` reports the effective capability in +`capabilities.image_input_supported` after backend initialization. Without +`--mmproj`, text serving follows its existing path and image requests are +passed through as they were before. + +Published llama.cpp conversions of the decoder load directly (image router +bias named `blk.N.exp_probs_b_vl.bias`, no `deepseek4.vocab_size` key). Split +GGUF files and llama.cpp's `clip` projector files are not read yet. ## Request contract diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index 20910754c..aba88c716 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -1155,11 +1155,20 @@ bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, // snapshots and retained backend pools. Charge only upcoming work here. reserves.primary_future_bytes = vision::SCRATCH_RESERVATION - resident_workspace + 128ULL * 1024 * 1024; + if (!moe_hybrid_) { + uint64_t free_bytes = 0; + if (!vision::check_deepseek4_image_single_gpu_admission(backend_, reserves.primary_domain, + reserves.primary_future_bytes, free_bytes, error)) { + std::fprintf(stderr, "[deepseek4] image runtime admission failed (one GPU): required/free=%.3f/%.3f GiB: %s\n", + gib(reserves.primary_future_bytes), gib(free_bytes), error.c_str()); + return false; + } + } vision::ImageAdmissionReport report; MoeHybridConfig runtime_cfg = make_ds4_parent_worker_cfg(w_); runtime_cfg.materialize_cold_experts = true; runtime_cfg.cold_expert_backend = MoeHybridColdBackend::Gpu; - if (!vision::check_deepseek4_image_runtime_admission(runtime_cfg, + if (moe_hybrid_ && !vision::check_deepseek4_image_runtime_admission(runtime_cfg, backend_, expert_backend_, reserves, report, error)) { std::fprintf(stderr, "[deepseek4] image runtime admission failed: primary required/free=%.3f/%.3f GiB " @@ -1206,6 +1215,36 @@ bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, } } +// The whole model is already on one GPU: load the projector next to it and +// check that the image scratch still fits. +bool DeepSeek4Backend::init_single_gpu_vision() { + if (cfg_.mmproj_path.empty()) return true; + if (!load_vision()) return false; +#if defined(DFLASH27B_BACKEND_HIP) || defined(GGML_USE_HIP) + hipDeviceProp_t properties{}; + if (hipGetDeviceProperties(&properties, cfg_.device.gpu) != hipSuccess) { + std::fprintf(stderr, "[deepseek4] cannot classify the image owner's memory domain\n"); + return false; + } + vision::ImageAdmissionReserves reserves; + reserves.primary_domain = properties.integrated || std::getenv("GGML_CUDA_ENABLE_UNIFIED_MEMORY") + ? vision::ImageMemoryDomain::HostShared : vision::ImageMemoryDomain::Dedicated; + reserves.primary_future_bytes = estimate_ds4_cache_bytes(w_, cfg_.max_ctx > 0 ? cfg_.max_ctx : 8192) + + vision::SCRATCH_RESERVATION + 256ULL * 1024 * 1024; + uint64_t free_bytes = 0; + std::string error; + const bool admitted = vision::check_deepseek4_image_single_gpu_admission( + backend_, reserves.primary_domain, reserves.primary_future_bytes, free_bytes, error); + std::fprintf(stderr, "[deepseek4] image memory admission (one GPU): required/free=%.3f/%.3f GiB result=%s\n", + gib(reserves.primary_future_bytes), gib(free_bytes), admitted ? "admitted" : error.c_str()); + if (!admitted) return false; + image_reserves_ = reserves; + return true; +#else + return false; +#endif +} + bool DeepSeek4Backend::load_vision() { if (cfg_.mmproj_path.empty()) return true; // The projector checks the decoder's width and vocabulary when it loads. @@ -1281,14 +1320,23 @@ bool DeepSeek4Backend::load_model() { const bool force_full = env_flag_enabled("DFLASH_DS4_FORCE_FULL_LOAD"); const bool heterogeneous_tp = env_flag_enabled("DFLASH_DS4_MOE_TP"); if (!cfg_.mmproj_path.empty()) { + // Images run on one HIP GPU holding the whole model, or on two HIP GPUs + // that split the experts in process. Both need batched sparse prefill. const auto tp = ds4_moe_tp_config(cfg_.device.gpu); + const bool two_gpu_ok = tp.in_process && tp.backend_valid && + tp.secondary_backend == PlacementBackend::Hip && + tp.secondary_gpu != cfg_.device.gpu && !tp.all_on_secondary && !force_full; if (target_backend != PlacementBackend::Hip || cfg_.device.is_layer_split() || cfg_.prefill_mode != PrefillAttentionMode::Sparse || - !tp.requested || !tp.in_process || !tp.backend_valid || - tp.secondary_backend != PlacementBackend::Hip || - tp.secondary_gpu == cfg_.device.gpu || tp.all_on_secondary || force_full || + (tp.requested && !two_gpu_ok) || env_flag_enabled("DFLASH_DS4_DENSE_TP_MASK")) { - std::fprintf(stderr, "[deepseek4] --mmproj requires sparse prefill with distinct local HIP expert owners\n"); + std::fprintf(stderr, "[deepseek4] --mmproj requires a HIP target with --ds4-prefill sparse, " + "on one GPU or with in-process expert owners on two distinct GPUs\n"); + return false; + } + if (!vision::detail::hip_bias_workspace(backend_)) { + std::fprintf(stderr, "[deepseek4] --mmproj needs the DS4V vision ops, which this build lacks " + "(hipBLASLt was not found when ggml-hip was configured)\n"); return false; } } @@ -1305,7 +1353,9 @@ bool DeepSeek4Backend::load_model() { cfg_.fused_decode ? "on" : "off", cfg_.fused_verify_f16_kv ? "on" : "off", prefill_attention_mode_name(cfg_.prefill_mode)); - if (!load_deepseek4_gguf(cfg_.model_path, backend_, w_)) { + TargetLoadPlan full_plan; + full_plan.load_ds4_image_bias = !cfg_.mmproj_path.empty(); + if (!load_deepseek4_gguf_partial(cfg_.model_path, backend_, full_plan, w_)) { if (prefill_attention_mode_is_approximate(cfg_.prefill_mode)) { std::fprintf(stderr, "[deepseek4] monolithic HIP load required for %s prefill\n", @@ -1317,6 +1367,7 @@ bool DeepSeek4Backend::load_model() { cfg_.model_path.c_str()); return false; } + if (!init_single_gpu_vision()) return false; } else if (target_backend == PlacementBackend::Hip || heterogeneous_tp) { std::fprintf(stderr, "[deepseek4] heterogeneous target detected; using hybrid expert load path\n"); @@ -2055,11 +2106,6 @@ bool DeepSeek4Backend::init_hybrid_model() { TargetLoadPlan plan; plan.skip_expert_tensors = true; plan.load_ds4_image_bias = !cfg_.mmproj_path.empty(); - if (plan.load_ds4_image_bias && !vision::detail::hip_bias_workspace(backend_)) { - std::fprintf(stderr, "[deepseek4] --mmproj needs the DS4V vision ops, which this build lacks " - "(hipBLASLt was not found when ggml-hip was configured)\n"); - return false; - } if (!load_deepseek4_gguf_partial(cfg_.model_path, backend_, plan, w_)) { std::fprintf(stderr, "[deepseek4] failed to partially load model for hybrid mode: %s (%s)\n", cfg_.model_path.c_str(), dflash27b_last_error()); @@ -2878,7 +2924,10 @@ int DeepSeek4Backend::do_prefill(const std::vector & tokens, need_logits ? &logits : nullptr, tokens.data() + i, timing ? &step_tel : nullptr, - cfg_.prefill_mode != PrefillAttentionMode::Sparse, hp); + cfg_.prefill_mode != PrefillAttentionMode::Sparse, hp, + /*moe_hybrid=*/nullptr, /*expert_runtime=*/nullptr, + /*routing_stats=*/nullptr, + images ? images->spans() : vision::ImageSpanView{}); } if (ok && hp && !spec_cap.empty()) { const int feat_row = spec_drafter_->n_target_layers * w_.n_embd; @@ -3149,7 +3198,8 @@ GenerateResult DeepSeek4Backend::generate_from_state( if (!images || images->owner_ != this || !images->matches(req.prompt) || kv_offset != 0 || req.snap_slot >= 0 || req.snap_pos >= 0 || req.prompt.size() + uint64_t(std::max(0, req.n_gen)) > uint64_t(cache_.max_ctx) || - !moe_hybrid_ || !expert_backend_ || expert_runtime_.compute) { + // Two-GPU serving needs its in-process second owner; one GPU has neither. + (moe_hybrid_ && !expert_backend_) || expert_runtime_.compute) { result.fail(GenerateErrorCode::PrefillFailed, "image request binding, context, or execution mode is invalid"); return result; } diff --git a/server/src/deepseek4/deepseek4_backend.h b/server/src/deepseek4/deepseek4_backend.h index b1c7447aa..599fbc071 100644 --- a/server/src/deepseek4/deepseek4_backend.h +++ b/server/src/deepseek4/deepseek4_backend.h @@ -194,6 +194,7 @@ class DeepSeek4Backend : public ModelBackend { int kv_offset = 0, int snap_slot = -1, int snap_pos = -1, const DeepSeek4ImagePrompt * images = nullptr); bool load_vision(); + bool init_single_gpu_vision(); bool materialize_images(const DeepSeek4ImagePrompt & images, const DaemonIO & io, std::string & error); diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index 89d32eda7..e875f69c4 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -349,7 +349,8 @@ static ggml_tensor * build_moe_ffn(ggml_context * ctx, const DeepSeek4Weights & w, const DeepSeek4Layer & L, int layer_idx, - int n_tokens); + int n_tokens, + ggml_tensor * selection_bias = nullptr); // Every cached per-layer decode/prefill graph below owns a StepGraph whose // metadata arena holds the ggml nodes the CUDA/HIP backend keys its captured @@ -4024,12 +4025,16 @@ static bool eval_ds4_hybrid( return true; } +// `selection_bias`, when given, is a per-token [n_expert, n_tokens] input that +// replaces the layer's single selection bias. Image batches use it: image rows +// select with the image router bias, text rows keep their usual selection. static Ds4MoeRouting build_moe_routing( ggml_context * ctx, ggml_tensor * cur, const DeepSeek4Weights & w, const DeepSeek4Layer & L, - int n_tokens) { + int n_tokens, + ggml_tensor * selection_bias = nullptr) { Ds4MoeRouting out; auto track = [&](ggml_tensor * tensor) { if (tensor) out.nodes.push_back(tensor); @@ -4043,7 +4048,9 @@ static Ds4MoeRouting build_moe_routing( ggml_tensor * softplus = track(ggml_softplus(ctx, logits)); ggml_tensor * probs = track(ggml_sqrt(ctx, softplus)); ggml_tensor * selection = probs; - if (L.ffn_exp_probs_b) { + if (selection_bias) { + selection = track(ggml_add(ctx, selection, selection_bias)); + } else if (L.ffn_exp_probs_b) { selection = track(ggml_add(ctx, selection, L.ffn_exp_probs_b)); } @@ -4068,7 +4075,8 @@ static ggml_tensor * build_moe_ffn( const DeepSeek4Weights & w, const DeepSeek4Layer & L, int layer_idx, - int n_tokens) { + int n_tokens, + ggml_tensor * selection_bias) { const int n_embd = w.n_embd; int n_used = w.n_expert_used; @@ -4076,10 +4084,10 @@ static ggml_tensor * build_moe_ffn( ggml_tensor * shared_out = build_shared_ffn(ctx, cur, w, L); ggml_tensor * routed_out = nullptr; - if (layer_idx < w.n_hash_layer && L.ffn_gate_tid2eid) { + if (!selection_bias && layer_idx < w.n_hash_layer && L.ffn_gate_tid2eid) { routed_out = ggml_scale(ctx, cur, 0.0f); } else { - Ds4MoeRouting routing = build_moe_routing(ctx, cur, w, L, n_tokens); + Ds4MoeRouting routing = build_moe_routing(ctx, cur, w, L, n_tokens, selection_bias); n_used = (int) routing.selected->ne[0]; ggml_tensor * cur_3d = ggml_reshape_3d(ctx, cur, n_embd, 1, n_tokens); ggml_tensor * gate_e = ggml_mul_mat_id(ctx, L.ffn_gate_exps, cur_3d, routing.selected); @@ -7113,12 +7121,16 @@ static int ds4_try_layer_major_prefill( std::vector * out_logits, const int32_t * token_ids, Ds4VerifyHooks * verify_hooks, - DeepSeek4StepTelemetry * telemetry) { + DeepSeek4StepTelemetry * telemetry, + vision::ImageSpanView image_spans = {}) { if (!backend || !embed || n_tokens <= 4 || n_tokens > DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS || kv_start < 0 || w.moe_hybrid) { return 0; } + // Image batches carry their own attention mask and per-token expert + // selection, so their graphs are built fresh and never cached. + const bool image_batch = image_spans.size != 0; if (cache.prefill_mode == PrefillAttentionMode::Exact) return 0; // Layer-major prefill returns only the final-position logits. DSpark's // per-layer feature capture is supported below, but verifier requests for @@ -7229,9 +7241,10 @@ static int ds4_try_layer_major_prefill( // growth costs several GiB without producing a cache hit. Keep the short // request win, then retire it before the first chunk beyond 32K. constexpr int layer_major_cache_context_limit = 32768; - const bool allow_graph_cache = + const bool cache_context_ok = token_ids && next_pos <= layer_major_cache_context_limit; - if (!allow_graph_cache) { + const bool allow_graph_cache = cache_context_ok && !image_batch; + if (!cache_context_ok) { for (auto & candidate : ds4_layer_major_graph_caches) { if (candidate.owner_ctx == w.ctx && candidate.backend == backend) { candidate.destroy(); @@ -7498,7 +7511,8 @@ static int ds4_try_layer_major_prefill( ggml_tensor * attn_out = build_mla_attention( ctx, gf, attn_normed, w, L, lc, il, kv_start, n_tokens, nullptr, i32_inputs, i32_array_inputs, i64_array_inputs, - &f32_array_inputs, attention_impl); + &f32_array_inputs, attention_impl, + /*boundary_checkpoint=*/nullptr, image_spans); if (!attn_out) { if (!cached_layer) ggml_free(ctx); return fail("attention graph build failed", il); @@ -7524,10 +7538,20 @@ static int ds4_try_layer_major_prefill( ggml_tensor * ffn_normed = build_rms_norm(ctx, ffn_in, L.ffn_norm, w.rms_eps); ggml_tensor * hash_ids = nullptr; + ggml_tensor * selection_bias = nullptr; ggml_tensor * ffn_out = nullptr; const bool hash_routed = il < w.n_hash_layer && L.ffn_gate_tid2eid && token_ids && hash_tables[(size_t) il].loaded; - if (hash_routed) { + if (image_batch) { + // One mechanism for every layer: top-k over probs + a per-token bias. + if (!L.ffn_gate_bias_vl) { + if (!cached_layer) ggml_free(ctx); + return fail("image batch without an image router bias", il); + } + selection_bias = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, w.n_expert, n_tokens); + ggml_set_input(selection_bias); + ffn_out = build_moe_ffn(ctx, ffn_normed, w, L, il, n_tokens, selection_bias); + } else if (hash_routed) { hash_ids = ggml_new_tensor_2d(ctx, GGML_TYPE_I32, w.n_expert_used, n_tokens); ggml_set_input(hash_ids); @@ -7624,6 +7648,33 @@ static int ds4_try_layer_major_prefill( ggml_backend_tensor_set(hash_ids, hash_scratch.data(), 0, sizeof(int32_t) * hash_scratch.size()); } + if (selection_bias) { + // Image rows: the image router bias. Text rows: the layer's usual + // bias, or for a hash-routed layer a large bias on exactly the + // experts its table names, so top-k returns that set. + constexpr float HASH_PICK = 1.0e4f; + const size_t n_expert = (size_t) w.n_expert; + std::vector image_bias(n_expert), text_bias(n_expert, 0.0f); + ggml_backend_tensor_get(L.ffn_gate_bias_vl, image_bias.data(), 0, sizeof(float) * n_expert); + if (!hash_routed && L.ffn_exp_probs_b) { + ggml_backend_tensor_get(L.ffn_exp_probs_b, text_bias.data(), 0, sizeof(float) * n_expert); + } + std::vector bias(n_expert * (size_t) n_tokens); + for (int t = 0; t < n_tokens; ++t) { + float * row = bias.data() + (size_t) t * n_expert; + if (vision::image_block_at(image_spans, uint64_t(kv_start) + uint64_t(t))) { + std::copy(image_bias.begin(), image_bias.end(), row); + continue; + } + std::copy(text_bias.begin(), text_bias.end(), row); + if (hash_routed) { + const int32_t * picks = hash_tables[(size_t) il].ids.data() + + (size_t) token_ids[t] * (size_t) w.n_expert_used; + for (int k = 0; k < w.n_expert_used; ++k) row[picks[k]] = HASH_PICK; + } + } + ggml_backend_tensor_set(selection_bias, bias.data(), 0, sizeof(float) * bias.size()); + } if (telemetry) { telemetry->full_graph_build_us += ds4_elapsed_us( build_t0, Ds4TimingClock::now()); @@ -7806,13 +7857,18 @@ bool deepseek4_validate_image_batch( } } if (!has_images) return true; - if (!hybrid || !w.moe_hybrid || !hybrid->materialized_cold_experts || - hybrid->cold_backend_kind != MoeHybridColdBackend::Gpu || !hybrid->cold_backend || - cache.prefill_mode != PrefillAttentionMode::Sparse || count <= 4 || + // Images run through a batched sparse prefill: the single-GPU layer-major + // path, or the two-GPU path with both expert owners materialized on GPUs. + if (cache.prefill_mode != PrefillAttentionMode::Sparse || count <= 4 || count > DS4_MAX_LAYER_MAJOR_PREFILL_TOKENS || w.layers.size() != size_t(w.n_layer) || cache.layers.size() != size_t(w.n_layer) || - w.compress_ratios.size() != size_t(w.n_layer) || hybrid->layers.size() != size_t(w.n_layer)) - return fail("image batch requires the heterogeneous sparse decoder path"); + w.compress_ratios.size() != size_t(w.n_layer)) + return fail("image batch requires batched sparse prefill"); + if ((hybrid || w.moe_hybrid) && + (!hybrid || !w.moe_hybrid || !hybrid->materialized_cold_experts || + hybrid->cold_backend_kind != MoeHybridColdBackend::Gpu || !hybrid->cold_backend || + hybrid->layers.size() != size_t(w.n_layer))) + return fail("image batch requires both expert owners on GPUs"); for (int il = 0; il < w.n_layer; ++il) { const auto & layer = w.layers[size_t(il)]; const auto & state = cache.layers[size_t(il)]; @@ -8246,7 +8302,8 @@ bool deepseek4_step_layer_range( n_tokens, kv_start, image_spans, image_batch, image_error) || (image_batch && (!embed || layer_begin != 0 || layer_end != w.n_layer || !out_logits || verify_hooks || expert_runtime || - !vision::detail::hip_bias_workspace(backend) || moe_hybrid->cold_backend == backend))) { + !vision::detail::hip_bias_workspace(backend) || + (moe_hybrid && moe_hybrid->cold_backend == backend)))) { std::fprintf(stderr, "[deepseek4] image prefill rejected before evaluation: %s\n", image_error.empty() ? "unsupported execution path" : image_error.c_str()); return false; @@ -8587,7 +8644,7 @@ bool deepseek4_step_layer_range( hc_layer_weights_range, hc_output_weights_range, hash_routing_tables_range, scratch.hash_expert_ids, embed, n_tokens, kv_start, out_logits, token_ids, verify_hooks, - telemetry); + telemetry, image_batch ? image_spans : vision::ImageSpanView{}); if (prc < 0) return false; if (prc > 0) { if (telemetry) { @@ -8597,6 +8654,11 @@ bool deepseek4_step_layer_range( return true; } } + // Only the two batched prefill paths know about image rows. + if (image_batch && !heterogeneous_sparse_prefill) { + std::fprintf(stderr, "[deepseek4] image prefill has no batched path for this configuration\n"); + return false; + } // The batched verifier graph is also the only whole-model graph that can // currently own tensors on both GPU backends. Reuse it for q=1 hybrid diff --git a/server/src/deepseek4/deepseek4_image_admission.cpp b/server/src/deepseek4/deepseek4_image_admission.cpp index 8734046cc..66a6dff71 100644 --- a/server/src/deepseek4/deepseek4_image_admission.cpp +++ b/server/src/deepseek4/deepseek4_image_admission.cpp @@ -301,6 +301,18 @@ bool check_deepseek4_image_host_preparation(uint64_t required_bytes, std::string return true; } +bool check_deepseek4_image_single_gpu_admission( + ggml_backend_t gpu, ImageMemoryDomain domain, uint64_t required_bytes, + uint64_t & free_bytes, std::string & error) { + error.clear(); + free_bytes = 0; + if (!gpu_device(gpu)) return fail(error, "image admission requires a GPU owner"); + if (domain == ImageMemoryDomain::Unknown) return fail(error, "the owner's memory domain must be classified"); + if (!device_free(gpu, domain, free_bytes, error)) return false; + if (required_bytes > free_bytes) return fail(error, "insufficient GPU headroom for image scratch"); + return true; +} + bool check_deepseek4_image_runtime_admission( const common::MoeHybridConfig & config, ggml_backend_t primary, ggml_backend_t cold, const ImageAdmissionReserves & reserves, ImageAdmissionReport & out, std::string & error) { diff --git a/server/src/deepseek4/deepseek4_image_admission.h b/server/src/deepseek4/deepseek4_image_admission.h index f6eeb5aca..e1d4efd73 100644 --- a/server/src/deepseek4/deepseek4_image_admission.h +++ b/server/src/deepseek4/deepseek4_image_admission.h @@ -118,6 +118,12 @@ bool check_deepseek4_image_host_preparation(uint64_t required_bytes, std::string // Recheck after synchronizing owners and releasing disposable graph caches. // Existing experts/tables/core/KV/snapshots are represented only by live free // memory. No model-loader or expert-copy charge is added a second time. +// One GPU holds the whole model: `required_bytes` of image scratch and future +// KV must fit in what that GPU has free right now. +bool check_deepseek4_image_single_gpu_admission( + ggml_backend * gpu, ImageMemoryDomain domain, uint64_t required_bytes, + uint64_t & free_bytes, std::string & error); + bool check_deepseek4_image_runtime_admission( const common::MoeHybridConfig & config, ggml_backend * primary, ggml_backend * cold, const ImageAdmissionReserves & reserves, ImageAdmissionReport & result, std::string & error); diff --git a/server/tests/test_deepseek4_unit.cpp b/server/tests/test_deepseek4_unit.cpp index e78dd85ab..7a69f5f08 100644 --- a/server/tests/test_deepseek4_unit.cpp +++ b/server/tests/test_deepseek4_unit.cpp @@ -2566,6 +2566,20 @@ static void test_image_batch_admission_before_execution(ggml_backend_t backend) const int32_t text[] = {1, 2}; TEST_ASSERT(deepseek4_validate_image_batch(weights, cache, nullptr, text, 2, 6, spans, has_images, error) && !has_images); + // One GPU holding the whole model has no second expert owner to check. + // (Dense weights here are unset, so the per-layer tensor checks still apply.) + weights.moe_hybrid = false; + cache.layers.resize(43); + for (auto & layer_cache : cache.layers) { + layer_cache.raw_kv = layer_cache.comp_kv = layer_cache.index_comp_kv = state; + layer_cache.attn_compressor.state_kv = layer_cache.attn_compressor.state_score = state; + layer_cache.indexer_compressor.state_kv = layer_cache.indexer_compressor.state_score = state; + } + TEST_ASSERT(deepseek4_validate_image_batch(weights, cache, nullptr, tokens.data(), + int(tokens.size()), 0, spans, has_images, error) && has_images); + cache.prefill_mode = PrefillAttentionMode::Exact; + TEST_ASSERT(!deepseek4_validate_image_batch(weights, cache, nullptr, tokens.data(), + int(tokens.size()), 0, spans, has_images, error)); ggml_free(ctx); std::fprintf(stderr, g_failures ? " done\n" : " ok\n"); } From b3409fc45d3ce054ab81efe4066825401c54206c Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 16:19:56 +0200 Subject: [PATCH 104/123] fix(ds4v): review fixes - images outside the last prefill chunk, 16-bit PNGs, offline builds - An image batch no longer needs a logits buffer: an image followed by more than one chunk of text, or a second image, was rejected before evaluation. - 16-bit greyscale PNGs keep their high byte instead of saturating to white. - The GPU page pool estimate is 0 on a host without an amdgpu device. - supports_images() is false while the backend is parked. - DFLASH27B_IMAGE_CODECS=OFF builds without downloading libjpeg-turbo/lodepng; image requests are then refused. - The converter rejects a config without compress_ratios instead of writing an all-zero schedule. - Smaller: --mmproj without a value, bounded vocabulary fallback, null-safe reclaim label, optional licence install, CPU planner case, one per-image byte cap constant, comments and docs. Co-Authored-By: Claude Fable 5.1 --- docs/ds4v-image-serving.md | 2 +- server/CMakeLists.txt | 40 ++++++++++++------- .../llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c | 3 +- server/src/common/copied_source_reclaim.h | 2 +- server/src/common/gpu_page_pool.cpp | 17 +++++--- server/src/common/moe_hybrid_storage.h | 7 ++-- server/src/common/vision/image_decode.cpp | 16 ++++++-- server/src/common/vision/image_spans.h | 6 ++- server/src/deepseek4/deepseek4_backend.h | 2 +- server/src/deepseek4/deepseek4_graph.cpp | 2 +- server/src/deepseek4/deepseek4_loader.cpp | 3 +- server/src/server/image_input.h | 6 ++- server/src/server/server_main.cpp | 6 ++- server/test/test_image_decode.cpp | 10 +++++ .../ds4_mix_converter/ds4_mix_converter.cpp | 8 ++-- 15 files changed, 88 insertions(+), 42 deletions(-) diff --git a/docs/ds4v-image-serving.md b/docs/ds4v-image-serving.md index 99166408e..03714921a 100644 --- a/docs/ds4v-image-serving.md +++ b/docs/ds4v-image-serving.md @@ -113,7 +113,7 @@ Shared by every model: | Reading images out of a request, limits, redaction | `server/src/server/image_input.*` | | JPEG and PNG decoding to RGB | `server/src/common/vision/image_decode.*`, codecs in `server/cmake/ImageCodecs.cmake` | | Image positions in a prompt, batches that keep an image whole | `server/src/common/vision/image_spans.h` | -| The backend contract | `supports_images`, `image_placeholder`, `prepare_images` in `server/src/common/model_backend.h`, and `GenerateRequest::images` | +| The backend contract | `supports_images`, `image_placeholder`, `prepare_images` in `server/src/common/model_backend.h`; `GenerateRequest::images` in `server/src/common/generation_types.h` | DS4V only, all under `server/src/deepseek4/`: resizing and patching (`deepseek4_vision_preprocess`), the vision tower (`deepseek4_vision`), marker diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 765876b8b..76e96f997 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -900,14 +900,24 @@ if(DFLASH27B_ENABLE_BSA) endif() endif() -# JPEG and PNG decoders for image input (common/vision/image_decode). -include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ImageCodecs.cmake") -install(FILES cmake/ImageCodecs.NOTICES.md - DESTINATION share/licenses/image-codecs - RENAME THIRD_PARTY_NOTICES.md) -install(FILES "${IMAGE_CODEC_JPEG_SOURCE_DIR}/LICENSE.md" - "${IMAGE_CODEC_JPEG_SOURCE_DIR}/README.ijg" - DESTINATION share/licenses/image-codecs/libjpeg-turbo) +# JPEG and PNG decoders for image input (common/vision/image_decode). They are +# downloaded at build time; an offline or text-only build can turn them off and +# image requests are then refused. +option(DFLASH27B_IMAGE_CODECS "Download and build the JPEG/PNG decoders for image input" ON) +if(DFLASH27B_IMAGE_CODECS) + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/ImageCodecs.cmake") + install(FILES cmake/ImageCodecs.NOTICES.md + DESTINATION share/licenses/image-codecs + RENAME THIRD_PARTY_NOTICES.md) + install(FILES "${IMAGE_CODEC_JPEG_SOURCE_DIR}/LICENSE.md" + "${IMAGE_CODEC_JPEG_SOURCE_DIR}/README.ijg" + DESTINATION share/licenses/image-codecs/libjpeg-turbo + OPTIONAL) + set(_dflash_image_codec_libs image_codec_jpeg image_codec_png) +else() + set(_dflash_image_codec_libs) + target_compile_definitions(dflash_common PRIVATE DFLASH_NO_IMAGE_CODECS) +endif() target_link_libraries(dflash_common PUBLIC @@ -916,8 +926,7 @@ target_link_libraries(dflash_common ggml-base nlohmann_json::nlohmann_json PRIVATE - image_codec_jpeg - image_codec_png + ${_dflash_image_codec_libs} ${CMAKE_DL_LIBS} ) # OpenMP for parallel MoE expert compute kernel (saturate memory bandwidth). @@ -1892,12 +1901,15 @@ if(DFLASH27B_TESTS) add_executable(test_image_input test/test_image_input.cpp src/server/image_input.cpp) target_include_directories(test_image_input PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(test_image_input PRIVATE nlohmann_json::nlohmann_json) - add_executable(test_image_decode test/test_image_decode.cpp src/common/vision/image_decode.cpp) - target_include_directories(test_image_decode PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) - target_link_libraries(test_image_decode PRIVATE image_codec_jpeg image_codec_png) + if(DFLASH27B_IMAGE_CODECS) + add_executable(test_image_decode test/test_image_decode.cpp src/common/vision/image_decode.cpp) + target_include_directories(test_image_decode PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(test_image_decode PRIVATE image_codec_jpeg image_codec_png) + list(APPEND _raw_unit_test_targets test_image_decode) + endif() add_executable(test_gpu_page_pool test/test_gpu_page_pool.cpp src/common/gpu_page_pool.cpp) target_include_directories(test_gpu_page_pool PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) - list(APPEND _raw_unit_test_targets test_image_input test_image_decode test_gpu_page_pool) + list(APPEND _raw_unit_test_targets test_image_input test_gpu_page_pool) # DS4V image units: each test builds only the unit it covers. foreach(_ds4v_unit assembly integration policy prompt) diff --git a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c index 4d2e99989..415ee2197 100644 --- a/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c +++ b/server/deps/llama.cpp/ggml/src/ggml-cpu/ggml-cpu.c @@ -2647,13 +2647,14 @@ static int ggml_get_n_tasks(struct ggml_tensor * node, int n_threads) { case GGML_OP_FLASH_ATTN_EXT: case GGML_OP_FLASH_ATTN_SPARSE: case GGML_OP_PAGED_ATTN: - case GGML_OP_MUL_MAT_BIAS_BF16: case GGML_OP_FLASH_ATTN_BACK: case GGML_OP_SSM_CONV: case GGML_OP_SSM_SCAN: { n_tasks = n_threads; } break; + case GGML_OP_MUL_MAT_BIAS_BF16: + GGML_ABORT("GGML_OP_MUL_MAT_BIAS_BF16 cannot be planned on CPU"); case GGML_OP_RMS_NORM_VISION_F32: GGML_ABORT("GGML_OP_RMS_NORM_VISION_F32 cannot be planned on CPU"); case GGML_OP_SOFT_MAX_VISION_F32: diff --git a/server/src/common/copied_source_reclaim.h b/server/src/common/copied_source_reclaim.h index 42344f7f3..1dd850a7a 100644 --- a/server/src/common/copied_source_reclaim.h +++ b/server/src/common/copied_source_reclaim.h @@ -51,7 +51,7 @@ inline CopiedSourceAdviceResult reclaim_copied_file_source( } if (result.range_error || result.madvise_error || result.fadvise_error) { std::fprintf(stderr, "[source-reclaim] %s layer=%d requested=%zu range_error=%d madvise_error=%d fadvise_error=%d\n", - label, layer, result.requested, result.range_error, + label ? label : "?", layer, result.requested, result.range_error, result.madvise_error, result.fadvise_error); } #else diff --git a/server/src/common/gpu_page_pool.cpp b/server/src/common/gpu_page_pool.cpp index 653bc0a83..d2954f468 100644 --- a/server/src/common/gpu_page_pool.cpp +++ b/server/src/common/gpu_page_pool.cpp @@ -24,20 +24,23 @@ constexpr const char * ACCOUNTED[] = { }; #if defined(__linux__) -uint64_t live_gpu_host_bytes() { - uint64_t total = 0; +// False when no amdgpu device reports its GTT use: the pool cannot then be told +// apart from pages owned by other drivers. +bool live_gpu_host_bytes(uint64_t & total) { + total = 0; + bool found = false; DIR * dir = opendir("/sys/class/drm"); - if (!dir) return 0; + if (!dir) return false; while (const dirent * entry = readdir(dir)) { const std::string name = entry->d_name; // card0, card1, ... but not the connector entries such as card0-DP-1. if (name.rfind("card", 0) != 0 || name.find('-') != std::string::npos) continue; std::ifstream used("/sys/class/drm/" + name + "/device/mem_info_gtt_used"); uint64_t bytes = 0; - if (used >> bytes) total += bytes; + if (used >> bytes) { total += bytes; found = true; } } closedir(dir); - return total; + return found; } #endif @@ -69,9 +72,11 @@ uint64_t reclaimable_gpu_page_pool_bytes() { #if defined(__linux__) std::ifstream input("/proc/meminfo"); if (!input) return 0; + uint64_t live = 0; + if (!live_gpu_host_bytes(live)) return 0; std::stringstream text; text << input.rdbuf(); - return reclaimable_gpu_page_pool_bytes(text.str().c_str(), live_gpu_host_bytes()); + return reclaimable_gpu_page_pool_bytes(text.str().c_str(), live); #else return 0; #endif diff --git a/server/src/common/moe_hybrid_storage.h b/server/src/common/moe_hybrid_storage.h index 773aac742..f2e56b839 100644 --- a/server/src/common/moe_hybrid_storage.h +++ b/server/src/common/moe_hybrid_storage.h @@ -264,9 +264,10 @@ int moe_hybrid_cache_swap_in(MoeHybridLayerStorage & st, int global_expert, ggml_backend_t gpu_backend); // Build hybrid storage by loading expert data directly from file (mmap). -// Optional: a caller that passes readonly_file_fd >= 0 for its read-only -// file-backed mapping opts in to advisory page-cache reclamation of completed -// materialized GPU layers. Source pointers stay valid; later reads refault. +// Optional: a caller opts in to advisory page-cache reclamation of completed +// materialized GPU layers by passing all three readonly_file_* arguments: the +// read-only mapping (which must start at file offset zero), its size, and the +// descriptor it was mapped from. Source pointers stay valid; later reads refault. bool build_moe_hybrid_storage_from_file( const MoeHybridConfig & cfg, ggml_backend_t gpu_backend, diff --git a/server/src/common/vision/image_decode.cpp b/server/src/common/vision/image_decode.cpp index a391f59f6..2e63097f1 100644 --- a/server/src/common/vision/image_decode.cpp +++ b/server/src/common/vision/image_decode.cpp @@ -1,8 +1,10 @@ #include "image_decode.h" +#if !defined(DFLASH_NO_IMAGE_CODECS) #include #include #include +#endif #include #include @@ -22,6 +24,7 @@ DecodeResult fail(DecodeError code, std::string message) { return result; } +#if !defined(DFLASH_NO_IMAGE_CODECS) DecodeStatus validate_encoded(const EncodedImageView & encoded, const DecodeLimits & limits) { if (encoded.data == nullptr || encoded.size == 0) { return {DecodeError::EmptyInput, "encoded image is empty"}; @@ -321,10 +324,9 @@ DecodeResult decode_png(const EncodedImageView & encoded, const DecodeLimits & l const std::size_t sample_count = static_cast(width) * height; result.image.pixels.resize(output_bytes); for (std::size_t sample = 0; sample < sample_count; ++sample) { - const std::uint16_t value = - static_cast(output[sample * 2]) << 8 | - output[sample * 2 + 1]; - const auto channel = static_cast(std::min(value, 255)); + // Samples are big-endian; the high byte is the 8-bit value. + // (Clamping the 16-bit value to 255 turns the image white.) + const std::uint8_t channel = output[sample * 2]; result.image.pixels[sample * 3] = channel; result.image.pixels[sample * 3 + 1] = channel; result.image.pixels[sample * 3 + 2] = channel; @@ -341,9 +343,14 @@ DecodeResult decode_png(const EncodedImageView & encoded, const DecodeLimits & l return result; } +#endif // !DFLASH_NO_IMAGE_CODECS } // namespace DecodeResult decode_image(const EncodedImageView & encoded, const DecodeLimits & limits) { +#if defined(DFLASH_NO_IMAGE_CODECS) + (void) encoded; (void) limits; + return fail(DecodeError::UnsupportedFormat, "this build has no image codecs (DFLASH27B_IMAGE_CODECS=OFF)"); +#else if (const auto status = validate_encoded(encoded, limits); !status) { DecodeResult result; result.status = status; @@ -359,6 +366,7 @@ DecodeResult decode_image(const EncodedImageView & encoded, const DecodeLimits & return decode_jpeg(encoded, limits); } return fail(DecodeError::UnsupportedFormat, "encoded image is neither JPEG nor PNG"); +#endif } const char * decode_error_name(DecodeError error) { diff --git a/server/src/common/vision/image_spans.h b/server/src/common/vision/image_spans.h index 84b8dd9de..6672ec2df 100644 --- a/server/src/common/vision/image_spans.h +++ b/server/src/common/vision/image_spans.h @@ -49,8 +49,10 @@ inline bool valid_image_spans(ImageSpanView spans, uint64_t prompt_size, return true; } -// Largest batch starting at `position` that does not cut an image in two. -// Returns 0 when no such batch fits in `capacity`. +// A batch of about `proposed` tokens starting at `position` that does not cut +// an image in two: it stops before an image it cannot hold, and grows up to +// `capacity` to finish an image it starts with. Returns 0 when an image that +// starts here does not fit in `capacity`. inline int atomic_image_chunk(ImageSpanView spans, uint64_t position, int proposed, uint64_t remaining, int capacity) { if (proposed <= 0 || capacity <= 0 || uint64_t(proposed) > remaining || diff --git a/server/src/deepseek4/deepseek4_backend.h b/server/src/deepseek4/deepseek4_backend.h index 599fbc071..7f3a960e3 100644 --- a/server/src/deepseek4/deepseek4_backend.h +++ b/server/src/deepseek4/deepseek4_backend.h @@ -72,7 +72,7 @@ class DeepSeek4Backend : public ModelBackend { // ModelBackend interface void print_ready_banner() const override; - bool supports_images() const override { return image_capable_; } + bool supports_images() const override { return image_capable_ && vision_ != nullptr; } std::string image_placeholder() const override { return vision::DS4V_IMAGE_PLACEHOLDER; } bool prepare_images(std::vector & tokens, std::vector images, diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index e875f69c4..b08bd7882 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -8301,7 +8301,7 @@ bool deepseek4_step_layer_range( if (!deepseek4_validate_image_batch(w, cache, moe_hybrid, token_ids, n_tokens, kv_start, image_spans, image_batch, image_error) || (image_batch && (!embed || layer_begin != 0 || layer_end != w.n_layer || - !out_logits || verify_hooks || expert_runtime || + verify_hooks || expert_runtime || !vision::detail::hip_bias_workspace(backend) || (moe_hybrid && moe_hybrid->cold_backend == backend)))) { std::fprintf(stderr, "[deepseek4] image prefill rejected before evaluation: %s\n", diff --git a/server/src/deepseek4/deepseek4_loader.cpp b/server/src/deepseek4/deepseek4_loader.cpp index 89d0ed693..fac1f48a5 100644 --- a/server/src/deepseek4/deepseek4_loader.cpp +++ b/server/src/deepseek4/deepseek4_loader.cpp @@ -1489,7 +1489,8 @@ bool load_deepseek4_gguf_partial(const std::string & path, if (n_vocab == 0) { const int64_t tokens_key = gguf_find_key(gctx, "tokenizer.ggml.tokens"); if (tokens_key >= 0 && gguf_get_kv_type(gctx, tokens_key) == GGUF_TYPE_ARRAY) { - n_vocab = (uint32_t) gguf_get_arr_n(gctx, tokens_key); + const int64_t n_tokens = (int64_t) gguf_get_arr_n(gctx, tokens_key); + if (n_tokens > 0 && n_tokens <= std::numeric_limits::max()) n_vocab = (uint32_t) n_tokens; } } const uint32_t n_head = get_u32_or(gctx, "deepseek4.attention.head_count", 64); diff --git a/server/src/server/image_input.h b/server/src/server/image_input.h index 909d5ebe1..c964023d4 100644 --- a/server/src/server/image_input.h +++ b/server/src/server/image_input.h @@ -12,8 +12,10 @@ namespace dflash::common { +inline constexpr size_t MAX_IMAGE_BYTES = 16 * 1024 * 1024; + struct ImageInputLimits { - size_t image_bytes = 16 * 1024 * 1024; + size_t image_bytes = MAX_IMAGE_BYTES; size_t request_bytes = 32 * 1024 * 1024; size_t image_count = 4; }; @@ -27,7 +29,7 @@ struct ImageRequestPolicy { }; bool parse_image_data_url(std::string_view url, EncodedImage & image, - std::string & error, size_t max_bytes = 16 * 1024 * 1024); + std::string & error, size_t max_bytes = MAX_IMAGE_BYTES); bool extract_chat_images(const nlohmann::json & messages, std::string_view placeholder, nlohmann::json & normalized, diff --git a/server/src/server/server_main.cpp b/server/src/server/server_main.cpp index 1a2cb16fa..133b90d75 100644 --- a/server/src/server/server_main.cpp +++ b/server/src/server/server_main.cpp @@ -359,7 +359,11 @@ static int parse_model_options(int argc, char ** argv, ModelOptions & model, } if (std::strcmp(argv[i], "--draft") == 0 && i + 1 < argc) { bargs.draft_path = argv[++i]; - } else if (std::strcmp(argv[i], "--mmproj") == 0 && i + 1 < argc) { + } else if (std::strcmp(argv[i], "--mmproj") == 0) { + if (i + 1 >= argc) { + std::fprintf(stderr, "[server] --mmproj needs a projector GGUF path\n"); + return 2; + } bargs.mmproj_path = argv[++i]; } else if (std::strcmp(argv[i], "--port") == 0 && i + 1 < argc) { sconfig.port = std::atoi(argv[++i]); diff --git a/server/test/test_image_decode.cpp b/server/test/test_image_decode.cpp index de5b7dd89..c5a05538a 100644 --- a/server/test/test_image_decode.cpp +++ b/server/test/test_image_decode.cpp @@ -78,6 +78,11 @@ const std::vector JPEG_GREY_8X8 = { 0, 8, 1, 1, 0, 0, 63, 0, 43, 255, 217, }; +// 2x1 16-bit greyscale PNG with samples 0x8000 and 0x0100. +const std::vector PNG_GREY16_2X1 = { + 137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 2, 0, 0, 0, 1, 16, 0, 0, 0, 0, 129, 217, 252, 21, 0, 0, 0, 13, 73, 68, 65, 84, 120, 156, 99, 104, 96, 96, 100, 0, 0, 2, 7, 0, 130, 159, 82, 239, 216, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130, +}; + static DecodeResult decode(const std::vector & bytes, const DecodeLimits & limits = {}) { return decode_image({bytes.data(), bytes.size()}, limits); } @@ -109,6 +114,11 @@ int main() { for (size_t i = 0; grey && i < result.image.pixels.size(); ++i) grey = near(result.image.pixels[i], 128); check(grey, "greyscale JPEG expands to RGB"); } + { + const auto result = decode(PNG_GREY16_2X1); + const std::vector expected = {128, 128, 128, 1, 1, 1}; + check(bool(result) && result.image.pixels == expected, "16-bit greyscale keeps its high byte"); + } { check(decode_image({nullptr, 0}).status.code == DecodeError::EmptyInput, "empty input"); const std::vector text = {'n', 'o', 't', ' ', 'a', 'n', ' ', 'i', 'm', 'a', 'g', 'e'}; diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index d3245ae03..fdff4f641 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -957,11 +957,11 @@ void set_model_metadata(gguf_context * ctx, const SafeTensorSet & source, gguf_set_val_f32(ctx, "deepseek4.expert_weights_scale", config_f32(c, "routed_scaling_factor", 1.5f)); gguf_set_val_f32(ctx, "deepseek4.swiglu_clamp_exp", config_f32(c, "swiglu_limit", 10.0f)); + // An all-zero schedule would load and silently run the wrong attention. + if (!c.contains("compress_ratios") || !c["compress_ratios"].is_array()) fail("config.json has no compress_ratios array"); + if (c["compress_ratios"].size() < layers) fail("config compress_ratios is shorter than selected layers"); std::vector ratios(layers, 0); - if (c.contains("compress_ratios") && c["compress_ratios"].is_array()) { - if (c["compress_ratios"].size() < layers) fail("config compress_ratios is shorter than selected layers"); - for (uint32_t i = 0; i < layers; ++i) ratios[i] = c["compress_ratios"][i].get(); - } + for (uint32_t i = 0; i < layers; ++i) ratios[i] = c["compress_ratios"][i].get(); gguf_set_arr_data(ctx, "deepseek4.attention.compress_ratios", GGUF_TYPE_UINT32, ratios.data(), ratios.size()); From 12a0b2932c71a6719f63f65610fd5907768c5fc2 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 16:41:04 +0200 Subject: [PATCH 105/123] build(vision): vendor lodepng instead of downloading it at configure time Both Radeon CI jobs failed when GitHub answered 504 for the on-the-fly commit archive. lodepng has no release archives and is two source files, so it now lives in server/deps/lodepng (pinned commit, archive checksum verified). libjpeg-turbo still comes from its release archive. Co-Authored-By: Claude Fable 5.1 --- server/cmake/ImageCodecs.cmake | 17 +- server/deps/lodepng/LICENSE | 21 + server/deps/lodepng/VENDOR.md | 11 + server/deps/lodepng/lodepng.cpp | 7244 +++++++++++++++++++++++++++++++ server/deps/lodepng/lodepng.h | 2188 ++++++++++ 5 files changed, 9471 insertions(+), 10 deletions(-) create mode 100644 server/deps/lodepng/LICENSE create mode 100644 server/deps/lodepng/VENDOR.md create mode 100644 server/deps/lodepng/lodepng.cpp create mode 100644 server/deps/lodepng/lodepng.h diff --git a/server/cmake/ImageCodecs.cmake b/server/cmake/ImageCodecs.cmake index 8a1973b58..8cab30b64 100644 --- a/server/cmake/ImageCodecs.cmake +++ b/server/cmake/ImageCodecs.cmake @@ -1,9 +1,9 @@ -# Pinned JPEG and PNG decoders behind common/vision/image_decode. License texts -# are in ImageCodecs.NOTICES.md and the unmodified upstream archives. +# JPEG and PNG decoders behind common/vision/image_decode: libjpeg-turbo from +# its pinned release archive, lodepng vendored. License texts are in +# ImageCodecs.NOTICES.md, deps/lodepng/LICENSE and the libjpeg-turbo archive. include_guard(GLOBAL) include(ExternalProject) -include(FetchContent) # DOWNLOAD_EXTRACT_TIMESTAMP exists from CMake 3.24; older releases would read # it as part of URL_HASH. @@ -42,13 +42,10 @@ set_target_properties(image_codec_jpeg PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${IMAGE_CODEC_JPEG_PREFIX}/include) add_dependencies(image_codec_jpeg libjpeg_turbo_external) -FetchContent_Declare(lodepng - URL https://github.com/lvandeve/lodepng/archive/ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a.tar.gz - URL_HASH SHA256=c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803 - ${IMAGE_CODEC_EXTRACT_TIMESTAMP}) -FetchContent_MakeAvailable(lodepng) -add_library(image_codec_png STATIC ${lodepng_SOURCE_DIR}/lodepng.cpp) -target_include_directories(image_codec_png PUBLIC ${lodepng_SOURCE_DIR}) +# lodepng is vendored (server/deps/lodepng): two source files, no release archives upstream. +set(IMAGE_CODEC_PNG_DIR "${CMAKE_CURRENT_LIST_DIR}/../deps/lodepng") +add_library(image_codec_png STATIC ${IMAGE_CODEC_PNG_DIR}/lodepng.cpp) +target_include_directories(image_codec_png PUBLIC ${IMAGE_CODEC_PNG_DIR}) # Expose the original JPEG notices for production installation after the # dependency build has downloaded its hash-verified source archive. diff --git a/server/deps/lodepng/LICENSE b/server/deps/lodepng/LICENSE new file mode 100644 index 000000000..a5fb0603d --- /dev/null +++ b/server/deps/lodepng/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2005-2018 Lode Vandevenne + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. + diff --git a/server/deps/lodepng/VENDOR.md b/server/deps/lodepng/VENDOR.md new file mode 100644 index 000000000..46773ca2d --- /dev/null +++ b/server/deps/lodepng/VENDOR.md @@ -0,0 +1,11 @@ +# Vendored lodepng + +PNG decoder used by `server/src/common/vision/image_decode.cpp`. + +- Source: https://github.com/lvandeve/lodepng +- Commit: `ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a` +- Archive SHA256: `c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803` +- Files: `lodepng.cpp`, `lodepng.h`, `LICENSE` (zlib), unmodified. + +Vendored rather than downloaded: the project has no release archives, and +GitHub's on-the-fly commit archives fail often enough to break CI. diff --git a/server/deps/lodepng/lodepng.cpp b/server/deps/lodepng/lodepng.cpp new file mode 100644 index 000000000..1a9e3e27c --- /dev/null +++ b/server/deps/lodepng/lodepng.cpp @@ -0,0 +1,7244 @@ +/* +LodePNG version 20260119 + +Copyright (c) 2005-2026 Lode Vandevenne + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ + +/* +The manual and changelog are in the header file "lodepng.h" +Rename this file to lodepng.cpp to use it for C++, or to lodepng.c to use it for C. +*/ + +#include "lodepng.h" + +#ifdef LODEPNG_COMPILE_DISK +#include /* LONG_MAX */ +#include /* file handling */ +#endif /* LODEPNG_COMPILE_DISK */ + +#ifdef LODEPNG_COMPILE_ALLOCATORS +#include /* allocations */ +#endif /* LODEPNG_COMPILE_ALLOCATORS */ + +#if defined(_MSC_VER) && (_MSC_VER >= 1310) /*Visual Studio: A few warning types are not desired here.*/ +#pragma warning( disable : 4244 ) /*implicit conversions: not warned by gcc -Wall -Wextra and requires too much casts*/ +#pragma warning( disable : 4996 ) /*VS does not like fopen, but fopen_s is not standard C so unusable here*/ +#endif /*_MSC_VER */ + +const char* LODEPNG_VERSION_STRING = "20260119"; + +/* +This source file is divided into the following large parts. The code sections +with the "LODEPNG_COMPILE_" #defines divide this up further in an intermixed way. +-Tools for C and common code for PNG and Zlib +-C Code for Zlib (huffman, deflate, ...) +-C Code for PNG (file format chunks, adam7, PNG filters, color conversions, ...) +-The C++ wrapper around all of the above +*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // Tools for C, and common code for PNG and Zlib. // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*The malloc, realloc and free functions defined here with "lodepng_" in front +of the name, so that you can easily change them to others related to your +platform if needed. Everything else in the code calls these. Pass +-DLODEPNG_NO_COMPILE_ALLOCATORS to the compiler, or comment out +#define LODEPNG_COMPILE_ALLOCATORS in the header, to disable the ones here and +define them in your own project's source files without needing to change +lodepng source code. Don't forget to remove "static" if you copypaste them +from here.*/ + +#ifdef LODEPNG_COMPILE_ALLOCATORS +static void* lodepng_malloc(size_t size) { +#ifdef LODEPNG_MAX_ALLOC + if(size > LODEPNG_MAX_ALLOC) return 0; +#endif + return malloc(size); +} + +/* NOTE: when realloc returns NULL, it leaves the original memory untouched */ +static void* lodepng_realloc(void* ptr, size_t new_size) { +#ifdef LODEPNG_MAX_ALLOC + if(new_size > LODEPNG_MAX_ALLOC) return 0; +#endif + return realloc(ptr, new_size); +} + +static void lodepng_free(void* ptr) { + free(ptr); +} +#else /*LODEPNG_COMPILE_ALLOCATORS*/ +/* TODO: support giving additional void* payload to the custom allocators */ +void* lodepng_malloc(size_t size); +void* lodepng_realloc(void* ptr, size_t new_size); +void lodepng_free(void* ptr); +#endif /*LODEPNG_COMPILE_ALLOCATORS*/ + +/* convince the compiler to inline a function, for use when this measurably improves performance */ +/* inline is not available in C90, but use it when supported by the compiler */ +#if (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || (defined(__cplusplus) && (__cplusplus >= 199711L)) +#define LODEPNG_INLINE inline +#else +#define LODEPNG_INLINE /* not available */ +#endif + +/* restrict is not available in C90, but use it when supported by the compiler */ +#if (defined(__GNUC__) && (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))) ||\ + (defined(_MSC_VER) && (_MSC_VER >= 1400)) || \ + (defined(__WATCOMC__) && (__WATCOMC__ >= 1250) && !defined(__cplusplus)) +#define LODEPNG_RESTRICT __restrict +#else +#define LODEPNG_RESTRICT /* not available */ +#endif + +/* Replacements for C library functions such as memcpy and strlen, to support platforms +where a full C library is not available. The compiler can recognize them and compile +to something as fast. */ + +static void lodepng_memcpy(void* LODEPNG_RESTRICT dst, + const void* LODEPNG_RESTRICT src, size_t size) { + size_t i; + for(i = 0; i < size; i++) ((char*)dst)[i] = ((const char*)src)[i]; +} + +static void lodepng_memset(void* LODEPNG_RESTRICT dst, + int value, size_t num) { + size_t i; + for(i = 0; i < num; i++) ((char*)dst)[i] = (char)value; +} + +/* does not check memory out of bounds, do not use on untrusted data */ +static size_t lodepng_strlen(const char* a) { + const char* orig = a; + /* avoid warning about unused function in case of disabled COMPILE... macros */ + (void)(&lodepng_strlen); + while(*a) a++; + return (size_t)(a - orig); +} + +#define LODEPNG_MAX(a, b) (((a) > (b)) ? (a) : (b)) +#define LODEPNG_MIN(a, b) (((a) < (b)) ? (a) : (b)) + +#if defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_DECODER) +/* Safely check if adding two integers will overflow (no undefined +behavior, compiler removing the code, etc...) and output result. */ +static int lodepng_addofl(size_t a, size_t b, size_t* result) { + *result = a + b; /* Unsigned addition is well defined and safe in C90 */ + return *result < a; +} +#endif /*defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_DECODER)*/ + +#ifdef LODEPNG_COMPILE_DECODER +/* Safely check if multiplying two integers will overflow (no undefined +behavior, compiler removing the code, etc...) and output result. */ +static int lodepng_mulofl(size_t a, size_t b, size_t* result) { + *result = a * b; /* Unsigned multiplication is well defined and safe in C90 */ + return (a != 0 && *result / a != b); +} + +#ifdef LODEPNG_COMPILE_ZLIB +/* Safely check if a + b > c, even if overflow could happen. */ +static int lodepng_gtofl(size_t a, size_t b, size_t c) { + size_t d; + if(lodepng_addofl(a, b, &d)) return 1; + return d > c; +} +#endif /*LODEPNG_COMPILE_ZLIB*/ +#endif /*LODEPNG_COMPILE_DECODER*/ + + +/* +Often in case of an error a value is assigned to a variable and then it breaks +out of a loop (to go to the cleanup phase of a function). This macro does that. +It makes the error handling code shorter and more readable. + +Example: if(!uivector_resize(&lz77_encoded, datasize)) ERROR_BREAK(83); +*/ +#define CERROR_BREAK(errorvar, code){\ + errorvar = code;\ + break;\ +} + +/*version of CERROR_BREAK that assumes the common case where the error variable is named "error"*/ +#define ERROR_BREAK(code) CERROR_BREAK(error, code) + +/*Set error var to the error code, and return it.*/ +#define CERROR_RETURN_ERROR(errorvar, code){\ + errorvar = code;\ + return code;\ +} + +/*Try the code, if it returns error, also return the error.*/ +#define CERROR_TRY_RETURN(call){\ + unsigned error_ = call;\ + if(error_) return error_;\ +} + +/*Set error var to the error code, and return from the void function.*/ +#define CERROR_RETURN(errorvar, code){\ + errorvar = code;\ + return;\ +} + +/* +About uivector, ucvector and string: +-All of them wrap dynamic arrays or text strings in a similar way. +-LodePNG was originally written in C++. The vectors replace the std::vectors that were used in the C++ version. +-The string tools are made to avoid problems with compilers that declare things like strncat as deprecated. +-They're not used in the interface, only internally in this file as static functions. +-As with many other structs in this file, the init and cleanup functions serve as ctor and dtor. +*/ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_ENCODER +/*dynamic vector of unsigned ints*/ +typedef struct uivector { + unsigned* data; + size_t size; /*size in number of unsigned longs*/ + size_t allocsize; /*allocated size in bytes*/ +} uivector; + +static void uivector_cleanup(void* p) { + ((uivector*)p)->size = ((uivector*)p)->allocsize = 0; + lodepng_free(((uivector*)p)->data); + ((uivector*)p)->data = NULL; +} + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned uivector_resize(uivector* p, size_t size) { + size_t allocsize = size * sizeof(unsigned); + if(allocsize > p->allocsize) { + size_t newsize = allocsize + (p->allocsize >> 1u); + void* data = lodepng_realloc(p->data, newsize); + if(data) { + p->allocsize = newsize; + p->data = (unsigned*)data; + } + else return 0; /*error: not enough memory*/ + } + p->size = size; + return 1; /*success*/ +} + +static void uivector_init(uivector* p) { + p->data = NULL; + p->size = p->allocsize = 0; +} + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned uivector_push_back(uivector* p, unsigned c) { + if(!uivector_resize(p, p->size + 1)) return 0; + p->data[p->size - 1] = c; + return 1; +} +#endif /*LODEPNG_COMPILE_ENCODER*/ +#endif /*LODEPNG_COMPILE_ZLIB*/ + +/* /////////////////////////////////////////////////////////////////////////// */ + +/*dynamic vector of unsigned chars*/ +typedef struct ucvector { + unsigned char* data; + size_t size; /*used size*/ + size_t allocsize; /*allocated size*/ +} ucvector; + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned ucvector_reserve(ucvector* p, size_t size) { + if(size > p->allocsize) { + size_t newsize = size + (p->allocsize >> 1u); + void* data = lodepng_realloc(p->data, newsize); + if(data) { + p->allocsize = newsize; + p->data = (unsigned char*)data; + } + else return 0; /*error: not enough memory*/ + } + return 1; /*success*/ +} + +/*returns 1 if success, 0 if failure ==> nothing done*/ +static unsigned ucvector_resize(ucvector* p, size_t size) { + p->size = size; + return ucvector_reserve(p, size); +} + +static ucvector ucvector_init(unsigned char* buffer, size_t size) { + ucvector v; + v.data = buffer; + v.allocsize = v.size = size; + return v; +} + +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_PNG +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + +/*also appends null termination character*/ +static char* alloc_string_sized(const char* in, size_t insize) { + char* out = (char*)lodepng_malloc(insize + 1); + if(out) { + lodepng_memcpy(out, in, insize); + out[insize] = 0; + } + return out; +} + +/* dynamically allocates a new string with a copy of the null terminated input text */ +static char* alloc_string(const char* in) { + return alloc_string_sized(in, lodepng_strlen(in)); +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +/* ////////////////////////////////////////////////////////////////////////// */ + +#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_PNG) +static unsigned lodepng_read32bitInt(const unsigned char* buffer) { + return (((unsigned)buffer[0] << 24u) | ((unsigned)buffer[1] << 16u) | + ((unsigned)buffer[2] << 8u) | (unsigned)buffer[3]); +} +#endif /*defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_PNG)*/ + +#if defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_ENCODER) +/*buffer must have at least 4 allocated bytes available*/ +static void lodepng_set32bitInt(unsigned char* buffer, unsigned value) { + buffer[0] = (unsigned char)((value >> 24) & 0xff); + buffer[1] = (unsigned char)((value >> 16) & 0xff); + buffer[2] = (unsigned char)((value >> 8) & 0xff); + buffer[3] = (unsigned char)((value ) & 0xff); +} +#endif /*defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_ENCODER)*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / File IO / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_DISK + +/* returns negative value on error. This should be pure C compatible, so no fstat. */ +static long lodepng_filesize(FILE* file) { + long size; + if(fseek(file, 0, SEEK_END) != 0) return -1; + size = ftell(file); + /* It may give LONG_MAX as directory size, this is invalid for us. */ + if(size == LONG_MAX) return -1; + if(fseek(file, 0, SEEK_SET) != 0) return -1; + return size; +} + +/* Allocates the output buffer to the file size and reads the file into it. Returns error code.*/ +static unsigned lodepng_load_file_(unsigned char** out, size_t* outsize, FILE* file) { + long size = lodepng_filesize(file); + if(size < 0) return 78; + *outsize = (size_t)size; + *out = (unsigned char*)lodepng_malloc((size_t)size); + if(!(*out) && size > 0) return 83; /*the above malloc failed*/ + if(fread(*out, 1, *outsize, file) != *outsize) return 78; + return 0; /*ok*/ +} + +unsigned lodepng_load_file(unsigned char** out, size_t* outsize, const char* filename) { + unsigned error; + FILE* file = fopen(filename, "rb"); + if(!file) return 78; + error = lodepng_load_file_(out, outsize, file); + fclose(file); + return error; +} + +/*write given buffer to the file, overwriting the file, it doesn't append to it.*/ +unsigned lodepng_save_file(const unsigned char* buffer, size_t buffersize, const char* filename) { + FILE* file = fopen(filename, "wb" ); + if(!file) return 79; + fwrite(buffer, 1, buffersize, file); + fclose(file); + return 0; +} + +#endif /*LODEPNG_COMPILE_DISK*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // End of common code and tools. Begin of Zlib related code. // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_ENCODER + +typedef struct { + ucvector* data; + unsigned char bp; /*ok to overflow, indicates bit pos inside byte*/ +} LodePNGBitWriter; + +static void LodePNGBitWriter_init(LodePNGBitWriter* writer, ucvector* data) { + writer->data = data; + writer->bp = 0; +} + +/*TODO: this ignores potential out of memory errors*/ +#define WRITEBIT(writer, bit){\ + /* append new byte */\ + if(((writer->bp) & 7u) == 0) {\ + if(!ucvector_resize(writer->data, writer->data->size + 1)) return;\ + writer->data->data[writer->data->size - 1] = 0;\ + }\ + (writer->data->data[writer->data->size - 1]) |= (bit << ((writer->bp) & 7u));\ + ++writer->bp;\ +} + +/* LSB of value is written first, and LSB of bytes is used first */ +static void writeBits(LodePNGBitWriter* writer, unsigned value, size_t nbits) { + if(nbits == 1) { /* compiler should statically compile this case if nbits == 1 */ + WRITEBIT(writer, value); + } else { + /* TODO: increase output size only once here rather than in each WRITEBIT */ + size_t i; + for(i = 0; i != nbits; ++i) { + WRITEBIT(writer, (unsigned char)((value >> i) & 1)); + } + } +} + +/* This one is to use for adding huffman symbol, the value bits are written MSB first */ +static void writeBitsReversed(LodePNGBitWriter* writer, unsigned value, size_t nbits) { + size_t i; + for(i = 0; i != nbits; ++i) { + /* TODO: increase output size only once here rather than in each WRITEBIT */ + WRITEBIT(writer, (unsigned char)((value >> (nbits - 1u - i)) & 1u)); + } +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_DECODER + +typedef struct { + const unsigned char* data; + size_t size; /*size of data in bytes*/ + size_t bitsize; /*size of data in bits, end of valid bp values, should be 8*size*/ + size_t bp; + unsigned buffer; /*buffer for reading bits. NOTE: 'unsigned' must support at least 32 bits*/ +} LodePNGBitReader; + +/* data size argument is in bytes. Returns error if size too large causing overflow */ +static unsigned LodePNGBitReader_init(LodePNGBitReader* reader, const unsigned char* data, size_t size) { + size_t temp; + reader->data = data; + reader->size = size; + /* size in bits, return error if overflow (if size_t is 32 bit this supports up to 500MB) */ + if(lodepng_mulofl(size, 8u, &reader->bitsize)) return 105; + /*ensure incremented bp can be compared to bitsize without overflow even when it would be incremented 32 too much and + trying to ensure 32 more bits*/ + if(lodepng_addofl(reader->bitsize, 64u, &temp)) return 105; + reader->bp = 0; + reader->buffer = 0; + return 0; /*ok*/ +} + +/* +ensureBits functions: +Ensures the reader can at least read nbits bits in one or more readBits calls, +safely even if not enough bits are available. +The nbits parameter is unused but is given for documentation purposes, error +checking for amount of bits must be done beforehand. +*/ + +/*See ensureBits documentation above. This one ensures up to 9 bits */ +static LODEPNG_INLINE void ensureBits9(LodePNGBitReader* reader, size_t nbits) { + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if(start + 1u < size) { + reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u); + reader->buffer >>= (reader->bp & 7u); + } else { + reader->buffer = 0; + if(start + 0u < size) reader->buffer = reader->data[start + 0]; + reader->buffer >>= (reader->bp & 7u); + } + (void)nbits; +} + +/*See ensureBits documentation above. This one ensures up to 17 bits */ +static LODEPNG_INLINE void ensureBits17(LodePNGBitReader* reader, size_t nbits) { + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if(start + 2u < size) { + reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u) | + ((unsigned)reader->data[start + 2] << 16u); + reader->buffer >>= (reader->bp & 7u); + } else { + reader->buffer = 0; + if(start + 0u < size) reader->buffer |= reader->data[start + 0]; + if(start + 1u < size) reader->buffer |= ((unsigned)reader->data[start + 1] << 8u); + reader->buffer >>= (reader->bp & 7u); + } + (void)nbits; +} + +/*See ensureBits documentation above. This one ensures up to 25 bits */ +static LODEPNG_INLINE void ensureBits25(LodePNGBitReader* reader, size_t nbits) { + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if(start + 3u < size) { + reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u) | + ((unsigned)reader->data[start + 2] << 16u) | ((unsigned)reader->data[start + 3] << 24u); + reader->buffer >>= (reader->bp & 7u); + } else { + reader->buffer = 0; + if(start + 0u < size) reader->buffer |= reader->data[start + 0]; + if(start + 1u < size) reader->buffer |= ((unsigned)reader->data[start + 1] << 8u); + if(start + 2u < size) reader->buffer |= ((unsigned)reader->data[start + 2] << 16u); + reader->buffer >>= (reader->bp & 7u); + } + (void)nbits; +} + +/*See ensureBits documentation above. This one ensures up to 32 bits */ +static LODEPNG_INLINE void ensureBits32(LodePNGBitReader* reader, size_t nbits) { + size_t start = reader->bp >> 3u; + size_t size = reader->size; + if(start + 4u < size) { + reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u) | + ((unsigned)reader->data[start + 2] << 16u) | ((unsigned)reader->data[start + 3] << 24u); + reader->buffer >>= (reader->bp & 7u); + reader->buffer |= (((unsigned)reader->data[start + 4] << 24u) << (8u - (reader->bp & 7u))); + } else { + reader->buffer = 0; + if(start + 0u < size) reader->buffer |= reader->data[start + 0]; + if(start + 1u < size) reader->buffer |= ((unsigned)reader->data[start + 1] << 8u); + if(start + 2u < size) reader->buffer |= ((unsigned)reader->data[start + 2] << 16u); + if(start + 3u < size) reader->buffer |= ((unsigned)reader->data[start + 3] << 24u); + reader->buffer >>= (reader->bp & 7u); + } + (void)nbits; +} + +/* Get bits without advancing the bit pointer. Must have enough bits available with ensureBits. Max nbits is 31. */ +static LODEPNG_INLINE unsigned peekBits(LodePNGBitReader* reader, size_t nbits) { + /* The shift allows nbits to be only up to 31. */ + return reader->buffer & ((1u << nbits) - 1u); +} + +/* Must have enough bits available with ensureBits */ +static LODEPNG_INLINE void advanceBits(LodePNGBitReader* reader, size_t nbits) { + reader->buffer >>= nbits; + reader->bp += nbits; +} + +/* Must have enough bits available with ensureBits */ +static LODEPNG_INLINE unsigned readBits(LodePNGBitReader* reader, size_t nbits) { + unsigned result = peekBits(reader, nbits); + advanceBits(reader, nbits); + return result; +} +#endif /*LODEPNG_COMPILE_DECODER*/ + +static unsigned reverseBits(unsigned bits, unsigned num) { + /*TODO: implement faster lookup table based version when needed*/ + unsigned i, result = 0; + for(i = 0; i < num; i++) result |= ((bits >> (num - i - 1u)) & 1u) << i; + return result; +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Deflate - Huffman / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#define FIRST_LENGTH_CODE_INDEX 257 +#define LAST_LENGTH_CODE_INDEX 285 +/*256 literals, the end code, some length codes, and 2 unused codes*/ +#define NUM_DEFLATE_CODE_SYMBOLS 288 +/*the distance codes have their own symbols, 30 used, 2 unused*/ +#define NUM_DISTANCE_SYMBOLS 32 +/*the code length codes. 0-15: code lengths, 16: copy previous 3-6 times, 17: 3-10 zeros, 18: 11-138 zeros*/ +#define NUM_CODE_LENGTH_CODES 19 + +/*the base lengths represented by codes 257-285*/ +static const unsigned LENGTHBASE[29] + = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, + 67, 83, 99, 115, 131, 163, 195, 227, 258}; + +/*the extra bits used by codes 257-285 (added to base length)*/ +static const unsigned LENGTHEXTRA[29] + = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, + 4, 4, 4, 4, 5, 5, 5, 5, 0}; + +/*the base backwards distances (the bits of distance codes appear after length codes and use their own huffman tree)*/ +static const unsigned DISTANCEBASE[30] + = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, + 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; + +/*the extra bits of backwards distances (added to base)*/ +static const unsigned DISTANCEEXTRA[30] + = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, + 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; + +/*the order in which "code length alphabet code lengths" are stored as specified by deflate, out of this the huffman +tree of the dynamic huffman tree lengths is generated*/ +static const unsigned CLCL_ORDER[NUM_CODE_LENGTH_CODES] + = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; + +/* ////////////////////////////////////////////////////////////////////////// */ + +/* +Huffman tree struct, containing multiple representations of the tree +*/ +typedef struct HuffmanTree { + unsigned* codes; /*the huffman codes (bit patterns representing the symbols)*/ + unsigned* lengths; /*the lengths of the huffman codes*/ + unsigned maxbitlen; /*maximum number of bits a single code can get*/ + unsigned numcodes; /*number of symbols in the alphabet = number of codes*/ + /* for reading only */ + unsigned char* table_len; /*length of symbol from lookup table, or max length if secondary lookup needed*/ + unsigned short* table_value; /*value of symbol from lookup table, or pointer to secondary table if needed*/ +} HuffmanTree; + +static void HuffmanTree_init(HuffmanTree* tree) { + tree->codes = 0; + tree->lengths = 0; + tree->table_len = 0; + tree->table_value = 0; +} + +static void HuffmanTree_cleanup(HuffmanTree* tree) { + lodepng_free(tree->codes); + lodepng_free(tree->lengths); + lodepng_free(tree->table_len); + lodepng_free(tree->table_value); +} + +/* amount of bits for first huffman table lookup (aka root bits), see HuffmanTree_makeTable and huffmanDecodeSymbol.*/ +/* values 8u and 9u work the fastest */ +#define FIRSTBITS 9u + +/* a symbol value too big to represent any valid symbol, to indicate reading disallowed huffman bits combination, +which is possible in case of only 0 or 1 present symbols. */ +#define INVALIDSYMBOL 65535u + +/* make table for huffman decoding */ +static unsigned HuffmanTree_makeTable(HuffmanTree* tree) { + static const unsigned headsize = 1u << FIRSTBITS; /*size of the first table*/ + static const unsigned mask = (1u << FIRSTBITS) /*headsize*/ - 1u; + size_t i, numpresent, pointer, size; /*total table size*/ + unsigned* maxlens = (unsigned*)lodepng_malloc(headsize * sizeof(unsigned)); + if(!maxlens) return 83; /*alloc fail*/ + + /* compute maxlens: max total bit length of symbols sharing prefix in the first table*/ + lodepng_memset(maxlens, 0, headsize * sizeof(*maxlens)); + for(i = 0; i < tree->numcodes; i++) { + unsigned symbol = tree->codes[i]; + unsigned l = tree->lengths[i]; + unsigned index; + if(l <= FIRSTBITS) continue; /*symbols that fit in first table don't increase secondary table size*/ + /*get the FIRSTBITS MSBs, the MSBs of the symbol are encoded first. See later comment about the reversing*/ + index = reverseBits(symbol >> (l - FIRSTBITS), FIRSTBITS); + maxlens[index] = LODEPNG_MAX(maxlens[index], l); + } + /* compute total table size: size of first table plus all secondary tables for symbols longer than FIRSTBITS */ + size = headsize; + for(i = 0; i < headsize; ++i) { + unsigned l = maxlens[i]; + if(l > FIRSTBITS) size += (((size_t)1) << (l - FIRSTBITS)); + } + tree->table_len = (unsigned char*)lodepng_malloc(size * sizeof(*tree->table_len)); + tree->table_value = (unsigned short*)lodepng_malloc(size * sizeof(*tree->table_value)); + if(!tree->table_len || !tree->table_value) { + lodepng_free(maxlens); + /* freeing tree->table values is done at a higher scope */ + return 83; /*alloc fail*/ + } + /*initialize with an invalid length to indicate unused entries*/ + for(i = 0; i < size; ++i) tree->table_len[i] = 16; + + /*fill in the first table for long symbols: max prefix size and pointer to secondary tables*/ + pointer = headsize; + for(i = 0; i < headsize; ++i) { + unsigned l = maxlens[i]; + if(l <= FIRSTBITS) continue; + tree->table_len[i] = l; + tree->table_value[i] = (unsigned short)pointer; + pointer += (((size_t)1) << (l - FIRSTBITS)); + } + lodepng_free(maxlens); + + /*fill in the first table for short symbols, or secondary table for long symbols*/ + numpresent = 0; + for(i = 0; i < tree->numcodes; ++i) { + unsigned l = tree->lengths[i]; + unsigned symbol, reverse; + if(l == 0) continue; + symbol = tree->codes[i]; /*the huffman bit pattern. i itself is the value.*/ + /*reverse bits, because the huffman bits are given in MSB first order but the bit reader reads LSB first*/ + reverse = reverseBits(symbol, l); + numpresent++; + + if(l <= FIRSTBITS) { + /*short symbol, fully in first table, replicated num times if l < FIRSTBITS*/ + unsigned num = 1u << (FIRSTBITS - l); + unsigned j; + for(j = 0; j < num; ++j) { + /*bit reader will read the l bits of symbol first, the remaining FIRSTBITS - l bits go to the MSB's*/ + unsigned index = reverse | (j << l); + if(tree->table_len[index] != 16) return 55; /*invalid tree: long symbol shares prefix with short symbol*/ + tree->table_len[index] = l; + tree->table_value[index] = (unsigned short)i; + } + } else { + /*long symbol, shares prefix with other long symbols in first lookup table, needs second lookup*/ + /*the FIRSTBITS MSBs of the symbol are the first table index*/ + unsigned index = reverse & mask; + unsigned maxlen = tree->table_len[index]; + /*log2 of secondary table length, should be >= l - FIRSTBITS*/ + unsigned tablelen = maxlen - FIRSTBITS; + unsigned start = tree->table_value[index]; /*starting index in secondary table*/ + unsigned num = 1u << (tablelen - (l - FIRSTBITS)); /*amount of entries of this symbol in secondary table*/ + unsigned j; + if(maxlen < l) return 55; /*invalid tree: long symbol shares prefix with short symbol*/ + for(j = 0; j < num; ++j) { + unsigned reverse2 = reverse >> FIRSTBITS; /* l - FIRSTBITS bits */ + unsigned index2 = start + (reverse2 | (j << (l - FIRSTBITS))); + tree->table_len[index2] = l; + tree->table_value[index2] = (unsigned short)i; + } + } + } + + if(numpresent < 2) { + /* In case of exactly 1 symbol, in theory the huffman symbol needs 0 bits, + but deflate uses 1 bit instead. In case of 0 symbols, no symbols can + appear at all, but such huffman tree could still exist (e.g. if distance + codes are never used). In both cases, not all symbols of the table will be + filled in. Fill them in with an invalid symbol value so returning them from + huffmanDecodeSymbol will cause error. */ + for(i = 0; i < size; ++i) { + if(tree->table_len[i] == 16) { + /* As length, use a value smaller than FIRSTBITS for the head table, + and a value larger than FIRSTBITS for the secondary table, to ensure + valid behavior for advanceBits when reading this symbol. */ + tree->table_len[i] = (i < headsize) ? 1 : (FIRSTBITS + 1); + tree->table_value[i] = INVALIDSYMBOL; + } + } + } else { + /* A good huffman tree has N * 2 - 1 nodes, of which N - 1 are internal nodes. + If that is not the case (due to too long length codes), the table will not + have been fully used, and this is an error (not all bit combinations can be + decoded): an oversubscribed huffman tree, indicated by error 55. */ + for(i = 0; i < size; ++i) { + if(tree->table_len[i] == 16) return 55; + } + } + + return 0; +} + +/* +Second step for the ...makeFromLengths and ...makeFromFrequencies functions. +numcodes, lengths and maxbitlen must already be filled in correctly. return +value is error. +*/ +static unsigned HuffmanTree_makeFromLengths2(HuffmanTree* tree) { + unsigned* blcount; + unsigned* nextcode; + unsigned error = 0; + unsigned bits, n; + + tree->codes = (unsigned*)lodepng_malloc(tree->numcodes * sizeof(unsigned)); + blcount = (unsigned*)lodepng_malloc((tree->maxbitlen + 1) * sizeof(unsigned)); + nextcode = (unsigned*)lodepng_malloc((tree->maxbitlen + 1) * sizeof(unsigned)); + if(!tree->codes || !blcount || !nextcode) error = 83; /*alloc fail*/ + + if(!error) { + for(n = 0; n != tree->maxbitlen + 1; n++) blcount[n] = nextcode[n] = 0; + /*step 1: count number of instances of each code length*/ + for(bits = 0; bits != tree->numcodes; ++bits) ++blcount[tree->lengths[bits]]; + /*step 2: generate the nextcode values*/ + for(bits = 1; bits <= tree->maxbitlen; ++bits) { + nextcode[bits] = (nextcode[bits - 1] + blcount[bits - 1]) << 1u; + } + /*step 3: generate all the codes*/ + for(n = 0; n != tree->numcodes; ++n) { + if(tree->lengths[n] != 0) { + tree->codes[n] = nextcode[tree->lengths[n]]++; + /*remove superfluous bits from the code*/ + tree->codes[n] &= ((1u << tree->lengths[n]) - 1u); + } + } + } + + lodepng_free(blcount); + lodepng_free(nextcode); + + if(!error) error = HuffmanTree_makeTable(tree); + return error; +} + +/* +given the code lengths (as stored in the PNG file), generate the tree as defined +by Deflate. maxbitlen is the maximum bits that a code in the tree can have. +return value is error. +*/ +static unsigned HuffmanTree_makeFromLengths(HuffmanTree* tree, const unsigned* bitlen, + size_t numcodes, unsigned maxbitlen) { + unsigned i; + tree->lengths = (unsigned*)lodepng_malloc(numcodes * sizeof(unsigned)); + if(!tree->lengths) return 83; /*alloc fail*/ + for(i = 0; i != numcodes; ++i) tree->lengths[i] = bitlen[i]; + tree->numcodes = (unsigned)numcodes; /*number of symbols*/ + tree->maxbitlen = maxbitlen; + return HuffmanTree_makeFromLengths2(tree); +} + +#ifdef LODEPNG_COMPILE_ENCODER + +/*BPM: Boundary Package Merge, see "A Fast and Space-Economical Algorithm for Length-Limited Coding", +Jyrki Katajainen, Alistair Moffat, Andrew Turpin, 1995.*/ + +/*chain node for boundary package merge*/ +typedef struct BPMNode { + int weight; /*the sum of all weights in this chain*/ + unsigned index; /*index of this leaf node (called "count" in the paper)*/ + struct BPMNode* tail; /*the next nodes in this chain (null if last)*/ + int in_use; +} BPMNode; + +/*lists of chains*/ +typedef struct BPMLists { + /*memory pool*/ + unsigned memsize; + BPMNode* memory; + unsigned numfree; + unsigned nextfree; + BPMNode** freelist; + /*two heads of lookahead chains per list*/ + unsigned listsize; + BPMNode** chains0; + BPMNode** chains1; +} BPMLists; + +/*creates a new chain node with the given parameters, from the memory in the lists */ +static BPMNode* bpmnode_create(BPMLists* lists, int weight, unsigned index, BPMNode* tail) { + unsigned i; + BPMNode* result; + + /*memory full, so garbage collect*/ + if(lists->nextfree >= lists->numfree) { + /*mark only those that are in use*/ + for(i = 0; i != lists->memsize; ++i) lists->memory[i].in_use = 0; + for(i = 0; i != lists->listsize; ++i) { + BPMNode* node; + for(node = lists->chains0[i]; node != 0; node = node->tail) node->in_use = 1; + for(node = lists->chains1[i]; node != 0; node = node->tail) node->in_use = 1; + } + /*collect those that are free*/ + lists->numfree = 0; + for(i = 0; i != lists->memsize; ++i) { + if(!lists->memory[i].in_use) lists->freelist[lists->numfree++] = &lists->memory[i]; + } + lists->nextfree = 0; + } + + result = lists->freelist[lists->nextfree++]; + result->weight = weight; + result->index = index; + result->tail = tail; + return result; +} + +/*sort the leaves with stable mergesort*/ +static void bpmnode_sort(BPMNode* leaves, size_t num) { + BPMNode* mem = (BPMNode*)lodepng_malloc(sizeof(*leaves) * num); + size_t width, counter = 0; + for(width = 1; width < num; width *= 2) { + BPMNode* a = (counter & 1) ? mem : leaves; + BPMNode* b = (counter & 1) ? leaves : mem; + size_t p; + for(p = 0; p < num; p += 2 * width) { + size_t q = (p + width > num) ? num : (p + width); + size_t r = (p + 2 * width > num) ? num : (p + 2 * width); + size_t i = p, j = q, k; + for(k = p; k < r; k++) { + if(i < q && (j >= r || a[i].weight <= a[j].weight)) b[k] = a[i++]; + else b[k] = a[j++]; + } + } + counter++; + } + if(counter & 1) lodepng_memcpy(leaves, mem, sizeof(*leaves) * num); + lodepng_free(mem); +} + +/*Boundary Package Merge step, numpresent is the amount of leaves, and c is the current chain.*/ +static void boundaryPM(BPMLists* lists, BPMNode* leaves, size_t numpresent, int c, int num) { + unsigned lastindex = lists->chains1[c]->index; + + if(c == 0) { + if(lastindex >= numpresent) return; + lists->chains0[c] = lists->chains1[c]; + lists->chains1[c] = bpmnode_create(lists, leaves[lastindex].weight, lastindex + 1, 0); + } else { + /*sum of the weights of the head nodes of the previous lookahead chains.*/ + int sum = lists->chains0[c - 1]->weight + lists->chains1[c - 1]->weight; + lists->chains0[c] = lists->chains1[c]; + if(lastindex < numpresent && sum > leaves[lastindex].weight) { + lists->chains1[c] = bpmnode_create(lists, leaves[lastindex].weight, lastindex + 1, lists->chains1[c]->tail); + return; + } + lists->chains1[c] = bpmnode_create(lists, sum, lastindex, lists->chains1[c - 1]); + /*in the end we are only interested in the chain of the last list, so no + need to recurse if we're at the last one (this gives measurable speedup)*/ + if(num + 1 < (int)(2 * numpresent - 2)) { + boundaryPM(lists, leaves, numpresent, c - 1, num); + boundaryPM(lists, leaves, numpresent, c - 1, num); + } + } +} + +unsigned lodepng_huffman_code_lengths(unsigned* lengths, const unsigned* frequencies, + size_t numcodes, unsigned maxbitlen) { + unsigned error = 0; + unsigned i; + size_t numpresent = 0; /*number of symbols with non-zero frequency*/ + BPMNode* leaves; /*the symbols, only those with > 0 frequency*/ + + if(numcodes == 0) return 80; /*error: a tree of 0 symbols is not supposed to be made*/ + if((1u << maxbitlen) < (unsigned)numcodes) return 80; /*error: represent all symbols*/ + + leaves = (BPMNode*)lodepng_malloc(numcodes * sizeof(*leaves)); + if(!leaves) return 83; /*alloc fail*/ + + for(i = 0; i != numcodes; ++i) { + if(frequencies[i] > 0) { + leaves[numpresent].weight = (int)frequencies[i]; + leaves[numpresent].index = i; + ++numpresent; + } + } + + lodepng_memset(lengths, 0, numcodes * sizeof(*lengths)); + + /*ensure at least two present symbols. There should be at least one symbol + according to RFC 1951 section 3.2.7. Some decoders incorrectly require two. To + make these work as well ensure there are at least two symbols. The + Package-Merge code below also doesn't work correctly if there's only one + symbol, it'd give it the theoretical 0 bits but in practice zlib wants 1 bit*/ + if(numpresent == 0) { + lengths[0] = lengths[1] = 1; /*note that for RFC 1951 section 3.2.7, only lengths[0] = 1 is needed*/ + } else if(numpresent == 1) { + lengths[leaves[0].index] = 1; + lengths[leaves[0].index == 0 ? 1 : 0] = 1; + } else { + BPMLists lists; + BPMNode* node; + + bpmnode_sort(leaves, numpresent); + + lists.listsize = maxbitlen; + lists.memsize = 2 * maxbitlen * (maxbitlen + 1); + lists.nextfree = 0; + lists.numfree = lists.memsize; + lists.memory = (BPMNode*)lodepng_malloc(lists.memsize * sizeof(*lists.memory)); + lists.freelist = (BPMNode**)lodepng_malloc(lists.memsize * sizeof(BPMNode*)); + lists.chains0 = (BPMNode**)lodepng_malloc(lists.listsize * sizeof(BPMNode*)); + lists.chains1 = (BPMNode**)lodepng_malloc(lists.listsize * sizeof(BPMNode*)); + if(!lists.memory || !lists.freelist || !lists.chains0 || !lists.chains1) error = 83; /*alloc fail*/ + + if(!error) { + for(i = 0; i != lists.memsize; ++i) lists.freelist[i] = &lists.memory[i]; + + bpmnode_create(&lists, leaves[0].weight, 1, 0); + bpmnode_create(&lists, leaves[1].weight, 2, 0); + + for(i = 0; i != lists.listsize; ++i) { + lists.chains0[i] = &lists.memory[0]; + lists.chains1[i] = &lists.memory[1]; + } + + /*each boundaryPM call adds one chain to the last list, and we need 2 * numpresent - 2 chains.*/ + for(i = 2; i != 2 * numpresent - 2; ++i) boundaryPM(&lists, leaves, numpresent, (int)maxbitlen - 1, (int)i); + + for(node = lists.chains1[maxbitlen - 1]; node; node = node->tail) { + for(i = 0; i != node->index; ++i) ++lengths[leaves[i].index]; + } + } + + lodepng_free(lists.memory); + lodepng_free(lists.freelist); + lodepng_free(lists.chains0); + lodepng_free(lists.chains1); + } + + lodepng_free(leaves); + return error; +} + +/*Create the Huffman tree given the symbol frequencies*/ +static unsigned HuffmanTree_makeFromFrequencies(HuffmanTree* tree, const unsigned* frequencies, + size_t mincodes, size_t numcodes, unsigned maxbitlen) { + unsigned error = 0; + while(!frequencies[numcodes - 1] && numcodes > mincodes) --numcodes; /*trim zeroes*/ + tree->lengths = (unsigned*)lodepng_malloc(numcodes * sizeof(unsigned)); + if(!tree->lengths) return 83; /*alloc fail*/ + tree->maxbitlen = maxbitlen; + tree->numcodes = (unsigned)numcodes; /*number of symbols*/ + + error = lodepng_huffman_code_lengths(tree->lengths, frequencies, numcodes, maxbitlen); + if(!error) error = HuffmanTree_makeFromLengths2(tree); + return error; +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +/*get the literal and length code tree of a deflated block with fixed tree, as per the deflate specification*/ +static unsigned generateFixedLitLenTree(HuffmanTree* tree) { + unsigned i, error = 0; + unsigned* bitlen = (unsigned*)lodepng_malloc(NUM_DEFLATE_CODE_SYMBOLS * sizeof(unsigned)); + if(!bitlen) return 83; /*alloc fail*/ + + /*288 possible codes: 0-255=literals, 256=endcode, 257-285=lengthcodes, 286-287=unused*/ + for(i = 0; i <= 143; ++i) bitlen[i] = 8; + for(i = 144; i <= 255; ++i) bitlen[i] = 9; + for(i = 256; i <= 279; ++i) bitlen[i] = 7; + for(i = 280; i <= 287; ++i) bitlen[i] = 8; + + error = HuffmanTree_makeFromLengths(tree, bitlen, NUM_DEFLATE_CODE_SYMBOLS, 15); + + lodepng_free(bitlen); + return error; +} + +/*get the distance code tree of a deflated block with fixed tree, as specified in the deflate specification*/ +static unsigned generateFixedDistanceTree(HuffmanTree* tree) { + unsigned i, error = 0; + unsigned* bitlen = (unsigned*)lodepng_malloc(NUM_DISTANCE_SYMBOLS * sizeof(unsigned)); + if(!bitlen) return 83; /*alloc fail*/ + + /*there are 32 distance codes, but 30-31 are unused*/ + for(i = 0; i != NUM_DISTANCE_SYMBOLS; ++i) bitlen[i] = 5; + error = HuffmanTree_makeFromLengths(tree, bitlen, NUM_DISTANCE_SYMBOLS, 15); + + lodepng_free(bitlen); + return error; +} + +#ifdef LODEPNG_COMPILE_DECODER + +/* +returns the code. The bit reader must already have been ensured at least 15 bits +*/ +static unsigned huffmanDecodeSymbol(LodePNGBitReader* reader, const HuffmanTree* codetree) { + unsigned short code = peekBits(reader, FIRSTBITS); + unsigned short l = codetree->table_len[code]; + unsigned short value = codetree->table_value[code]; + if(l <= FIRSTBITS) { + advanceBits(reader, l); + return value; + } else { + advanceBits(reader, FIRSTBITS); + value += peekBits(reader, l - FIRSTBITS); + advanceBits(reader, codetree->table_len[value] - FIRSTBITS); + return codetree->table_value[value]; + } +} +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_DECODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Inflator (Decompressor) / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*get the tree of a deflated block with fixed tree, as specified in the deflate specification +Returns error code.*/ +static unsigned getTreeInflateFixed(HuffmanTree* tree_ll, HuffmanTree* tree_d) { + unsigned error = generateFixedLitLenTree(tree_ll); + if(error) return error; + return generateFixedDistanceTree(tree_d); +} + +/*get the tree of a deflated block with dynamic tree, the tree itself is also Huffman compressed with a known tree*/ +static unsigned getTreeInflateDynamic(HuffmanTree* tree_ll, HuffmanTree* tree_d, + LodePNGBitReader* reader) { + /*make sure that length values that aren't filled in will be 0, or a wrong tree will be generated*/ + unsigned error = 0; + unsigned n, HLIT, HDIST, HCLEN, i; + + /*see comments in deflateDynamic for explanation of the context and these variables, it is analogous*/ + unsigned* bitlen_ll = 0; /*lit,len code lengths*/ + unsigned* bitlen_d = 0; /*dist code lengths*/ + /*code length code lengths ("clcl"), the bit lengths of the huffman tree used to compress bitlen_ll and bitlen_d*/ + unsigned* bitlen_cl = 0; + HuffmanTree tree_cl; /*the code tree for code length codes (the huffman tree for compressed huffman trees)*/ + + if(reader->bitsize - reader->bp < 14) return 49; /*error: the bit pointer is or will go past the memory*/ + ensureBits17(reader, 14); + + /*number of literal/length codes + 257. Unlike the spec, the value 257 is added to it here already*/ + HLIT = readBits(reader, 5) + 257; + /*number of distance codes. Unlike the spec, the value 1 is added to it here already*/ + HDIST = readBits(reader, 5) + 1; + /*number of code length codes. Unlike the spec, the value 4 is added to it here already*/ + HCLEN = readBits(reader, 4) + 4; + + bitlen_cl = (unsigned*)lodepng_malloc(NUM_CODE_LENGTH_CODES * sizeof(unsigned)); + if(!bitlen_cl) return 83 /*alloc fail*/; + + HuffmanTree_init(&tree_cl); + + while(!error) { + /*read the code length codes out of 3 * (amount of code length codes) bits*/ + if(lodepng_gtofl(reader->bp, HCLEN * 3, reader->bitsize)) { + ERROR_BREAK(50); /*error: the bit pointer is or will go past the memory*/ + } + for(i = 0; i != HCLEN; ++i) { + ensureBits9(reader, 3); /*out of bounds already checked above */ + bitlen_cl[CLCL_ORDER[i]] = readBits(reader, 3); + } + for(i = HCLEN; i != NUM_CODE_LENGTH_CODES; ++i) { + bitlen_cl[CLCL_ORDER[i]] = 0; + } + + error = HuffmanTree_makeFromLengths(&tree_cl, bitlen_cl, NUM_CODE_LENGTH_CODES, 7); + if(error) break; + + /*now we can use this tree to read the lengths for the tree that this function will return*/ + bitlen_ll = (unsigned*)lodepng_malloc(NUM_DEFLATE_CODE_SYMBOLS * sizeof(unsigned)); + bitlen_d = (unsigned*)lodepng_malloc(NUM_DISTANCE_SYMBOLS * sizeof(unsigned)); + if(!bitlen_ll || !bitlen_d) ERROR_BREAK(83 /*alloc fail*/); + lodepng_memset(bitlen_ll, 0, NUM_DEFLATE_CODE_SYMBOLS * sizeof(*bitlen_ll)); + lodepng_memset(bitlen_d, 0, NUM_DISTANCE_SYMBOLS * sizeof(*bitlen_d)); + + /*i is the current symbol we're reading in the part that contains the code lengths of lit/len and dist codes*/ + i = 0; + while(i < HLIT + HDIST) { + unsigned code; + ensureBits25(reader, 22); /* up to 15 bits for huffman code, up to 7 extra bits below*/ + code = huffmanDecodeSymbol(reader, &tree_cl); + if(code <= 15) /*a length code*/ { + if(i < HLIT) bitlen_ll[i] = code; + else bitlen_d[i - HLIT] = code; + ++i; + } else if(code == 16) /*repeat previous*/ { + unsigned replength = 3; /*read in the 2 bits that indicate repeat length (3-6)*/ + unsigned value; /*set value to the previous code*/ + + if(i == 0) ERROR_BREAK(54); /*can't repeat previous if i is 0*/ + + replength += readBits(reader, 2); + + if(i < HLIT + 1) value = bitlen_ll[i - 1]; + else value = bitlen_d[i - HLIT - 1]; + /*repeat this value in the next lengths*/ + for(n = 0; n < replength; ++n) { + if(i >= HLIT + HDIST) ERROR_BREAK(13); /*error: i is larger than the amount of codes*/ + if(i < HLIT) bitlen_ll[i] = value; + else bitlen_d[i - HLIT] = value; + ++i; + } + } else if(code == 17) /*repeat "0" 3-10 times*/ { + unsigned replength = 3; /*read in the bits that indicate repeat length*/ + replength += readBits(reader, 3); + + /*repeat this value in the next lengths*/ + for(n = 0; n < replength; ++n) { + if(i >= HLIT + HDIST) ERROR_BREAK(14); /*error: i is larger than the amount of codes*/ + + if(i < HLIT) bitlen_ll[i] = 0; + else bitlen_d[i - HLIT] = 0; + ++i; + } + } else if(code == 18) /*repeat "0" 11-138 times*/ { + unsigned replength = 11; /*read in the bits that indicate repeat length*/ + replength += readBits(reader, 7); + + /*repeat this value in the next lengths*/ + for(n = 0; n < replength; ++n) { + if(i >= HLIT + HDIST) ERROR_BREAK(15); /*error: i is larger than the amount of codes*/ + + if(i < HLIT) bitlen_ll[i] = 0; + else bitlen_d[i - HLIT] = 0; + ++i; + } + } else /*if(code == INVALIDSYMBOL)*/ { + ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ + } + /*check if any of the ensureBits above went out of bounds*/ + if(reader->bp > reader->bitsize) { + /*return error code 10 or 11 depending on the situation that happened in huffmanDecodeSymbol + (10=no endcode, 11=wrong jump outside of tree)*/ + /* TODO: revise error codes 10,11,50: the above comment is no longer valid */ + ERROR_BREAK(50); /*error, bit pointer jumps past memory*/ + } + } + if(error) break; + + if(bitlen_ll[256] == 0) ERROR_BREAK(64); /*the length of the end code 256 must be larger than 0*/ + + /*now we've finally got HLIT and HDIST, so generate the code trees, and the function is done*/ + error = HuffmanTree_makeFromLengths(tree_ll, bitlen_ll, NUM_DEFLATE_CODE_SYMBOLS, 15); + if(error) break; + error = HuffmanTree_makeFromLengths(tree_d, bitlen_d, NUM_DISTANCE_SYMBOLS, 15); + + break; /*end of error-while*/ + } + + lodepng_free(bitlen_cl); + lodepng_free(bitlen_ll); + lodepng_free(bitlen_d); + HuffmanTree_cleanup(&tree_cl); + + return error; +} + +/*inflate a block with dynamic of fixed Huffman tree. btype must be 1 or 2.*/ +static unsigned inflateHuffmanBlock(ucvector* out, LodePNGBitReader* reader, + unsigned btype, size_t max_output_size) { + unsigned error = 0; + HuffmanTree tree_ll; /*the huffman tree for literal and length codes*/ + HuffmanTree tree_d; /*the huffman tree for distance codes*/ + const size_t reserved_size = 260; /* must be at least 258 for max length, and a few extra for adding a few extra literals */ + int done = 0; + + if(!ucvector_reserve(out, out->size + reserved_size)) return 83; /*alloc fail*/ + + HuffmanTree_init(&tree_ll); + HuffmanTree_init(&tree_d); + + if(btype == 1) error = getTreeInflateFixed(&tree_ll, &tree_d); + else /*if(btype == 2)*/ error = getTreeInflateDynamic(&tree_ll, &tree_d, reader); + + + while(!error && !done) /*decode all symbols until end reached, breaks at end code*/ { + /*code_ll is literal, length or end code*/ + unsigned code_ll; + /* ensure enough bits for 2 huffman code reads (15 bits each): if the first is a literal, a second literal is read at once. This + appears to be slightly faster, than ensuring 20 bits here for 1 huffman symbol and the potential 5 extra bits for the length symbol.*/ + ensureBits32(reader, 30); + code_ll = huffmanDecodeSymbol(reader, &tree_ll); + if(code_ll <= 255) { + /*slightly faster code path if multiple literals in a row*/ + out->data[out->size++] = (unsigned char)code_ll; + code_ll = huffmanDecodeSymbol(reader, &tree_ll); + } + if(code_ll <= 255) /*literal symbol*/ { + out->data[out->size++] = (unsigned char)code_ll; + } else if(code_ll >= FIRST_LENGTH_CODE_INDEX && code_ll <= LAST_LENGTH_CODE_INDEX) /*length code*/ { + unsigned code_d, distance; + unsigned numextrabits_l, numextrabits_d; /*extra bits for length and distance*/ + size_t start, backward, length; + + /*part 1: get length base*/ + length = LENGTHBASE[code_ll - FIRST_LENGTH_CODE_INDEX]; + + /*part 2: get extra bits and add the value of that to length*/ + numextrabits_l = LENGTHEXTRA[code_ll - FIRST_LENGTH_CODE_INDEX]; + if(numextrabits_l != 0) { + /* bits already ensured above */ + ensureBits25(reader, 5); + length += readBits(reader, numextrabits_l); + } + + /*part 3: get distance code*/ + ensureBits32(reader, 28); /* up to 15 for the huffman symbol, up to 13 for the extra bits */ + code_d = huffmanDecodeSymbol(reader, &tree_d); + if(code_d > 29) { + if(code_d <= 31) { + ERROR_BREAK(18); /*error: invalid distance code (30-31 are never used)*/ + } else /* if(code_d == INVALIDSYMBOL) */{ + ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ + } + } + distance = DISTANCEBASE[code_d]; + + /*part 4: get extra bits from distance*/ + numextrabits_d = DISTANCEEXTRA[code_d]; + if(numextrabits_d != 0) { + /* bits already ensured above */ + distance += readBits(reader, numextrabits_d); + } + + /*part 5: fill in all the out[n] values based on the length and dist*/ + start = out->size; + if(distance > start) ERROR_BREAK(52); /*too long backward distance*/ + backward = start - distance; + + out->size += length; + if(distance < length) { + size_t forward; + lodepng_memcpy(out->data + start, out->data + backward, distance); + start += distance; + for(forward = distance; forward < length; ++forward) { + out->data[start++] = out->data[backward++]; + } + } else { + lodepng_memcpy(out->data + start, out->data + backward, length); + } + } else if(code_ll == 256) { + done = 1; /*end code, finish the loop*/ + } else /*if(code_ll == INVALIDSYMBOL)*/ { + ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ + } + if(out->allocsize - out->size < reserved_size) { + if(!ucvector_reserve(out, out->size + reserved_size)) ERROR_BREAK(83); /*alloc fail*/ + } + /*check if any of the ensureBits above went out of bounds*/ + if(reader->bp > reader->bitsize) { + /*return error code 10 or 11 depending on the situation that happened in huffmanDecodeSymbol + (10=no endcode, 11=wrong jump outside of tree)*/ + /* TODO: revise error codes 10,11,50: the above comment is no longer valid */ + ERROR_BREAK(51); /*error, bit pointer jumps past memory*/ + } + if(max_output_size && out->size > max_output_size) { + ERROR_BREAK(109); /*error, larger than max size*/ + } + } + + HuffmanTree_cleanup(&tree_ll); + HuffmanTree_cleanup(&tree_d); + + return error; +} + +static unsigned inflateNoCompression(ucvector* out, LodePNGBitReader* reader, + const LodePNGDecompressSettings* settings) { + size_t bytepos; + size_t size = reader->size; + unsigned LEN, NLEN, error = 0; + + /*go to first boundary of byte*/ + bytepos = (reader->bp + 7u) >> 3u; + + /*read LEN (2 bytes) and NLEN (2 bytes)*/ + if(bytepos + 4 >= size) return 52; /*error, bit pointer will jump past memory*/ + LEN = (unsigned)reader->data[bytepos] + ((unsigned)reader->data[bytepos + 1] << 8u); bytepos += 2; + NLEN = (unsigned)reader->data[bytepos] + ((unsigned)reader->data[bytepos + 1] << 8u); bytepos += 2; + + /*check if 16-bit NLEN is really the one's complement of LEN*/ + if(!settings->ignore_nlen && LEN + NLEN != 65535) { + return 21; /*error: NLEN is not one's complement of LEN*/ + } + + if(!ucvector_resize(out, out->size + LEN)) return 83; /*alloc fail*/ + + /*read the literal data: LEN bytes are now stored in the out buffer*/ + if(bytepos + LEN > size) return 23; /*error: reading outside of in buffer*/ + + /*out->data can be NULL (when LEN is zero), and arithmetic on NULL ptr is undefined*/ + if (LEN) { + lodepng_memcpy(out->data + out->size - LEN, reader->data + bytepos, LEN); + bytepos += LEN; + } + + reader->bp = bytepos << 3u; + + return error; +} + +static unsigned lodepng_inflatev(ucvector* out, + const unsigned char* in, size_t insize, + const LodePNGDecompressSettings* settings) { + unsigned BFINAL = 0; + LodePNGBitReader reader; + unsigned error = LodePNGBitReader_init(&reader, in, insize); + + if(error) return error; + + while(!BFINAL) { + unsigned BTYPE; + if(reader.bitsize - reader.bp < 3) return 52; /*error, bit pointer will jump past memory*/ + ensureBits9(&reader, 3); + BFINAL = readBits(&reader, 1); + BTYPE = readBits(&reader, 2); + + if(BTYPE == 3) return 20; /*error: invalid BTYPE*/ + else if(BTYPE == 0) error = inflateNoCompression(out, &reader, settings); /*no compression*/ + else error = inflateHuffmanBlock(out, &reader, BTYPE, settings->max_output_size); /*compression, BTYPE 01 or 10*/ + if(!error && settings->max_output_size && out->size > settings->max_output_size) error = 109; + if(error) break; + } + + return error; +} + +unsigned lodepng_inflate(unsigned char** out, size_t* outsize, + const unsigned char* in, size_t insize, + const LodePNGDecompressSettings* settings) { + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_inflatev(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + return error; +} + +static unsigned inflatev(ucvector* out, const unsigned char* in, size_t insize, + const LodePNGDecompressSettings* settings) { + if(settings->custom_inflate) { + unsigned error = settings->custom_inflate(&out->data, &out->size, in, insize, settings); + out->allocsize = out->size; + if(error) { + /*the custom inflate is allowed to have its own error codes, however, we translate it to code 110*/ + error = 110; + /*if there's a max output size, and the custom zlib returned error, then indicate that error instead*/ + if(settings->max_output_size && out->size > settings->max_output_size) error = 109; + } + return error; + } else { + return lodepng_inflatev(out, in, insize, settings); + } +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Deflator (Compressor) / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +static const unsigned MAX_SUPPORTED_DEFLATE_LENGTH = 258; + +/*search the index in the array, that has the largest value smaller than or equal to the given value, +given array must be sorted (if no value is smaller, it returns the size of the given array)*/ +static size_t searchCodeIndex(const unsigned* array, size_t array_size, size_t value) { + /*binary search (only small gain over linear). TODO: use CPU log2 instruction for getting symbols instead*/ + size_t left = 1; + size_t right = array_size - 1; + + while(left <= right) { + size_t mid = (left + right) >> 1; + if(array[mid] >= value) right = mid - 1; + else left = mid + 1; + } + if(left >= array_size || array[left] > value) left--; + return left; +} + +static void addLengthDistance(uivector* values, size_t length, size_t distance) { + /*values in encoded vector are those used by deflate: + 0-255: literal bytes + 256: end + 257-285: length/distance pair (length code, followed by extra length bits, distance code, extra distance bits) + 286-287: invalid*/ + + unsigned length_code = (unsigned)searchCodeIndex(LENGTHBASE, 29, length); + unsigned extra_length = (unsigned)(length - LENGTHBASE[length_code]); + unsigned dist_code = (unsigned)searchCodeIndex(DISTANCEBASE, 30, distance); + unsigned extra_distance = (unsigned)(distance - DISTANCEBASE[dist_code]); + + size_t pos = values->size; + /*TODO: return error when this fails (out of memory)*/ + unsigned ok = uivector_resize(values, values->size + 4); + if(ok) { + values->data[pos + 0] = length_code + FIRST_LENGTH_CODE_INDEX; + values->data[pos + 1] = extra_length; + values->data[pos + 2] = dist_code; + values->data[pos + 3] = extra_distance; + } +} + +/*3 bytes of data get encoded into two bytes. The hash cannot use more than 3 +bytes as input because 3 is the minimum match length for deflate*/ +static const unsigned HASH_NUM_VALUES = 65536; +static const unsigned HASH_BIT_MASK = 65535; /*HASH_NUM_VALUES - 1, but C90 does not like that as initializer*/ + +typedef struct Hash { + int* head; /*hash value to head circular pos - can be outdated if went around window*/ + /*circular pos to prev circular pos*/ + unsigned short* chain; + int* val; /*circular pos to hash value*/ + + /*TODO: do this not only for zeros but for any repeated byte. However for PNG + it's always going to be the zeros that dominate, so not important for PNG*/ + int* headz; /*similar to head, but for chainz*/ + unsigned short* chainz; /*those with same amount of zeros*/ + unsigned short* zeros; /*length of zeros streak, used as a second hash chain*/ +} Hash; + +static unsigned hash_init(Hash* hash, unsigned windowsize) { + unsigned i; + hash->head = (int*)lodepng_malloc(sizeof(int) * HASH_NUM_VALUES); + hash->val = (int*)lodepng_malloc(sizeof(int) * windowsize); + hash->chain = (unsigned short*)lodepng_malloc(sizeof(unsigned short) * windowsize); + + hash->zeros = (unsigned short*)lodepng_malloc(sizeof(unsigned short) * windowsize); + hash->headz = (int*)lodepng_malloc(sizeof(int) * (MAX_SUPPORTED_DEFLATE_LENGTH + 1)); + hash->chainz = (unsigned short*)lodepng_malloc(sizeof(unsigned short) * windowsize); + + if(!hash->head || !hash->chain || !hash->val || !hash->headz|| !hash->chainz || !hash->zeros) { + return 83; /*alloc fail*/ + } + + /*initialize hash table*/ + for(i = 0; i != HASH_NUM_VALUES; ++i) hash->head[i] = -1; + for(i = 0; i != windowsize; ++i) hash->val[i] = -1; + for(i = 0; i != windowsize; ++i) hash->chain[i] = i; /*same value as index indicates uninitialized*/ + + for(i = 0; i <= MAX_SUPPORTED_DEFLATE_LENGTH; ++i) hash->headz[i] = -1; + for(i = 0; i != windowsize; ++i) hash->chainz[i] = i; /*same value as index indicates uninitialized*/ + + return 0; +} + +static void hash_cleanup(Hash* hash) { + lodepng_free(hash->head); + lodepng_free(hash->val); + lodepng_free(hash->chain); + + lodepng_free(hash->zeros); + lodepng_free(hash->headz); + lodepng_free(hash->chainz); +} + + + +static unsigned getHash(const unsigned char* data, size_t size, size_t pos) { + unsigned result = 0; + if(pos + 2 < size) { + /*A simple shift and xor hash is used. Since the data of PNGs is dominated + by zeroes due to the filters, a better hash does not have a significant + effect on speed in traversing the chain, and causes more time spend on + calculating the hash.*/ + result ^= ((unsigned)data[pos + 0] << 0u); + result ^= ((unsigned)data[pos + 1] << 4u); + result ^= ((unsigned)data[pos + 2] << 8u); + } else { + size_t amount, i; + if(pos >= size) return 0; + amount = size - pos; + for(i = 0; i != amount; ++i) result ^= ((unsigned)data[pos + i] << (i * 8u)); + } + return result & HASH_BIT_MASK; +} + +static unsigned countZeros(const unsigned char* data, size_t size, size_t pos) { + const unsigned char* start = data + pos; + const unsigned char* end = start + MAX_SUPPORTED_DEFLATE_LENGTH; + if(end > data + size) end = data + size; + data = start; + while(data != end && *data == 0) ++data; + /*subtracting two addresses returned as 32-bit number (max value is MAX_SUPPORTED_DEFLATE_LENGTH)*/ + return (unsigned)(data - start); +} + +/*wpos = pos & (windowsize - 1)*/ +static void updateHashChain(Hash* hash, size_t wpos, unsigned hashval, unsigned short numzeros) { + hash->val[wpos] = (int)hashval; + if(hash->head[hashval] != -1) hash->chain[wpos] = hash->head[hashval]; + hash->head[hashval] = (int)wpos; + + hash->zeros[wpos] = numzeros; + if(hash->headz[numzeros] != -1) hash->chainz[wpos] = hash->headz[numzeros]; + hash->headz[numzeros] = (int)wpos; +} + +/* +LZ77-encode the data. Return value is error code. The input are raw bytes, the output +is in the form of unsigned integers with codes representing for example literal bytes, or +length/distance pairs. +It uses a hash table technique to let it encode faster. When doing LZ77 encoding, a +sliding window (of windowsize) is used, and all past bytes in that window can be used as +the "dictionary". A brute force search through all possible distances would be slow, and +this hash technique is one out of several ways to speed this up. +*/ +static unsigned encodeLZ77(uivector* out, Hash* hash, + const unsigned char* in, size_t inpos, size_t insize, unsigned windowsize, + unsigned minmatch, unsigned nicematch, unsigned lazymatching) { + size_t pos; + unsigned i, error = 0; + /*for large window lengths, assume the user wants no compression loss. Otherwise, max hash chain length speedup.*/ + unsigned maxchainlength = windowsize >= 8192 ? windowsize : windowsize / 8u; + unsigned maxlazymatch = windowsize >= 8192 ? MAX_SUPPORTED_DEFLATE_LENGTH : 64; + + unsigned usezeros = 1; /*not sure if setting it to false for windowsize < 8192 is better or worse*/ + unsigned numzeros = 0; + + unsigned offset; /*the offset represents the distance in LZ77 terminology*/ + unsigned length; + unsigned lazy = 0; + unsigned lazylength = 0, lazyoffset = 0; + unsigned hashval; + unsigned current_offset, current_length; + unsigned prev_offset; + const unsigned char *lastptr, *foreptr, *backptr; + unsigned hashpos; + + if(windowsize == 0 || windowsize > 32768) return 60; /*error: windowsize smaller/larger than allowed*/ + if((windowsize & (windowsize - 1)) != 0) return 90; /*error: must be power of two*/ + + if(nicematch > MAX_SUPPORTED_DEFLATE_LENGTH) nicematch = MAX_SUPPORTED_DEFLATE_LENGTH; + + for(pos = inpos; pos < insize; ++pos) { + size_t wpos = pos & (windowsize - 1); /*position for in 'circular' hash buffers*/ + unsigned chainlength = 0; + + hashval = getHash(in, insize, pos); + + if(usezeros && hashval == 0) { + if(numzeros == 0) numzeros = countZeros(in, insize, pos); + else if(pos + numzeros > insize || in[pos + numzeros - 1] != 0) --numzeros; + } else { + numzeros = 0; + } + + updateHashChain(hash, wpos, hashval, numzeros); + + /*the length and offset found for the current position*/ + length = 0; + offset = 0; + + hashpos = hash->chain[wpos]; + + lastptr = &in[insize < pos + MAX_SUPPORTED_DEFLATE_LENGTH ? insize : pos + MAX_SUPPORTED_DEFLATE_LENGTH]; + + /*search for the longest string*/ + prev_offset = 0; + for(;;) { + if(chainlength++ >= maxchainlength) break; + current_offset = (unsigned)(hashpos <= wpos ? wpos - hashpos : wpos - hashpos + windowsize); + + if(current_offset < prev_offset) break; /*stop when went completely around the circular buffer*/ + prev_offset = current_offset; + if(current_offset > 0) { + /*test the next characters*/ + foreptr = &in[pos]; + backptr = &in[pos - current_offset]; + + /*common case in PNGs is lots of zeros. Quickly skip over them as a speedup*/ + if(numzeros >= 3) { + unsigned skip = hash->zeros[hashpos]; + if(skip > numzeros) skip = numzeros; + backptr += skip; + foreptr += skip; + } + + while(foreptr != lastptr && *backptr == *foreptr) /*maximum supported length by deflate is max length*/ { + ++backptr; + ++foreptr; + } + current_length = (unsigned)(foreptr - &in[pos]); + + if(current_length > length) { + length = current_length; /*the longest length*/ + offset = current_offset; /*the offset that is related to this longest length*/ + /*jump out once a length of max length is found (speed gain). This also jumps + out if length is MAX_SUPPORTED_DEFLATE_LENGTH*/ + if(current_length >= nicematch) break; + } + } + + if(hashpos == hash->chain[hashpos]) break; + + if(numzeros >= 3 && length > numzeros) { + hashpos = hash->chainz[hashpos]; + if(hash->zeros[hashpos] != numzeros) break; + } else { + hashpos = hash->chain[hashpos]; + /*outdated hash value, happens if particular value was not encountered in whole last window*/ + if(hash->val[hashpos] != (int)hashval) break; + } + } + + if(lazymatching) { + if(!lazy && length >= 3 && length <= maxlazymatch && length < MAX_SUPPORTED_DEFLATE_LENGTH) { + lazy = 1; + lazylength = length; + lazyoffset = offset; + continue; /*try the next byte*/ + } + if(lazy) { + lazy = 0; + if(pos == 0) ERROR_BREAK(81); + if(length > lazylength + 1) { + /*push the previous character as literal*/ + if(!uivector_push_back(out, in[pos - 1])) ERROR_BREAK(83 /*alloc fail*/); + } else { + length = lazylength; + offset = lazyoffset; + hash->head[hashval] = -1; /*the same hashchain update will be done, this ensures no wrong alteration*/ + hash->headz[numzeros] = -1; /*idem*/ + --pos; + } + } + } + if(length >= 3 && offset > windowsize) ERROR_BREAK(86 /*too big (or overflown negative) offset*/); + + /*encode it as length/distance pair or literal value*/ + if(length < 3) /*only lengths of 3 or higher are supported as length/distance pair*/ { + if(!uivector_push_back(out, in[pos])) ERROR_BREAK(83 /*alloc fail*/); + } else if(length < minmatch || (length == 3 && offset > 4096)) { + /*compensate for the fact that longer offsets have more extra bits, a + length of only 3 may be not worth it then*/ + if(!uivector_push_back(out, in[pos])) ERROR_BREAK(83 /*alloc fail*/); + } else { + addLengthDistance(out, length, offset); + for(i = 1; i < length; ++i) { + ++pos; + wpos = pos & (windowsize - 1); + hashval = getHash(in, insize, pos); + if(usezeros && hashval == 0) { + if(numzeros == 0) numzeros = countZeros(in, insize, pos); + else if(pos + numzeros > insize || in[pos + numzeros - 1] != 0) --numzeros; + } else { + numzeros = 0; + } + updateHashChain(hash, wpos, hashval, numzeros); + } + } + } /*end of the loop through each character of input*/ + + return error; +} + +/* /////////////////////////////////////////////////////////////////////////// */ + +static unsigned deflateNoCompression(ucvector* out, const unsigned char* data, size_t datasize) { + /*non compressed deflate block data: 1 bit BFINAL,2 bits BTYPE,(5 bits): it jumps to start of next byte, + 2 bytes LEN, 2 bytes NLEN, LEN bytes literal DATA*/ + + size_t i, numdeflateblocks = (datasize + 65534u) / 65535u; + size_t datapos = 0; + for(i = 0; i != numdeflateblocks; ++i) { + unsigned BFINAL, BTYPE, LEN, NLEN; + unsigned char firstbyte; + size_t pos = out->size; + + BFINAL = (i == numdeflateblocks - 1); + BTYPE = 0; + + LEN = 65535; + if(datasize - datapos < 65535u) LEN = (unsigned)datasize - (unsigned)datapos; + NLEN = 65535 - LEN; + + if(!ucvector_resize(out, out->size + LEN + 5)) return 83; /*alloc fail*/ + + firstbyte = (unsigned char)(BFINAL + ((BTYPE & 1u) << 1u) + ((BTYPE & 2u) << 1u)); + out->data[pos + 0] = firstbyte; + out->data[pos + 1] = (unsigned char)(LEN & 255); + out->data[pos + 2] = (unsigned char)(LEN >> 8u); + out->data[pos + 3] = (unsigned char)(NLEN & 255); + out->data[pos + 4] = (unsigned char)(NLEN >> 8u); + lodepng_memcpy(out->data + pos + 5, data + datapos, LEN); + datapos += LEN; + } + + return 0; +} + +/* +write the lz77-encoded data, which has lit, len and dist codes, to compressed stream using huffman trees. +tree_ll: the tree for lit and len codes. +tree_d: the tree for distance codes. +*/ +static void writeLZ77data(LodePNGBitWriter* writer, const uivector* lz77_encoded, + const HuffmanTree* tree_ll, const HuffmanTree* tree_d) { + size_t i = 0; + for(i = 0; i != lz77_encoded->size; ++i) { + unsigned val = lz77_encoded->data[i]; + writeBitsReversed(writer, tree_ll->codes[val], tree_ll->lengths[val]); + if(val > 256) /*for a length code, 3 more things have to be added*/ { + unsigned length_index = val - FIRST_LENGTH_CODE_INDEX; + unsigned n_length_extra_bits = LENGTHEXTRA[length_index]; + unsigned length_extra_bits = lz77_encoded->data[++i]; + + unsigned distance_code = lz77_encoded->data[++i]; + + unsigned distance_index = distance_code; + unsigned n_distance_extra_bits = DISTANCEEXTRA[distance_index]; + unsigned distance_extra_bits = lz77_encoded->data[++i]; + + writeBits(writer, length_extra_bits, n_length_extra_bits); + writeBitsReversed(writer, tree_d->codes[distance_code], tree_d->lengths[distance_code]); + writeBits(writer, distance_extra_bits, n_distance_extra_bits); + } + } +} + +/*Deflate for a block of type "dynamic", that is, with freely, optimally, created huffman trees*/ +static unsigned deflateDynamic(LodePNGBitWriter* writer, Hash* hash, + const unsigned char* data, size_t datapos, size_t dataend, + const LodePNGCompressSettings* settings, unsigned final) { + unsigned error = 0; + + /* + A block is compressed as follows: The PNG data is lz77 encoded, resulting in + literal bytes and length/distance pairs. This is then huffman compressed with + two huffman trees. One huffman tree is used for the lit and len values ("ll"), + another huffman tree is used for the dist values ("d"). These two trees are + stored using their code lengths, and to compress even more these code lengths + are also run-length encoded and huffman compressed. This gives a huffman tree + of code lengths "cl". The code lengths used to describe this third tree are + the code length code lengths ("clcl"). + */ + + /*The lz77 encoded data, represented with integers since there will also be length and distance codes in it*/ + uivector lz77_encoded; + HuffmanTree tree_ll; /*tree for lit,len values*/ + HuffmanTree tree_d; /*tree for distance codes*/ + HuffmanTree tree_cl; /*tree for encoding the code lengths representing tree_ll and tree_d*/ + unsigned* frequencies_ll = 0; /*frequency of lit,len codes*/ + unsigned* frequencies_d = 0; /*frequency of dist codes*/ + unsigned* frequencies_cl = 0; /*frequency of code length codes*/ + unsigned* bitlen_lld = 0; /*lit,len,dist code lengths (int bits), literally (without repeat codes).*/ + unsigned* bitlen_lld_e = 0; /*bitlen_lld encoded with repeat codes (this is a rudimentary run length compression)*/ + size_t datasize = dataend - datapos; + + /* + If we could call "bitlen_cl" the the code length code lengths ("clcl"), that is the bit lengths of codes to represent + tree_cl in CLCL_ORDER, then due to the huffman compression of huffman tree representations ("two levels"), there are + some analogies: + bitlen_lld is to tree_cl what data is to tree_ll and tree_d. + bitlen_lld_e is to bitlen_lld what lz77_encoded is to data. + bitlen_cl is to bitlen_lld_e what bitlen_lld is to lz77_encoded. + */ + + unsigned BFINAL = final; + size_t i; + size_t numcodes_ll, numcodes_d, numcodes_lld, numcodes_lld_e, numcodes_cl; + unsigned HLIT, HDIST, HCLEN; + + uivector_init(&lz77_encoded); + HuffmanTree_init(&tree_ll); + HuffmanTree_init(&tree_d); + HuffmanTree_init(&tree_cl); + /* could fit on stack, but >1KB is on the larger side so allocate instead */ + frequencies_ll = (unsigned*)lodepng_malloc(286 * sizeof(*frequencies_ll)); + frequencies_d = (unsigned*)lodepng_malloc(30 * sizeof(*frequencies_d)); + frequencies_cl = (unsigned*)lodepng_malloc(NUM_CODE_LENGTH_CODES * sizeof(*frequencies_cl)); + + if(!frequencies_ll || !frequencies_d || !frequencies_cl) error = 83; /*alloc fail*/ + + /*This while loop never loops due to a break at the end, it is here to + allow breaking out of it to the cleanup phase on error conditions.*/ + while(!error) { + lodepng_memset(frequencies_ll, 0, 286 * sizeof(*frequencies_ll)); + lodepng_memset(frequencies_d, 0, 30 * sizeof(*frequencies_d)); + lodepng_memset(frequencies_cl, 0, NUM_CODE_LENGTH_CODES * sizeof(*frequencies_cl)); + + if(settings->use_lz77) { + error = encodeLZ77(&lz77_encoded, hash, data, datapos, dataend, settings->windowsize, + settings->minmatch, settings->nicematch, settings->lazymatching); + if(error) break; + } else { + if(!uivector_resize(&lz77_encoded, datasize)) ERROR_BREAK(83 /*alloc fail*/); + for(i = datapos; i < dataend; ++i) lz77_encoded.data[i - datapos] = data[i]; /*no LZ77, but still will be Huffman compressed*/ + } + + /*Count the frequencies of lit, len and dist codes*/ + for(i = 0; i != lz77_encoded.size; ++i) { + unsigned symbol = lz77_encoded.data[i]; + ++frequencies_ll[symbol]; + if(symbol > 256) { + unsigned dist = lz77_encoded.data[i + 2]; + ++frequencies_d[dist]; + i += 3; + } + } + frequencies_ll[256] = 1; /*there will be exactly 1 end code, at the end of the block*/ + + /*Make both huffman trees, one for the lit and len codes, one for the dist codes*/ + error = HuffmanTree_makeFromFrequencies(&tree_ll, frequencies_ll, 257, 286, 15); + if(error) break; + /*2, not 1, is chosen for mincodes: some buggy PNG decoders require at least 2 symbols in the dist tree*/ + error = HuffmanTree_makeFromFrequencies(&tree_d, frequencies_d, 2, 30, 15); + if(error) break; + + numcodes_ll = LODEPNG_MIN(tree_ll.numcodes, 286); + numcodes_d = LODEPNG_MIN(tree_d.numcodes, 30); + /*store the code lengths of both generated trees in bitlen_lld*/ + numcodes_lld = numcodes_ll + numcodes_d; + bitlen_lld = (unsigned*)lodepng_malloc(numcodes_lld * sizeof(*bitlen_lld)); + /*numcodes_lld_e never needs more size than bitlen_lld*/ + bitlen_lld_e = (unsigned*)lodepng_malloc(numcodes_lld * sizeof(*bitlen_lld_e)); + if(!bitlen_lld || !bitlen_lld_e) ERROR_BREAK(83); /*alloc fail*/ + numcodes_lld_e = 0; + + for(i = 0; i != numcodes_ll; ++i) bitlen_lld[i] = tree_ll.lengths[i]; + for(i = 0; i != numcodes_d; ++i) bitlen_lld[numcodes_ll + i] = tree_d.lengths[i]; + + /*run-length compress bitlen_ldd into bitlen_lld_e by using repeat codes 16 (copy length 3-6 times), + 17 (3-10 zeroes), 18 (11-138 zeroes)*/ + for(i = 0; i != numcodes_lld; ++i) { + unsigned j = 0; /*amount of repetitions*/ + while(i + j + 1 < numcodes_lld && bitlen_lld[i + j + 1] == bitlen_lld[i]) ++j; + + if(bitlen_lld[i] == 0 && j >= 2) /*repeat code for zeroes*/ { + ++j; /*include the first zero*/ + if(j <= 10) /*repeat code 17 supports max 10 zeroes*/ { + bitlen_lld_e[numcodes_lld_e++] = 17; + bitlen_lld_e[numcodes_lld_e++] = j - 3; + } else /*repeat code 18 supports max 138 zeroes*/ { + if(j > 138) j = 138; + bitlen_lld_e[numcodes_lld_e++] = 18; + bitlen_lld_e[numcodes_lld_e++] = j - 11; + } + i += (j - 1); + } else if(j >= 3) /*repeat code for value other than zero*/ { + size_t k; + unsigned num = j / 6u, rest = j % 6u; + bitlen_lld_e[numcodes_lld_e++] = bitlen_lld[i]; + for(k = 0; k < num; ++k) { + bitlen_lld_e[numcodes_lld_e++] = 16; + bitlen_lld_e[numcodes_lld_e++] = 6 - 3; + } + if(rest >= 3) { + bitlen_lld_e[numcodes_lld_e++] = 16; + bitlen_lld_e[numcodes_lld_e++] = rest - 3; + } + else j -= rest; + i += j; + } else /*too short to benefit from repeat code*/ { + bitlen_lld_e[numcodes_lld_e++] = bitlen_lld[i]; + } + } + + /*generate tree_cl, the huffmantree of huffmantrees*/ + for(i = 0; i != numcodes_lld_e; ++i) { + ++frequencies_cl[bitlen_lld_e[i]]; + /*after a repeat code come the bits that specify the number of repetitions, + those don't need to be in the frequencies_cl calculation*/ + if(bitlen_lld_e[i] >= 16) ++i; + } + + error = HuffmanTree_makeFromFrequencies(&tree_cl, frequencies_cl, + NUM_CODE_LENGTH_CODES, NUM_CODE_LENGTH_CODES, 7); + if(error) break; + + /*compute amount of code-length-code-lengths to output*/ + numcodes_cl = NUM_CODE_LENGTH_CODES; + /*trim zeros at the end (using CLCL_ORDER), but minimum size must be 4 (see HCLEN below)*/ + while(numcodes_cl > 4u && tree_cl.lengths[CLCL_ORDER[numcodes_cl - 1u]] == 0) { + numcodes_cl--; + } + + /* + Write everything into the output + + After the BFINAL and BTYPE, the dynamic block consists out of the following: + - 5 bits HLIT, 5 bits HDIST, 4 bits HCLEN + - (HCLEN+4)*3 bits code lengths of code length alphabet + - HLIT + 257 code lengths of lit/length alphabet (encoded using the code length + alphabet, + possible repetition codes 16, 17, 18) + - HDIST + 1 code lengths of distance alphabet (encoded using the code length + alphabet, + possible repetition codes 16, 17, 18) + - compressed data + - 256 (end code) + */ + + /*Write block type*/ + writeBits(writer, BFINAL, 1); + writeBits(writer, 0, 1); /*first bit of BTYPE "dynamic"*/ + writeBits(writer, 1, 1); /*second bit of BTYPE "dynamic"*/ + + /*write the HLIT, HDIST and HCLEN values*/ + /*all three sizes take trimmed ending zeroes into account, done either by HuffmanTree_makeFromFrequencies + or in the loop for numcodes_cl above, which saves space. */ + HLIT = (unsigned)(numcodes_ll - 257); + HDIST = (unsigned)(numcodes_d - 1); + HCLEN = (unsigned)(numcodes_cl - 4); + writeBits(writer, HLIT, 5); + writeBits(writer, HDIST, 5); + writeBits(writer, HCLEN, 4); + + /*write the code lengths of the code length alphabet ("bitlen_cl")*/ + for(i = 0; i != numcodes_cl; ++i) writeBits(writer, tree_cl.lengths[CLCL_ORDER[i]], 3); + + /*write the lengths of the lit/len AND the dist alphabet*/ + for(i = 0; i != numcodes_lld_e; ++i) { + writeBitsReversed(writer, tree_cl.codes[bitlen_lld_e[i]], tree_cl.lengths[bitlen_lld_e[i]]); + /*extra bits of repeat codes*/ + if(bitlen_lld_e[i] == 16) writeBits(writer, bitlen_lld_e[++i], 2); + else if(bitlen_lld_e[i] == 17) writeBits(writer, bitlen_lld_e[++i], 3); + else if(bitlen_lld_e[i] == 18) writeBits(writer, bitlen_lld_e[++i], 7); + } + + /*write the compressed data symbols*/ + writeLZ77data(writer, &lz77_encoded, &tree_ll, &tree_d); + /*error: the length of the end code 256 must be larger than 0*/ + if(tree_ll.lengths[256] == 0) ERROR_BREAK(64); + + /*write the end code*/ + writeBitsReversed(writer, tree_ll.codes[256], tree_ll.lengths[256]); + + break; /*end of error-while*/ + } + + /*cleanup*/ + uivector_cleanup(&lz77_encoded); + HuffmanTree_cleanup(&tree_ll); + HuffmanTree_cleanup(&tree_d); + HuffmanTree_cleanup(&tree_cl); + lodepng_free(frequencies_ll); + lodepng_free(frequencies_d); + lodepng_free(frequencies_cl); + lodepng_free(bitlen_lld); + lodepng_free(bitlen_lld_e); + + return error; +} + +static unsigned deflateFixed(LodePNGBitWriter* writer, Hash* hash, + const unsigned char* data, + size_t datapos, size_t dataend, + const LodePNGCompressSettings* settings, unsigned final) { + HuffmanTree tree_ll; /*tree for literal values and length codes*/ + HuffmanTree tree_d; /*tree for distance codes*/ + + unsigned BFINAL = final; + unsigned error = 0; + size_t i; + + HuffmanTree_init(&tree_ll); + HuffmanTree_init(&tree_d); + + error = generateFixedLitLenTree(&tree_ll); + if(!error) error = generateFixedDistanceTree(&tree_d); + + if(!error) { + writeBits(writer, BFINAL, 1); + writeBits(writer, 1, 1); /*first bit of BTYPE*/ + writeBits(writer, 0, 1); /*second bit of BTYPE*/ + + if(settings->use_lz77) /*LZ77 encoded*/ { + uivector lz77_encoded; + uivector_init(&lz77_encoded); + error = encodeLZ77(&lz77_encoded, hash, data, datapos, dataend, settings->windowsize, + settings->minmatch, settings->nicematch, settings->lazymatching); + if(!error) writeLZ77data(writer, &lz77_encoded, &tree_ll, &tree_d); + uivector_cleanup(&lz77_encoded); + } else /*no LZ77, but still will be Huffman compressed*/ { + for(i = datapos; i < dataend; ++i) { + writeBitsReversed(writer, tree_ll.codes[data[i]], tree_ll.lengths[data[i]]); + } + } + /*add END code*/ + if(!error) writeBitsReversed(writer,tree_ll.codes[256], tree_ll.lengths[256]); + } + + /*cleanup*/ + HuffmanTree_cleanup(&tree_ll); + HuffmanTree_cleanup(&tree_d); + + return error; +} + +static unsigned lodepng_deflatev(ucvector* out, const unsigned char* in, size_t insize, + const LodePNGCompressSettings* settings) { + unsigned error = 0; + size_t i, blocksize, numdeflateblocks; + Hash hash; + LodePNGBitWriter writer; + + LodePNGBitWriter_init(&writer, out); + + if(settings->btype > 2) return 61; + else if(settings->btype == 0) return deflateNoCompression(out, in, insize); + else if(settings->btype == 1) blocksize = insize; + else /*if(settings->btype == 2)*/ { + /*on PNGs, deflate blocks of 65-262k seem to give most dense encoding*/ + blocksize = insize / 8u + 8; + if(blocksize < 65536) blocksize = 65536; + if(blocksize > 262144) blocksize = 262144; + } + + numdeflateblocks = (insize + blocksize - 1) / blocksize; + if(numdeflateblocks == 0) numdeflateblocks = 1; + + error = hash_init(&hash, settings->windowsize); + + if(!error) { + for(i = 0; i != numdeflateblocks && !error; ++i) { + unsigned final = (i == numdeflateblocks - 1); + size_t start = i * blocksize; + size_t end = start + blocksize; + if(end > insize) end = insize; + + if(settings->btype == 1) error = deflateFixed(&writer, &hash, in, start, end, settings, final); + else if(settings->btype == 2) error = deflateDynamic(&writer, &hash, in, start, end, settings, final); + } + } + + hash_cleanup(&hash); + + return error; +} + +unsigned lodepng_deflate(unsigned char** out, size_t* outsize, + const unsigned char* in, size_t insize, + const LodePNGCompressSettings* settings) { + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_deflatev(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + return error; +} + +static unsigned deflate(unsigned char** out, size_t* outsize, + const unsigned char* in, size_t insize, + const LodePNGCompressSettings* settings) { + if(settings->custom_deflate) { + unsigned error = settings->custom_deflate(out, outsize, in, insize, settings); + /*the custom deflate is allowed to have its own error codes, however, we translate it to code 111*/ + return error ? 111 : 0; + } else { + return lodepng_deflate(out, outsize, in, insize, settings); + } +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Adler32 / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +static unsigned update_adler32(unsigned adler, const unsigned char* data, unsigned len) { + unsigned s1 = adler & 0xffffu; + unsigned s2 = (adler >> 16u) & 0xffffu; + + while(len != 0u) { + unsigned i; + /*at least 5552 sums can be done before the sums overflow, saving a lot of module divisions*/ + unsigned amount = len > 5552u ? 5552u : len; + len -= amount; + for(i = 0; i != amount; ++i) { + s1 += (*data++); + s2 += s1; + } + s1 %= 65521u; + s2 %= 65521u; + } + + return (s2 << 16u) | s1; +} + +/*Return the adler32 of the bytes data[0..len-1]*/ +static unsigned adler32(const unsigned char* data, unsigned len) { + return update_adler32(1u, data, len); +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Zlib / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_DECODER + +static unsigned lodepng_zlib_decompressv(ucvector* out, + const unsigned char* in, size_t insize, + const LodePNGDecompressSettings* settings) { + unsigned error = 0; + unsigned CM, CINFO, FDICT; + + if(insize < 2) return 53; /*error, size of zlib data too small*/ + /*read information from zlib header*/ + if((in[0] * 256 + in[1]) % 31 != 0) { + /*error: 256 * in[0] + in[1] must be a multiple of 31, the FCHECK value is supposed to be made that way*/ + return 24; + } + + CM = in[0] & 15; + CINFO = (in[0] >> 4) & 15; + /*FCHECK = in[1] & 31;*/ /*FCHECK is already tested above*/ + FDICT = (in[1] >> 5) & 1; + /*FLEVEL = (in[1] >> 6) & 3;*/ /*FLEVEL is not used here*/ + + if(CM != 8 || CINFO > 7) { + /*error: only compression method 8: inflate with sliding window of 32k is supported by the PNG spec*/ + return 25; + } + if(FDICT != 0) { + /*error: the specification of PNG says about the zlib stream: + "The additional flags shall not specify a preset dictionary."*/ + return 26; + } + + error = inflatev(out, in + 2, insize - 2, settings); + if(error) return error; + + if(!settings->ignore_adler32) { + unsigned ADLER32 = lodepng_read32bitInt(&in[insize - 4]); + unsigned checksum = adler32(out->data, (unsigned)(out->size)); + if(checksum != ADLER32) return 58; /*error, adler checksum not correct, data must be corrupted*/ + } + + return 0; /*no error*/ +} + + +unsigned lodepng_zlib_decompress(unsigned char** out, size_t* outsize, const unsigned char* in, + size_t insize, const LodePNGDecompressSettings* settings) { + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_zlib_decompressv(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + return error; +} + +/*expected_size is expected output size, to avoid intermediate allocations. Set to 0 if not known. */ +static unsigned zlib_decompress(unsigned char** out, size_t* outsize, size_t expected_size, + const unsigned char* in, size_t insize, const LodePNGDecompressSettings* settings) { + unsigned error; + if(settings->custom_zlib) { + error = settings->custom_zlib(out, outsize, in, insize, settings); + if(error) { + /*the custom zlib is allowed to have its own error codes, however, we translate it to code 110*/ + error = 110; + /*if there's a max output size, and the custom zlib returned error, then indicate that error instead*/ + if(settings->max_output_size && *outsize > settings->max_output_size) error = 109; + } + } else { + ucvector v = ucvector_init(*out, *outsize); + if(expected_size) { + /*reserve the memory to avoid intermediate reallocations*/ + ucvector_resize(&v, *outsize + expected_size); + v.size = *outsize; + } + error = lodepng_zlib_decompressv(&v, in, insize, settings); + *out = v.data; + *outsize = v.size; + } + return error; +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER + +unsigned lodepng_zlib_compress(unsigned char** out, size_t* outsize, const unsigned char* in, + size_t insize, const LodePNGCompressSettings* settings) { + size_t i; + unsigned error; + unsigned char* deflatedata = 0; + size_t deflatesize = 0; + + error = deflate(&deflatedata, &deflatesize, in, insize, settings); + + *out = NULL; + *outsize = 0; + if(!error) { + *outsize = deflatesize + 6; + *out = (unsigned char*)lodepng_malloc(*outsize); + if(!*out) error = 83; /*alloc fail*/ + } + + if(!error) { + unsigned ADLER32 = adler32(in, (unsigned)insize); + /*zlib data: 1 byte CMF (CM+CINFO), 1 byte FLG, deflate data, 4 byte ADLER32 checksum of the Decompressed data*/ + unsigned CMF = 120; /*0b01111000: CM 8, CINFO 7. With CINFO 7, any window size up to 32768 can be used.*/ + unsigned FLEVEL = 0; + unsigned FDICT = 0; + unsigned CMFFLG = 256 * CMF + FDICT * 32 + FLEVEL * 64; + unsigned FCHECK = 31 - CMFFLG % 31; + CMFFLG += FCHECK; + + (*out)[0] = (unsigned char)(CMFFLG >> 8); + (*out)[1] = (unsigned char)(CMFFLG & 255); + for(i = 0; i != deflatesize; ++i) (*out)[i + 2] = deflatedata[i]; + lodepng_set32bitInt(&(*out)[*outsize - 4], ADLER32); + } + + lodepng_free(deflatedata); + return error; +} + +/* compress using the default or custom zlib function */ +static unsigned zlib_compress(unsigned char** out, size_t* outsize, const unsigned char* in, + size_t insize, const LodePNGCompressSettings* settings) { + if(settings->custom_zlib) { + unsigned error = settings->custom_zlib(out, outsize, in, insize, settings); + /*the custom zlib is allowed to have its own error codes, however, we translate it to code 111*/ + return error ? 111 : 0; + } else { + return lodepng_zlib_compress(out, outsize, in, insize, settings); + } +} + +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#else /*no LODEPNG_COMPILE_ZLIB*/ + +#ifdef LODEPNG_COMPILE_DECODER +static unsigned zlib_decompress(unsigned char** out, size_t* outsize, size_t expected_size, + const unsigned char* in, size_t insize, const LodePNGDecompressSettings* settings) { + if(!settings->custom_zlib) return 87; /*no custom zlib function provided */ + (void)expected_size; + return settings->custom_zlib(out, outsize, in, insize, settings); +} +#endif /*LODEPNG_COMPILE_DECODER*/ +#ifdef LODEPNG_COMPILE_ENCODER +static unsigned zlib_compress(unsigned char** out, size_t* outsize, const unsigned char* in, + size_t insize, const LodePNGCompressSettings* settings) { + if(!settings->custom_zlib) return 87; /*no custom zlib function provided */ + return settings->custom_zlib(out, outsize, in, insize, settings); +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#endif /*LODEPNG_COMPILE_ZLIB*/ + +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_ENCODER + +/*this is a good tradeoff between speed and compression ratio*/ +#define DEFAULT_WINDOWSIZE 2048 + +void lodepng_compress_settings_init(LodePNGCompressSettings* settings) { + /*compress with dynamic huffman tree (not in the mathematical sense, just not the predefined one)*/ + settings->btype = 2; + settings->use_lz77 = 1; + settings->windowsize = DEFAULT_WINDOWSIZE; + settings->minmatch = 3; + settings->nicematch = 128; + settings->lazymatching = 1; + + settings->custom_zlib = 0; + settings->custom_deflate = 0; + settings->custom_context = 0; +} + +const LodePNGCompressSettings lodepng_default_compress_settings = {2, 1, DEFAULT_WINDOWSIZE, 3, 128, 1, 0, 0, 0}; + + +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_DECODER + +void lodepng_decompress_settings_init(LodePNGDecompressSettings* settings) { + settings->ignore_adler32 = 0; + settings->ignore_nlen = 0; + settings->max_output_size = 0; + + settings->custom_zlib = 0; + settings->custom_inflate = 0; + settings->custom_context = 0; +} + +const LodePNGDecompressSettings lodepng_default_decompress_settings = {0, 0, 0, 0, 0, 0}; + +#endif /*LODEPNG_COMPILE_DECODER*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // End of Zlib related code. Begin of PNG related code. // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_PNG + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / CRC32 / */ +/* ////////////////////////////////////////////////////////////////////////// */ + + +#ifdef LODEPNG_COMPILE_CRC + +static const unsigned lodepng_crc32_table0[256] = { + 0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u, 0x9e6495a3u, + 0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu, 0xe7b82d07u, 0x90bf1d91u, + 0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du, 0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, + 0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu, 0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u, + 0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u, 0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, + 0x35b5a8fau, 0x42b2986cu, 0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, + 0x26d930acu, 0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu, + 0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu, 0xb6662d3du, + 0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu, 0x9fbfe4a5u, 0xe8b8d433u, + 0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu, 0x086d3d2du, 0x91646c97u, 0xe6635c01u, + 0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu, 0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, + 0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau, 0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u, + 0x4db26158u, 0x3ab551ceu, 0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, + 0x4369e96au, 0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u, + 0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u, 0xce61e49fu, + 0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u, 0xb7bd5c3bu, 0xc0ba6cadu, + 0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u, 0x9dd277afu, 0x04db2615u, 0x73dc1683u, + 0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u, 0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, + 0xf00f9344u, 0x8708a3d2u, 0x1e01f268u, 0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u, + 0xfed41b76u, 0x89d32be0u, 0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, + 0xd6d6a3e8u, 0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu, + 0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu, 0x4669be79u, + 0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u, 0x220216b9u, 0x5505262fu, + 0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u, 0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du, + 0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au, 0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, + 0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu, 0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, + 0x86d3d2d4u, 0xf1d4e242u, 0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u, + 0x88085ae6u, 0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u, + 0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du, 0x3e6e77dbu, + 0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u, 0x47b2cf7fu, 0x30b5ffe9u, + 0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u, 0xcdd70693u, 0x54de5729u, 0x23d967bfu, + 0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u, 0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du +}; + +static const unsigned lodepng_crc32_table1[256] = { + 0x00000000u, 0x191b3141u, 0x32366282u, 0x2b2d53c3u, 0x646cc504u, 0x7d77f445u, 0x565aa786u, 0x4f4196c7u, + 0xc8d98a08u, 0xd1c2bb49u, 0xfaefe88au, 0xe3f4d9cbu, 0xacb54f0cu, 0xb5ae7e4du, 0x9e832d8eu, 0x87981ccfu, + 0x4ac21251u, 0x53d92310u, 0x78f470d3u, 0x61ef4192u, 0x2eaed755u, 0x37b5e614u, 0x1c98b5d7u, 0x05838496u, + 0x821b9859u, 0x9b00a918u, 0xb02dfadbu, 0xa936cb9au, 0xe6775d5du, 0xff6c6c1cu, 0xd4413fdfu, 0xcd5a0e9eu, + 0x958424a2u, 0x8c9f15e3u, 0xa7b24620u, 0xbea97761u, 0xf1e8e1a6u, 0xe8f3d0e7u, 0xc3de8324u, 0xdac5b265u, + 0x5d5daeaau, 0x44469febu, 0x6f6bcc28u, 0x7670fd69u, 0x39316baeu, 0x202a5aefu, 0x0b07092cu, 0x121c386du, + 0xdf4636f3u, 0xc65d07b2u, 0xed705471u, 0xf46b6530u, 0xbb2af3f7u, 0xa231c2b6u, 0x891c9175u, 0x9007a034u, + 0x179fbcfbu, 0x0e848dbau, 0x25a9de79u, 0x3cb2ef38u, 0x73f379ffu, 0x6ae848beu, 0x41c51b7du, 0x58de2a3cu, + 0xf0794f05u, 0xe9627e44u, 0xc24f2d87u, 0xdb541cc6u, 0x94158a01u, 0x8d0ebb40u, 0xa623e883u, 0xbf38d9c2u, + 0x38a0c50du, 0x21bbf44cu, 0x0a96a78fu, 0x138d96ceu, 0x5ccc0009u, 0x45d73148u, 0x6efa628bu, 0x77e153cau, + 0xbabb5d54u, 0xa3a06c15u, 0x888d3fd6u, 0x91960e97u, 0xded79850u, 0xc7cca911u, 0xece1fad2u, 0xf5facb93u, + 0x7262d75cu, 0x6b79e61du, 0x4054b5deu, 0x594f849fu, 0x160e1258u, 0x0f152319u, 0x243870dau, 0x3d23419bu, + 0x65fd6ba7u, 0x7ce65ae6u, 0x57cb0925u, 0x4ed03864u, 0x0191aea3u, 0x188a9fe2u, 0x33a7cc21u, 0x2abcfd60u, + 0xad24e1afu, 0xb43fd0eeu, 0x9f12832du, 0x8609b26cu, 0xc94824abu, 0xd05315eau, 0xfb7e4629u, 0xe2657768u, + 0x2f3f79f6u, 0x362448b7u, 0x1d091b74u, 0x04122a35u, 0x4b53bcf2u, 0x52488db3u, 0x7965de70u, 0x607eef31u, + 0xe7e6f3feu, 0xfefdc2bfu, 0xd5d0917cu, 0xcccba03du, 0x838a36fau, 0x9a9107bbu, 0xb1bc5478u, 0xa8a76539u, + 0x3b83984bu, 0x2298a90au, 0x09b5fac9u, 0x10aecb88u, 0x5fef5d4fu, 0x46f46c0eu, 0x6dd93fcdu, 0x74c20e8cu, + 0xf35a1243u, 0xea412302u, 0xc16c70c1u, 0xd8774180u, 0x9736d747u, 0x8e2de606u, 0xa500b5c5u, 0xbc1b8484u, + 0x71418a1au, 0x685abb5bu, 0x4377e898u, 0x5a6cd9d9u, 0x152d4f1eu, 0x0c367e5fu, 0x271b2d9cu, 0x3e001cddu, + 0xb9980012u, 0xa0833153u, 0x8bae6290u, 0x92b553d1u, 0xddf4c516u, 0xc4eff457u, 0xefc2a794u, 0xf6d996d5u, + 0xae07bce9u, 0xb71c8da8u, 0x9c31de6bu, 0x852aef2au, 0xca6b79edu, 0xd37048acu, 0xf85d1b6fu, 0xe1462a2eu, + 0x66de36e1u, 0x7fc507a0u, 0x54e85463u, 0x4df36522u, 0x02b2f3e5u, 0x1ba9c2a4u, 0x30849167u, 0x299fa026u, + 0xe4c5aeb8u, 0xfdde9ff9u, 0xd6f3cc3au, 0xcfe8fd7bu, 0x80a96bbcu, 0x99b25afdu, 0xb29f093eu, 0xab84387fu, + 0x2c1c24b0u, 0x350715f1u, 0x1e2a4632u, 0x07317773u, 0x4870e1b4u, 0x516bd0f5u, 0x7a468336u, 0x635db277u, + 0xcbfad74eu, 0xd2e1e60fu, 0xf9ccb5ccu, 0xe0d7848du, 0xaf96124au, 0xb68d230bu, 0x9da070c8u, 0x84bb4189u, + 0x03235d46u, 0x1a386c07u, 0x31153fc4u, 0x280e0e85u, 0x674f9842u, 0x7e54a903u, 0x5579fac0u, 0x4c62cb81u, + 0x8138c51fu, 0x9823f45eu, 0xb30ea79du, 0xaa1596dcu, 0xe554001bu, 0xfc4f315au, 0xd7626299u, 0xce7953d8u, + 0x49e14f17u, 0x50fa7e56u, 0x7bd72d95u, 0x62cc1cd4u, 0x2d8d8a13u, 0x3496bb52u, 0x1fbbe891u, 0x06a0d9d0u, + 0x5e7ef3ecu, 0x4765c2adu, 0x6c48916eu, 0x7553a02fu, 0x3a1236e8u, 0x230907a9u, 0x0824546au, 0x113f652bu, + 0x96a779e4u, 0x8fbc48a5u, 0xa4911b66u, 0xbd8a2a27u, 0xf2cbbce0u, 0xebd08da1u, 0xc0fdde62u, 0xd9e6ef23u, + 0x14bce1bdu, 0x0da7d0fcu, 0x268a833fu, 0x3f91b27eu, 0x70d024b9u, 0x69cb15f8u, 0x42e6463bu, 0x5bfd777au, + 0xdc656bb5u, 0xc57e5af4u, 0xee530937u, 0xf7483876u, 0xb809aeb1u, 0xa1129ff0u, 0x8a3fcc33u, 0x9324fd72u +}; + +static const unsigned lodepng_crc32_table2[256] = { + 0x00000000u, 0x01c26a37u, 0x0384d46eu, 0x0246be59u, 0x0709a8dcu, 0x06cbc2ebu, 0x048d7cb2u, 0x054f1685u, + 0x0e1351b8u, 0x0fd13b8fu, 0x0d9785d6u, 0x0c55efe1u, 0x091af964u, 0x08d89353u, 0x0a9e2d0au, 0x0b5c473du, + 0x1c26a370u, 0x1de4c947u, 0x1fa2771eu, 0x1e601d29u, 0x1b2f0bacu, 0x1aed619bu, 0x18abdfc2u, 0x1969b5f5u, + 0x1235f2c8u, 0x13f798ffu, 0x11b126a6u, 0x10734c91u, 0x153c5a14u, 0x14fe3023u, 0x16b88e7au, 0x177ae44du, + 0x384d46e0u, 0x398f2cd7u, 0x3bc9928eu, 0x3a0bf8b9u, 0x3f44ee3cu, 0x3e86840bu, 0x3cc03a52u, 0x3d025065u, + 0x365e1758u, 0x379c7d6fu, 0x35dac336u, 0x3418a901u, 0x3157bf84u, 0x3095d5b3u, 0x32d36beau, 0x331101ddu, + 0x246be590u, 0x25a98fa7u, 0x27ef31feu, 0x262d5bc9u, 0x23624d4cu, 0x22a0277bu, 0x20e69922u, 0x2124f315u, + 0x2a78b428u, 0x2bbade1fu, 0x29fc6046u, 0x283e0a71u, 0x2d711cf4u, 0x2cb376c3u, 0x2ef5c89au, 0x2f37a2adu, + 0x709a8dc0u, 0x7158e7f7u, 0x731e59aeu, 0x72dc3399u, 0x7793251cu, 0x76514f2bu, 0x7417f172u, 0x75d59b45u, + 0x7e89dc78u, 0x7f4bb64fu, 0x7d0d0816u, 0x7ccf6221u, 0x798074a4u, 0x78421e93u, 0x7a04a0cau, 0x7bc6cafdu, + 0x6cbc2eb0u, 0x6d7e4487u, 0x6f38fadeu, 0x6efa90e9u, 0x6bb5866cu, 0x6a77ec5bu, 0x68315202u, 0x69f33835u, + 0x62af7f08u, 0x636d153fu, 0x612bab66u, 0x60e9c151u, 0x65a6d7d4u, 0x6464bde3u, 0x662203bau, 0x67e0698du, + 0x48d7cb20u, 0x4915a117u, 0x4b531f4eu, 0x4a917579u, 0x4fde63fcu, 0x4e1c09cbu, 0x4c5ab792u, 0x4d98dda5u, + 0x46c49a98u, 0x4706f0afu, 0x45404ef6u, 0x448224c1u, 0x41cd3244u, 0x400f5873u, 0x4249e62au, 0x438b8c1du, + 0x54f16850u, 0x55330267u, 0x5775bc3eu, 0x56b7d609u, 0x53f8c08cu, 0x523aaabbu, 0x507c14e2u, 0x51be7ed5u, + 0x5ae239e8u, 0x5b2053dfu, 0x5966ed86u, 0x58a487b1u, 0x5deb9134u, 0x5c29fb03u, 0x5e6f455au, 0x5fad2f6du, + 0xe1351b80u, 0xe0f771b7u, 0xe2b1cfeeu, 0xe373a5d9u, 0xe63cb35cu, 0xe7fed96bu, 0xe5b86732u, 0xe47a0d05u, + 0xef264a38u, 0xeee4200fu, 0xeca29e56u, 0xed60f461u, 0xe82fe2e4u, 0xe9ed88d3u, 0xebab368au, 0xea695cbdu, + 0xfd13b8f0u, 0xfcd1d2c7u, 0xfe976c9eu, 0xff5506a9u, 0xfa1a102cu, 0xfbd87a1bu, 0xf99ec442u, 0xf85cae75u, + 0xf300e948u, 0xf2c2837fu, 0xf0843d26u, 0xf1465711u, 0xf4094194u, 0xf5cb2ba3u, 0xf78d95fau, 0xf64fffcdu, + 0xd9785d60u, 0xd8ba3757u, 0xdafc890eu, 0xdb3ee339u, 0xde71f5bcu, 0xdfb39f8bu, 0xddf521d2u, 0xdc374be5u, + 0xd76b0cd8u, 0xd6a966efu, 0xd4efd8b6u, 0xd52db281u, 0xd062a404u, 0xd1a0ce33u, 0xd3e6706au, 0xd2241a5du, + 0xc55efe10u, 0xc49c9427u, 0xc6da2a7eu, 0xc7184049u, 0xc25756ccu, 0xc3953cfbu, 0xc1d382a2u, 0xc011e895u, + 0xcb4dafa8u, 0xca8fc59fu, 0xc8c97bc6u, 0xc90b11f1u, 0xcc440774u, 0xcd866d43u, 0xcfc0d31au, 0xce02b92du, + 0x91af9640u, 0x906dfc77u, 0x922b422eu, 0x93e92819u, 0x96a63e9cu, 0x976454abu, 0x9522eaf2u, 0x94e080c5u, + 0x9fbcc7f8u, 0x9e7eadcfu, 0x9c381396u, 0x9dfa79a1u, 0x98b56f24u, 0x99770513u, 0x9b31bb4au, 0x9af3d17du, + 0x8d893530u, 0x8c4b5f07u, 0x8e0de15eu, 0x8fcf8b69u, 0x8a809decu, 0x8b42f7dbu, 0x89044982u, 0x88c623b5u, + 0x839a6488u, 0x82580ebfu, 0x801eb0e6u, 0x81dcdad1u, 0x8493cc54u, 0x8551a663u, 0x8717183au, 0x86d5720du, + 0xa9e2d0a0u, 0xa820ba97u, 0xaa6604ceu, 0xaba46ef9u, 0xaeeb787cu, 0xaf29124bu, 0xad6fac12u, 0xacadc625u, + 0xa7f18118u, 0xa633eb2fu, 0xa4755576u, 0xa5b73f41u, 0xa0f829c4u, 0xa13a43f3u, 0xa37cfdaau, 0xa2be979du, + 0xb5c473d0u, 0xb40619e7u, 0xb640a7beu, 0xb782cd89u, 0xb2cddb0cu, 0xb30fb13bu, 0xb1490f62u, 0xb08b6555u, + 0xbbd72268u, 0xba15485fu, 0xb853f606u, 0xb9919c31u, 0xbcde8ab4u, 0xbd1ce083u, 0xbf5a5edau, 0xbe9834edu +}; + +static const unsigned lodepng_crc32_table3[256] = { + 0x00000000u, 0xb8bc6765u, 0xaa09c88bu, 0x12b5afeeu, 0x8f629757u, 0x37def032u, 0x256b5fdcu, 0x9dd738b9u, + 0xc5b428efu, 0x7d084f8au, 0x6fbde064u, 0xd7018701u, 0x4ad6bfb8u, 0xf26ad8ddu, 0xe0df7733u, 0x58631056u, + 0x5019579fu, 0xe8a530fau, 0xfa109f14u, 0x42acf871u, 0xdf7bc0c8u, 0x67c7a7adu, 0x75720843u, 0xcdce6f26u, + 0x95ad7f70u, 0x2d111815u, 0x3fa4b7fbu, 0x8718d09eu, 0x1acfe827u, 0xa2738f42u, 0xb0c620acu, 0x087a47c9u, + 0xa032af3eu, 0x188ec85bu, 0x0a3b67b5u, 0xb28700d0u, 0x2f503869u, 0x97ec5f0cu, 0x8559f0e2u, 0x3de59787u, + 0x658687d1u, 0xdd3ae0b4u, 0xcf8f4f5au, 0x7733283fu, 0xeae41086u, 0x525877e3u, 0x40edd80du, 0xf851bf68u, + 0xf02bf8a1u, 0x48979fc4u, 0x5a22302au, 0xe29e574fu, 0x7f496ff6u, 0xc7f50893u, 0xd540a77du, 0x6dfcc018u, + 0x359fd04eu, 0x8d23b72bu, 0x9f9618c5u, 0x272a7fa0u, 0xbafd4719u, 0x0241207cu, 0x10f48f92u, 0xa848e8f7u, + 0x9b14583du, 0x23a83f58u, 0x311d90b6u, 0x89a1f7d3u, 0x1476cf6au, 0xaccaa80fu, 0xbe7f07e1u, 0x06c36084u, + 0x5ea070d2u, 0xe61c17b7u, 0xf4a9b859u, 0x4c15df3cu, 0xd1c2e785u, 0x697e80e0u, 0x7bcb2f0eu, 0xc377486bu, + 0xcb0d0fa2u, 0x73b168c7u, 0x6104c729u, 0xd9b8a04cu, 0x446f98f5u, 0xfcd3ff90u, 0xee66507eu, 0x56da371bu, + 0x0eb9274du, 0xb6054028u, 0xa4b0efc6u, 0x1c0c88a3u, 0x81dbb01au, 0x3967d77fu, 0x2bd27891u, 0x936e1ff4u, + 0x3b26f703u, 0x839a9066u, 0x912f3f88u, 0x299358edu, 0xb4446054u, 0x0cf80731u, 0x1e4da8dfu, 0xa6f1cfbau, + 0xfe92dfecu, 0x462eb889u, 0x549b1767u, 0xec277002u, 0x71f048bbu, 0xc94c2fdeu, 0xdbf98030u, 0x6345e755u, + 0x6b3fa09cu, 0xd383c7f9u, 0xc1366817u, 0x798a0f72u, 0xe45d37cbu, 0x5ce150aeu, 0x4e54ff40u, 0xf6e89825u, + 0xae8b8873u, 0x1637ef16u, 0x048240f8u, 0xbc3e279du, 0x21e91f24u, 0x99557841u, 0x8be0d7afu, 0x335cb0cau, + 0xed59b63bu, 0x55e5d15eu, 0x47507eb0u, 0xffec19d5u, 0x623b216cu, 0xda874609u, 0xc832e9e7u, 0x708e8e82u, + 0x28ed9ed4u, 0x9051f9b1u, 0x82e4565fu, 0x3a58313au, 0xa78f0983u, 0x1f336ee6u, 0x0d86c108u, 0xb53aa66du, + 0xbd40e1a4u, 0x05fc86c1u, 0x1749292fu, 0xaff54e4au, 0x322276f3u, 0x8a9e1196u, 0x982bbe78u, 0x2097d91du, + 0x78f4c94bu, 0xc048ae2eu, 0xd2fd01c0u, 0x6a4166a5u, 0xf7965e1cu, 0x4f2a3979u, 0x5d9f9697u, 0xe523f1f2u, + 0x4d6b1905u, 0xf5d77e60u, 0xe762d18eu, 0x5fdeb6ebu, 0xc2098e52u, 0x7ab5e937u, 0x680046d9u, 0xd0bc21bcu, + 0x88df31eau, 0x3063568fu, 0x22d6f961u, 0x9a6a9e04u, 0x07bda6bdu, 0xbf01c1d8u, 0xadb46e36u, 0x15080953u, + 0x1d724e9au, 0xa5ce29ffu, 0xb77b8611u, 0x0fc7e174u, 0x9210d9cdu, 0x2aacbea8u, 0x38191146u, 0x80a57623u, + 0xd8c66675u, 0x607a0110u, 0x72cfaefeu, 0xca73c99bu, 0x57a4f122u, 0xef189647u, 0xfdad39a9u, 0x45115eccu, + 0x764dee06u, 0xcef18963u, 0xdc44268du, 0x64f841e8u, 0xf92f7951u, 0x41931e34u, 0x5326b1dau, 0xeb9ad6bfu, + 0xb3f9c6e9u, 0x0b45a18cu, 0x19f00e62u, 0xa14c6907u, 0x3c9b51beu, 0x842736dbu, 0x96929935u, 0x2e2efe50u, + 0x2654b999u, 0x9ee8defcu, 0x8c5d7112u, 0x34e11677u, 0xa9362eceu, 0x118a49abu, 0x033fe645u, 0xbb838120u, + 0xe3e09176u, 0x5b5cf613u, 0x49e959fdu, 0xf1553e98u, 0x6c820621u, 0xd43e6144u, 0xc68bceaau, 0x7e37a9cfu, + 0xd67f4138u, 0x6ec3265du, 0x7c7689b3u, 0xc4caeed6u, 0x591dd66fu, 0xe1a1b10au, 0xf3141ee4u, 0x4ba87981u, + 0x13cb69d7u, 0xab770eb2u, 0xb9c2a15cu, 0x017ec639u, 0x9ca9fe80u, 0x241599e5u, 0x36a0360bu, 0x8e1c516eu, + 0x866616a7u, 0x3eda71c2u, 0x2c6fde2cu, 0x94d3b949u, 0x090481f0u, 0xb1b8e695u, 0xa30d497bu, 0x1bb12e1eu, + 0x43d23e48u, 0xfb6e592du, 0xe9dbf6c3u, 0x516791a6u, 0xccb0a91fu, 0x740cce7au, 0x66b96194u, 0xde0506f1u +}; + +static const unsigned lodepng_crc32_table4[256] = { + 0x00000000u, 0x3d6029b0u, 0x7ac05360u, 0x47a07ad0u, 0xf580a6c0u, 0xc8e08f70u, 0x8f40f5a0u, 0xb220dc10u, + 0x30704bc1u, 0x0d106271u, 0x4ab018a1u, 0x77d03111u, 0xc5f0ed01u, 0xf890c4b1u, 0xbf30be61u, 0x825097d1u, + 0x60e09782u, 0x5d80be32u, 0x1a20c4e2u, 0x2740ed52u, 0x95603142u, 0xa80018f2u, 0xefa06222u, 0xd2c04b92u, + 0x5090dc43u, 0x6df0f5f3u, 0x2a508f23u, 0x1730a693u, 0xa5107a83u, 0x98705333u, 0xdfd029e3u, 0xe2b00053u, + 0xc1c12f04u, 0xfca106b4u, 0xbb017c64u, 0x866155d4u, 0x344189c4u, 0x0921a074u, 0x4e81daa4u, 0x73e1f314u, + 0xf1b164c5u, 0xccd14d75u, 0x8b7137a5u, 0xb6111e15u, 0x0431c205u, 0x3951ebb5u, 0x7ef19165u, 0x4391b8d5u, + 0xa121b886u, 0x9c419136u, 0xdbe1ebe6u, 0xe681c256u, 0x54a11e46u, 0x69c137f6u, 0x2e614d26u, 0x13016496u, + 0x9151f347u, 0xac31daf7u, 0xeb91a027u, 0xd6f18997u, 0x64d15587u, 0x59b17c37u, 0x1e1106e7u, 0x23712f57u, + 0x58f35849u, 0x659371f9u, 0x22330b29u, 0x1f532299u, 0xad73fe89u, 0x9013d739u, 0xd7b3ade9u, 0xead38459u, + 0x68831388u, 0x55e33a38u, 0x124340e8u, 0x2f236958u, 0x9d03b548u, 0xa0639cf8u, 0xe7c3e628u, 0xdaa3cf98u, + 0x3813cfcbu, 0x0573e67bu, 0x42d39cabu, 0x7fb3b51bu, 0xcd93690bu, 0xf0f340bbu, 0xb7533a6bu, 0x8a3313dbu, + 0x0863840au, 0x3503adbau, 0x72a3d76au, 0x4fc3fedau, 0xfde322cau, 0xc0830b7au, 0x872371aau, 0xba43581au, + 0x9932774du, 0xa4525efdu, 0xe3f2242du, 0xde920d9du, 0x6cb2d18du, 0x51d2f83du, 0x167282edu, 0x2b12ab5du, + 0xa9423c8cu, 0x9422153cu, 0xd3826fecu, 0xeee2465cu, 0x5cc29a4cu, 0x61a2b3fcu, 0x2602c92cu, 0x1b62e09cu, + 0xf9d2e0cfu, 0xc4b2c97fu, 0x8312b3afu, 0xbe729a1fu, 0x0c52460fu, 0x31326fbfu, 0x7692156fu, 0x4bf23cdfu, + 0xc9a2ab0eu, 0xf4c282beu, 0xb362f86eu, 0x8e02d1deu, 0x3c220dceu, 0x0142247eu, 0x46e25eaeu, 0x7b82771eu, + 0xb1e6b092u, 0x8c869922u, 0xcb26e3f2u, 0xf646ca42u, 0x44661652u, 0x79063fe2u, 0x3ea64532u, 0x03c66c82u, + 0x8196fb53u, 0xbcf6d2e3u, 0xfb56a833u, 0xc6368183u, 0x74165d93u, 0x49767423u, 0x0ed60ef3u, 0x33b62743u, + 0xd1062710u, 0xec660ea0u, 0xabc67470u, 0x96a65dc0u, 0x248681d0u, 0x19e6a860u, 0x5e46d2b0u, 0x6326fb00u, + 0xe1766cd1u, 0xdc164561u, 0x9bb63fb1u, 0xa6d61601u, 0x14f6ca11u, 0x2996e3a1u, 0x6e369971u, 0x5356b0c1u, + 0x70279f96u, 0x4d47b626u, 0x0ae7ccf6u, 0x3787e546u, 0x85a73956u, 0xb8c710e6u, 0xff676a36u, 0xc2074386u, + 0x4057d457u, 0x7d37fde7u, 0x3a978737u, 0x07f7ae87u, 0xb5d77297u, 0x88b75b27u, 0xcf1721f7u, 0xf2770847u, + 0x10c70814u, 0x2da721a4u, 0x6a075b74u, 0x576772c4u, 0xe547aed4u, 0xd8278764u, 0x9f87fdb4u, 0xa2e7d404u, + 0x20b743d5u, 0x1dd76a65u, 0x5a7710b5u, 0x67173905u, 0xd537e515u, 0xe857cca5u, 0xaff7b675u, 0x92979fc5u, + 0xe915e8dbu, 0xd475c16bu, 0x93d5bbbbu, 0xaeb5920bu, 0x1c954e1bu, 0x21f567abu, 0x66551d7bu, 0x5b3534cbu, + 0xd965a31au, 0xe4058aaau, 0xa3a5f07au, 0x9ec5d9cau, 0x2ce505dau, 0x11852c6au, 0x562556bau, 0x6b457f0au, + 0x89f57f59u, 0xb49556e9u, 0xf3352c39u, 0xce550589u, 0x7c75d999u, 0x4115f029u, 0x06b58af9u, 0x3bd5a349u, + 0xb9853498u, 0x84e51d28u, 0xc34567f8u, 0xfe254e48u, 0x4c059258u, 0x7165bbe8u, 0x36c5c138u, 0x0ba5e888u, + 0x28d4c7dfu, 0x15b4ee6fu, 0x521494bfu, 0x6f74bd0fu, 0xdd54611fu, 0xe03448afu, 0xa794327fu, 0x9af41bcfu, + 0x18a48c1eu, 0x25c4a5aeu, 0x6264df7eu, 0x5f04f6ceu, 0xed242adeu, 0xd044036eu, 0x97e479beu, 0xaa84500eu, + 0x4834505du, 0x755479edu, 0x32f4033du, 0x0f942a8du, 0xbdb4f69du, 0x80d4df2du, 0xc774a5fdu, 0xfa148c4du, + 0x78441b9cu, 0x4524322cu, 0x028448fcu, 0x3fe4614cu, 0x8dc4bd5cu, 0xb0a494ecu, 0xf704ee3cu, 0xca64c78cu +}; + +static const unsigned lodepng_crc32_table5[256] = { + 0x00000000u, 0xcb5cd3a5u, 0x4dc8a10bu, 0x869472aeu, 0x9b914216u, 0x50cd91b3u, 0xd659e31du, 0x1d0530b8u, + 0xec53826du, 0x270f51c8u, 0xa19b2366u, 0x6ac7f0c3u, 0x77c2c07bu, 0xbc9e13deu, 0x3a0a6170u, 0xf156b2d5u, + 0x03d6029bu, 0xc88ad13eu, 0x4e1ea390u, 0x85427035u, 0x9847408du, 0x531b9328u, 0xd58fe186u, 0x1ed33223u, + 0xef8580f6u, 0x24d95353u, 0xa24d21fdu, 0x6911f258u, 0x7414c2e0u, 0xbf481145u, 0x39dc63ebu, 0xf280b04eu, + 0x07ac0536u, 0xccf0d693u, 0x4a64a43du, 0x81387798u, 0x9c3d4720u, 0x57619485u, 0xd1f5e62bu, 0x1aa9358eu, + 0xebff875bu, 0x20a354feu, 0xa6372650u, 0x6d6bf5f5u, 0x706ec54du, 0xbb3216e8u, 0x3da66446u, 0xf6fab7e3u, + 0x047a07adu, 0xcf26d408u, 0x49b2a6a6u, 0x82ee7503u, 0x9feb45bbu, 0x54b7961eu, 0xd223e4b0u, 0x197f3715u, + 0xe82985c0u, 0x23755665u, 0xa5e124cbu, 0x6ebdf76eu, 0x73b8c7d6u, 0xb8e41473u, 0x3e7066ddu, 0xf52cb578u, + 0x0f580a6cu, 0xc404d9c9u, 0x4290ab67u, 0x89cc78c2u, 0x94c9487au, 0x5f959bdfu, 0xd901e971u, 0x125d3ad4u, + 0xe30b8801u, 0x28575ba4u, 0xaec3290au, 0x659ffaafu, 0x789aca17u, 0xb3c619b2u, 0x35526b1cu, 0xfe0eb8b9u, + 0x0c8e08f7u, 0xc7d2db52u, 0x4146a9fcu, 0x8a1a7a59u, 0x971f4ae1u, 0x5c439944u, 0xdad7ebeau, 0x118b384fu, + 0xe0dd8a9au, 0x2b81593fu, 0xad152b91u, 0x6649f834u, 0x7b4cc88cu, 0xb0101b29u, 0x36846987u, 0xfdd8ba22u, + 0x08f40f5au, 0xc3a8dcffu, 0x453cae51u, 0x8e607df4u, 0x93654d4cu, 0x58399ee9u, 0xdeadec47u, 0x15f13fe2u, + 0xe4a78d37u, 0x2ffb5e92u, 0xa96f2c3cu, 0x6233ff99u, 0x7f36cf21u, 0xb46a1c84u, 0x32fe6e2au, 0xf9a2bd8fu, + 0x0b220dc1u, 0xc07ede64u, 0x46eaaccau, 0x8db67f6fu, 0x90b34fd7u, 0x5bef9c72u, 0xdd7beedcu, 0x16273d79u, + 0xe7718facu, 0x2c2d5c09u, 0xaab92ea7u, 0x61e5fd02u, 0x7ce0cdbau, 0xb7bc1e1fu, 0x31286cb1u, 0xfa74bf14u, + 0x1eb014d8u, 0xd5ecc77du, 0x5378b5d3u, 0x98246676u, 0x852156ceu, 0x4e7d856bu, 0xc8e9f7c5u, 0x03b52460u, + 0xf2e396b5u, 0x39bf4510u, 0xbf2b37beu, 0x7477e41bu, 0x6972d4a3u, 0xa22e0706u, 0x24ba75a8u, 0xefe6a60du, + 0x1d661643u, 0xd63ac5e6u, 0x50aeb748u, 0x9bf264edu, 0x86f75455u, 0x4dab87f0u, 0xcb3ff55eu, 0x006326fbu, + 0xf135942eu, 0x3a69478bu, 0xbcfd3525u, 0x77a1e680u, 0x6aa4d638u, 0xa1f8059du, 0x276c7733u, 0xec30a496u, + 0x191c11eeu, 0xd240c24bu, 0x54d4b0e5u, 0x9f886340u, 0x828d53f8u, 0x49d1805du, 0xcf45f2f3u, 0x04192156u, + 0xf54f9383u, 0x3e134026u, 0xb8873288u, 0x73dbe12du, 0x6eded195u, 0xa5820230u, 0x2316709eu, 0xe84aa33bu, + 0x1aca1375u, 0xd196c0d0u, 0x5702b27eu, 0x9c5e61dbu, 0x815b5163u, 0x4a0782c6u, 0xcc93f068u, 0x07cf23cdu, + 0xf6999118u, 0x3dc542bdu, 0xbb513013u, 0x700de3b6u, 0x6d08d30eu, 0xa65400abu, 0x20c07205u, 0xeb9ca1a0u, + 0x11e81eb4u, 0xdab4cd11u, 0x5c20bfbfu, 0x977c6c1au, 0x8a795ca2u, 0x41258f07u, 0xc7b1fda9u, 0x0ced2e0cu, + 0xfdbb9cd9u, 0x36e74f7cu, 0xb0733dd2u, 0x7b2fee77u, 0x662adecfu, 0xad760d6au, 0x2be27fc4u, 0xe0beac61u, + 0x123e1c2fu, 0xd962cf8au, 0x5ff6bd24u, 0x94aa6e81u, 0x89af5e39u, 0x42f38d9cu, 0xc467ff32u, 0x0f3b2c97u, + 0xfe6d9e42u, 0x35314de7u, 0xb3a53f49u, 0x78f9ececu, 0x65fcdc54u, 0xaea00ff1u, 0x28347d5fu, 0xe368aefau, + 0x16441b82u, 0xdd18c827u, 0x5b8cba89u, 0x90d0692cu, 0x8dd55994u, 0x46898a31u, 0xc01df89fu, 0x0b412b3au, + 0xfa1799efu, 0x314b4a4au, 0xb7df38e4u, 0x7c83eb41u, 0x6186dbf9u, 0xaada085cu, 0x2c4e7af2u, 0xe712a957u, + 0x15921919u, 0xdececabcu, 0x585ab812u, 0x93066bb7u, 0x8e035b0fu, 0x455f88aau, 0xc3cbfa04u, 0x089729a1u, + 0xf9c19b74u, 0x329d48d1u, 0xb4093a7fu, 0x7f55e9dau, 0x6250d962u, 0xa90c0ac7u, 0x2f987869u, 0xe4c4abccu +}; + +static const unsigned lodepng_crc32_table6[256] = { + 0x00000000u, 0xa6770bb4u, 0x979f1129u, 0x31e81a9du, 0xf44f2413u, 0x52382fa7u, 0x63d0353au, 0xc5a73e8eu, + 0x33ef4e67u, 0x959845d3u, 0xa4705f4eu, 0x020754fau, 0xc7a06a74u, 0x61d761c0u, 0x503f7b5du, 0xf64870e9u, + 0x67de9cceu, 0xc1a9977au, 0xf0418de7u, 0x56368653u, 0x9391b8ddu, 0x35e6b369u, 0x040ea9f4u, 0xa279a240u, + 0x5431d2a9u, 0xf246d91du, 0xc3aec380u, 0x65d9c834u, 0xa07ef6bau, 0x0609fd0eu, 0x37e1e793u, 0x9196ec27u, + 0xcfbd399cu, 0x69ca3228u, 0x582228b5u, 0xfe552301u, 0x3bf21d8fu, 0x9d85163bu, 0xac6d0ca6u, 0x0a1a0712u, + 0xfc5277fbu, 0x5a257c4fu, 0x6bcd66d2u, 0xcdba6d66u, 0x081d53e8u, 0xae6a585cu, 0x9f8242c1u, 0x39f54975u, + 0xa863a552u, 0x0e14aee6u, 0x3ffcb47bu, 0x998bbfcfu, 0x5c2c8141u, 0xfa5b8af5u, 0xcbb39068u, 0x6dc49bdcu, + 0x9b8ceb35u, 0x3dfbe081u, 0x0c13fa1cu, 0xaa64f1a8u, 0x6fc3cf26u, 0xc9b4c492u, 0xf85cde0fu, 0x5e2bd5bbu, + 0x440b7579u, 0xe27c7ecdu, 0xd3946450u, 0x75e36fe4u, 0xb044516au, 0x16335adeu, 0x27db4043u, 0x81ac4bf7u, + 0x77e43b1eu, 0xd19330aau, 0xe07b2a37u, 0x460c2183u, 0x83ab1f0du, 0x25dc14b9u, 0x14340e24u, 0xb2430590u, + 0x23d5e9b7u, 0x85a2e203u, 0xb44af89eu, 0x123df32au, 0xd79acda4u, 0x71edc610u, 0x4005dc8du, 0xe672d739u, + 0x103aa7d0u, 0xb64dac64u, 0x87a5b6f9u, 0x21d2bd4du, 0xe47583c3u, 0x42028877u, 0x73ea92eau, 0xd59d995eu, + 0x8bb64ce5u, 0x2dc14751u, 0x1c295dccu, 0xba5e5678u, 0x7ff968f6u, 0xd98e6342u, 0xe86679dfu, 0x4e11726bu, + 0xb8590282u, 0x1e2e0936u, 0x2fc613abu, 0x89b1181fu, 0x4c162691u, 0xea612d25u, 0xdb8937b8u, 0x7dfe3c0cu, + 0xec68d02bu, 0x4a1fdb9fu, 0x7bf7c102u, 0xdd80cab6u, 0x1827f438u, 0xbe50ff8cu, 0x8fb8e511u, 0x29cfeea5u, + 0xdf879e4cu, 0x79f095f8u, 0x48188f65u, 0xee6f84d1u, 0x2bc8ba5fu, 0x8dbfb1ebu, 0xbc57ab76u, 0x1a20a0c2u, + 0x8816eaf2u, 0x2e61e146u, 0x1f89fbdbu, 0xb9fef06fu, 0x7c59cee1u, 0xda2ec555u, 0xebc6dfc8u, 0x4db1d47cu, + 0xbbf9a495u, 0x1d8eaf21u, 0x2c66b5bcu, 0x8a11be08u, 0x4fb68086u, 0xe9c18b32u, 0xd82991afu, 0x7e5e9a1bu, + 0xefc8763cu, 0x49bf7d88u, 0x78576715u, 0xde206ca1u, 0x1b87522fu, 0xbdf0599bu, 0x8c184306u, 0x2a6f48b2u, + 0xdc27385bu, 0x7a5033efu, 0x4bb82972u, 0xedcf22c6u, 0x28681c48u, 0x8e1f17fcu, 0xbff70d61u, 0x198006d5u, + 0x47abd36eu, 0xe1dcd8dau, 0xd034c247u, 0x7643c9f3u, 0xb3e4f77du, 0x1593fcc9u, 0x247be654u, 0x820cede0u, + 0x74449d09u, 0xd23396bdu, 0xe3db8c20u, 0x45ac8794u, 0x800bb91au, 0x267cb2aeu, 0x1794a833u, 0xb1e3a387u, + 0x20754fa0u, 0x86024414u, 0xb7ea5e89u, 0x119d553du, 0xd43a6bb3u, 0x724d6007u, 0x43a57a9au, 0xe5d2712eu, + 0x139a01c7u, 0xb5ed0a73u, 0x840510eeu, 0x22721b5au, 0xe7d525d4u, 0x41a22e60u, 0x704a34fdu, 0xd63d3f49u, + 0xcc1d9f8bu, 0x6a6a943fu, 0x5b828ea2u, 0xfdf58516u, 0x3852bb98u, 0x9e25b02cu, 0xafcdaab1u, 0x09baa105u, + 0xfff2d1ecu, 0x5985da58u, 0x686dc0c5u, 0xce1acb71u, 0x0bbdf5ffu, 0xadcafe4bu, 0x9c22e4d6u, 0x3a55ef62u, + 0xabc30345u, 0x0db408f1u, 0x3c5c126cu, 0x9a2b19d8u, 0x5f8c2756u, 0xf9fb2ce2u, 0xc813367fu, 0x6e643dcbu, + 0x982c4d22u, 0x3e5b4696u, 0x0fb35c0bu, 0xa9c457bfu, 0x6c636931u, 0xca146285u, 0xfbfc7818u, 0x5d8b73acu, + 0x03a0a617u, 0xa5d7ada3u, 0x943fb73eu, 0x3248bc8au, 0xf7ef8204u, 0x519889b0u, 0x6070932du, 0xc6079899u, + 0x304fe870u, 0x9638e3c4u, 0xa7d0f959u, 0x01a7f2edu, 0xc400cc63u, 0x6277c7d7u, 0x539fdd4au, 0xf5e8d6feu, + 0x647e3ad9u, 0xc209316du, 0xf3e12bf0u, 0x55962044u, 0x90311ecau, 0x3646157eu, 0x07ae0fe3u, 0xa1d90457u, + 0x579174beu, 0xf1e67f0au, 0xc00e6597u, 0x66796e23u, 0xa3de50adu, 0x05a95b19u, 0x34414184u, 0x92364a30u +}; + +static const unsigned lodepng_crc32_table7[256] = { + 0x00000000u, 0xccaa009eu, 0x4225077du, 0x8e8f07e3u, 0x844a0efau, 0x48e00e64u, 0xc66f0987u, 0x0ac50919u, + 0xd3e51bb5u, 0x1f4f1b2bu, 0x91c01cc8u, 0x5d6a1c56u, 0x57af154fu, 0x9b0515d1u, 0x158a1232u, 0xd92012acu, + 0x7cbb312bu, 0xb01131b5u, 0x3e9e3656u, 0xf23436c8u, 0xf8f13fd1u, 0x345b3f4fu, 0xbad438acu, 0x767e3832u, + 0xaf5e2a9eu, 0x63f42a00u, 0xed7b2de3u, 0x21d12d7du, 0x2b142464u, 0xe7be24fau, 0x69312319u, 0xa59b2387u, + 0xf9766256u, 0x35dc62c8u, 0xbb53652bu, 0x77f965b5u, 0x7d3c6cacu, 0xb1966c32u, 0x3f196bd1u, 0xf3b36b4fu, + 0x2a9379e3u, 0xe639797du, 0x68b67e9eu, 0xa41c7e00u, 0xaed97719u, 0x62737787u, 0xecfc7064u, 0x205670fau, + 0x85cd537du, 0x496753e3u, 0xc7e85400u, 0x0b42549eu, 0x01875d87u, 0xcd2d5d19u, 0x43a25afau, 0x8f085a64u, + 0x562848c8u, 0x9a824856u, 0x140d4fb5u, 0xd8a74f2bu, 0xd2624632u, 0x1ec846acu, 0x9047414fu, 0x5ced41d1u, + 0x299dc2edu, 0xe537c273u, 0x6bb8c590u, 0xa712c50eu, 0xadd7cc17u, 0x617dcc89u, 0xeff2cb6au, 0x2358cbf4u, + 0xfa78d958u, 0x36d2d9c6u, 0xb85dde25u, 0x74f7debbu, 0x7e32d7a2u, 0xb298d73cu, 0x3c17d0dfu, 0xf0bdd041u, + 0x5526f3c6u, 0x998cf358u, 0x1703f4bbu, 0xdba9f425u, 0xd16cfd3cu, 0x1dc6fda2u, 0x9349fa41u, 0x5fe3fadfu, + 0x86c3e873u, 0x4a69e8edu, 0xc4e6ef0eu, 0x084cef90u, 0x0289e689u, 0xce23e617u, 0x40ace1f4u, 0x8c06e16au, + 0xd0eba0bbu, 0x1c41a025u, 0x92cea7c6u, 0x5e64a758u, 0x54a1ae41u, 0x980baedfu, 0x1684a93cu, 0xda2ea9a2u, + 0x030ebb0eu, 0xcfa4bb90u, 0x412bbc73u, 0x8d81bcedu, 0x8744b5f4u, 0x4beeb56au, 0xc561b289u, 0x09cbb217u, + 0xac509190u, 0x60fa910eu, 0xee7596edu, 0x22df9673u, 0x281a9f6au, 0xe4b09ff4u, 0x6a3f9817u, 0xa6959889u, + 0x7fb58a25u, 0xb31f8abbu, 0x3d908d58u, 0xf13a8dc6u, 0xfbff84dfu, 0x37558441u, 0xb9da83a2u, 0x7570833cu, + 0x533b85dau, 0x9f918544u, 0x111e82a7u, 0xddb48239u, 0xd7718b20u, 0x1bdb8bbeu, 0x95548c5du, 0x59fe8cc3u, + 0x80de9e6fu, 0x4c749ef1u, 0xc2fb9912u, 0x0e51998cu, 0x04949095u, 0xc83e900bu, 0x46b197e8u, 0x8a1b9776u, + 0x2f80b4f1u, 0xe32ab46fu, 0x6da5b38cu, 0xa10fb312u, 0xabcaba0bu, 0x6760ba95u, 0xe9efbd76u, 0x2545bde8u, + 0xfc65af44u, 0x30cfafdau, 0xbe40a839u, 0x72eaa8a7u, 0x782fa1beu, 0xb485a120u, 0x3a0aa6c3u, 0xf6a0a65du, + 0xaa4de78cu, 0x66e7e712u, 0xe868e0f1u, 0x24c2e06fu, 0x2e07e976u, 0xe2ade9e8u, 0x6c22ee0bu, 0xa088ee95u, + 0x79a8fc39u, 0xb502fca7u, 0x3b8dfb44u, 0xf727fbdau, 0xfde2f2c3u, 0x3148f25du, 0xbfc7f5beu, 0x736df520u, + 0xd6f6d6a7u, 0x1a5cd639u, 0x94d3d1dau, 0x5879d144u, 0x52bcd85du, 0x9e16d8c3u, 0x1099df20u, 0xdc33dfbeu, + 0x0513cd12u, 0xc9b9cd8cu, 0x4736ca6fu, 0x8b9ccaf1u, 0x8159c3e8u, 0x4df3c376u, 0xc37cc495u, 0x0fd6c40bu, + 0x7aa64737u, 0xb60c47a9u, 0x3883404au, 0xf42940d4u, 0xfeec49cdu, 0x32464953u, 0xbcc94eb0u, 0x70634e2eu, + 0xa9435c82u, 0x65e95c1cu, 0xeb665bffu, 0x27cc5b61u, 0x2d095278u, 0xe1a352e6u, 0x6f2c5505u, 0xa386559bu, + 0x061d761cu, 0xcab77682u, 0x44387161u, 0x889271ffu, 0x825778e6u, 0x4efd7878u, 0xc0727f9bu, 0x0cd87f05u, + 0xd5f86da9u, 0x19526d37u, 0x97dd6ad4u, 0x5b776a4au, 0x51b26353u, 0x9d1863cdu, 0x1397642eu, 0xdf3d64b0u, + 0x83d02561u, 0x4f7a25ffu, 0xc1f5221cu, 0x0d5f2282u, 0x079a2b9bu, 0xcb302b05u, 0x45bf2ce6u, 0x89152c78u, + 0x50353ed4u, 0x9c9f3e4au, 0x121039a9u, 0xdeba3937u, 0xd47f302eu, 0x18d530b0u, 0x965a3753u, 0x5af037cdu, + 0xff6b144au, 0x33c114d4u, 0xbd4e1337u, 0x71e413a9u, 0x7b211ab0u, 0xb78b1a2eu, 0x39041dcdu, 0xf5ae1d53u, + 0x2c8e0fffu, 0xe0240f61u, 0x6eab0882u, 0xa201081cu, 0xa8c40105u, 0x646e019bu, 0xeae10678u, 0x264b06e6u +}; + +/* Computes the cyclic redundancy check as used by PNG chunks*/ +unsigned lodepng_crc32(const unsigned char* data, size_t length) { + /*Using the Slicing by Eight algorithm*/ + unsigned r = 0xffffffffu; + while(length >= 8) { + r = lodepng_crc32_table7[(data[0] ^ (r & 0xffu))] ^ + lodepng_crc32_table6[(data[1] ^ ((r >> 8) & 0xffu))] ^ + lodepng_crc32_table5[(data[2] ^ ((r >> 16) & 0xffu))] ^ + lodepng_crc32_table4[(data[3] ^ ((r >> 24) & 0xffu))] ^ + lodepng_crc32_table3[data[4]] ^ + lodepng_crc32_table2[data[5]] ^ + lodepng_crc32_table1[data[6]] ^ + lodepng_crc32_table0[data[7]]; + data += 8; + length -= 8; + } + while(length--) { + r = lodepng_crc32_table0[(r ^ *data++) & 0xffu] ^ (r >> 8); + } + return r ^ 0xffffffffu; +} +#else /* LODEPNG_COMPILE_CRC */ +/*in this case, the function is only declared here, and must be defined externally +so that it will be linked in. + +Example implementation that uses a much smaller lookup table for memory constrained cases: + +unsigned lodepng_crc32(const unsigned char* data, size_t length) { + unsigned r = 0xffffffffu; + static const unsigned table[16] = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c + }; + while(length--) { + r = table[(r ^ *data) & 0xf] ^ (r >> 4); + r = table[(r ^ (*data >> 4)) & 0xf] ^ (r >> 4); + data++; + } + return r ^ 0xffffffffu; +} +*/ +unsigned lodepng_crc32(const unsigned char* data, size_t length); +#endif /* LODEPNG_COMPILE_CRC */ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Reading and writing PNG color channel bits / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/* The color channel bits of less-than-8-bit pixels are read with the MSB of bytes first, +so LodePNGBitWriter and LodePNGBitReader can't be used for those. */ + +static unsigned char readBitFromReversedStream(size_t* bitpointer, const unsigned char* bitstream) { + unsigned char result = (unsigned char)((bitstream[(*bitpointer) >> 3] >> (7 - ((*bitpointer) & 0x7))) & 1); + ++(*bitpointer); + return result; +} + +/* TODO: make this faster */ +static unsigned readBitsFromReversedStream(size_t* bitpointer, const unsigned char* bitstream, size_t nbits) { + unsigned result = 0; + size_t i; + for(i = 0 ; i < nbits; ++i) { + result <<= 1u; + result |= (unsigned)readBitFromReversedStream(bitpointer, bitstream); + } + return result; +} + +static void setBitOfReversedStream(size_t* bitpointer, unsigned char* bitstream, unsigned char bit) { + /*the current bit in bitstream may be 0 or 1 for this to work*/ + if(bit == 0) bitstream[(*bitpointer) >> 3u] &= (unsigned char)(~(1u << (7u - ((*bitpointer) & 7u)))); + else bitstream[(*bitpointer) >> 3u] |= (1u << (7u - ((*bitpointer) & 7u))); + ++(*bitpointer); +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / PNG chunks / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +unsigned lodepng_chunk_length(const unsigned char* chunk) { + return lodepng_read32bitInt(chunk); +} + +void lodepng_chunk_type(char type[5], const unsigned char* chunk) { + unsigned i; + for(i = 0; i != 4; ++i) type[i] = (char)chunk[4 + i]; + type[4] = 0; /*null termination char*/ +} + +unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, const char* type) { + if(lodepng_strlen(type) != 4) return 0; + return (chunk[4] == type[0] && chunk[5] == type[1] && chunk[6] == type[2] && chunk[7] == type[3]); +} + +/* chunk type name must exist only out of alphabetic characters a-z or A-Z */ +static unsigned char lodepng_chunk_type_name_valid(const unsigned char* chunk) { + unsigned i; + for(i = 0; i != 4; ++i) { + char c = (char)chunk[4 + i]; + if(!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) { + return 0; /* not valid */ + } + } + return 1; /* valid */ +} + +unsigned char lodepng_chunk_ancillary(const unsigned char* chunk) { + return((chunk[4] & 32) != 0); +} + +unsigned char lodepng_chunk_private(const unsigned char* chunk) { + return((chunk[5] & 32) != 0); +} + +/* this is an error if it is reserved: the third character must be uppercase in the PNG standard, +lowercasing this character is reserved for possible future extension by the spec*/ +static unsigned char lodepng_chunk_reserved(const unsigned char* chunk) { + return((chunk[6] & 32) != 0); +} + +unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk) { + return((chunk[7] & 32) != 0); +} + +unsigned char* lodepng_chunk_data(unsigned char* chunk) { + return &chunk[8]; +} + +const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk) { + return &chunk[8]; +} + +unsigned lodepng_chunk_check_crc(const unsigned char* chunk) { + unsigned length = lodepng_chunk_length(chunk); + unsigned crc = lodepng_read32bitInt(&chunk[length + 8]); + /*the CRC is taken of the data and the 4 chunk type letters, not the length*/ + unsigned checksum = lodepng_crc32(&chunk[4], length + 4); + if(crc != checksum) return 1; + else return 0; +} + +void lodepng_chunk_generate_crc(unsigned char* chunk) { + unsigned length = lodepng_chunk_length(chunk); + unsigned crc = lodepng_crc32(&chunk[4], length + 4); + lodepng_set32bitInt(chunk + 8 + length, crc); +} + +unsigned char* lodepng_chunk_next(unsigned char* chunk, unsigned char* end) { + size_t available_size = (size_t)(end - chunk); + if(chunk >= end || available_size < 12) return end; /*too small to contain a chunk*/ + if(chunk[0] == 0x89 && chunk[1] == 0x50 && chunk[2] == 0x4e && chunk[3] == 0x47 + && chunk[4] == 0x0d && chunk[5] == 0x0a && chunk[6] == 0x1a && chunk[7] == 0x0a) { + /* Is PNG magic header at start of PNG file. Jump to first actual chunk. */ + return chunk + 8; + } else { + size_t total_chunk_length; + if(lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) return end; + if(total_chunk_length > available_size) return end; /*outside of range*/ + return chunk + total_chunk_length; + } +} + +const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk, const unsigned char* end) { + size_t available_size = (size_t)(end - chunk); + if(chunk >= end || available_size < 12) return end; /*too small to contain a chunk*/ + if(chunk[0] == 0x89 && chunk[1] == 0x50 && chunk[2] == 0x4e && chunk[3] == 0x47 + && chunk[4] == 0x0d && chunk[5] == 0x0a && chunk[6] == 0x1a && chunk[7] == 0x0a) { + /* Is PNG magic header at start of PNG file. Jump to first actual chunk. */ + return chunk + 8; + } else { + size_t total_chunk_length; + if(lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) return end; + if(total_chunk_length > available_size) return end; /*outside of range*/ + return chunk + total_chunk_length; + } +} + +unsigned char* lodepng_chunk_find(unsigned char* chunk, unsigned char* end, const char type[5]) { + for(;;) { + if(chunk >= end || end - chunk < 12) return 0; /* past file end: chunk + 12 > end */ + if(lodepng_chunk_type_equals(chunk, type)) return chunk; + chunk = lodepng_chunk_next(chunk, end); + } +} + +const unsigned char* lodepng_chunk_find_const(const unsigned char* chunk, const unsigned char* end, const char type[5]) { + for(;;) { + if(chunk >= end || end - chunk < 12) return 0; /* past file end: chunk + 12 > end */ + if(lodepng_chunk_type_equals(chunk, type)) return chunk; + chunk = lodepng_chunk_next_const(chunk, end); + } +} + +unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsigned char* chunk) { + unsigned i; + size_t total_chunk_length, new_length; + unsigned char *chunk_start, *new_buffer; + + if(!lodepng_chunk_type_name_valid(chunk)) { + return 121; /* invalid chunk type name */ + } + if(lodepng_chunk_reserved(chunk)) { + return 122; /* invalid third lowercase character */ + } + + if(lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) return 77; + if(lodepng_addofl(*outsize, total_chunk_length, &new_length)) return 77; + + new_buffer = (unsigned char*)lodepng_realloc(*out, new_length); + if(!new_buffer) return 83; /*alloc fail*/ + (*out) = new_buffer; + (*outsize) = new_length; + chunk_start = &(*out)[new_length - total_chunk_length]; + + for(i = 0; i != total_chunk_length; ++i) chunk_start[i] = chunk[i]; + + return 0; +} + +/*Sets length and name and allocates the space for data and crc but does not +set data or crc yet. Returns the start of the chunk in chunk. The start of +the data is at chunk + 8. To finalize chunk, add the data, then use +lodepng_chunk_generate_crc */ +static unsigned lodepng_chunk_init(unsigned char** chunk, + ucvector* out, + size_t length, const char* type) { + size_t new_length = out->size; + if(lodepng_addofl(new_length, length, &new_length)) return 77; + if(lodepng_addofl(new_length, 12, &new_length)) return 77; + if(!ucvector_resize(out, new_length)) return 83; /*alloc fail*/ + *chunk = out->data + new_length - length - 12u; + + /*1: length*/ + lodepng_set32bitInt(*chunk, (unsigned)length); + + /*2: chunk name (4 letters)*/ + lodepng_memcpy(*chunk + 4, type, 4); + + return 0; +} + +/* like lodepng_chunk_create but with custom allocsize */ +static unsigned lodepng_chunk_createv(ucvector* out, + size_t length, const char* type, const unsigned char* data) { + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, length, type)); + + /*3: the data*/ + lodepng_memcpy(chunk + 8, data, length); + + /*4: CRC (of the chunkname characters and the data)*/ + lodepng_chunk_generate_crc(chunk); + + return 0; +} + +unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, + size_t length, const char* type, const unsigned char* data) { + ucvector v = ucvector_init(*out, *outsize); + unsigned error = lodepng_chunk_createv(&v, length, type, data); + *out = v.data; + *outsize = v.size; + return error; +} + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / Color types, channels, bits / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*checks if the colortype is valid and the bitdepth bd is allowed for this colortype. +Return value is a LodePNG error code.*/ +static unsigned checkColorValidity(LodePNGColorType colortype, unsigned bd) { + switch(colortype) { + case LCT_GREY: if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 || bd == 16)) return 37; break; + case LCT_RGB: if(!( bd == 8 || bd == 16)) return 37; break; + case LCT_PALETTE: if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 )) return 37; break; + case LCT_GREY_ALPHA: if(!( bd == 8 || bd == 16)) return 37; break; + case LCT_RGBA: if(!( bd == 8 || bd == 16)) return 37; break; + case LCT_MAX_OCTET_VALUE: return 31; /* invalid color type */ + default: return 31; /* invalid color type */ + } + return 0; /*allowed color type / bits combination*/ +} + +static unsigned getNumColorChannels(LodePNGColorType colortype) { + switch(colortype) { + case LCT_GREY: return 1; + case LCT_RGB: return 3; + case LCT_PALETTE: return 1; + case LCT_GREY_ALPHA: return 2; + case LCT_RGBA: return 4; + case LCT_MAX_OCTET_VALUE: return 0; /* invalid color type */ + default: return 0; /*invalid color type*/ + } +} + +static unsigned lodepng_get_bpp_lct(LodePNGColorType colortype, unsigned bitdepth) { + /*bits per pixel is amount of channels * bits per channel*/ + return getNumColorChannels(colortype) * bitdepth; +} + +/* ////////////////////////////////////////////////////////////////////////// */ + +void lodepng_color_mode_init(LodePNGColorMode* info) { + info->key_defined = 0; + info->key_r = info->key_g = info->key_b = 0; + info->colortype = LCT_RGBA; + info->bitdepth = 8; + info->palette = 0; + info->palettesize = 0; +} + +/*allocates palette memory if needed, and initializes all colors to black*/ +static void lodepng_color_mode_alloc_palette(LodePNGColorMode* info) { + size_t i; + /*if the palette is already allocated, it will have size 1024 so no reallocation needed in that case*/ + /*the palette must have room for up to 256 colors with 4 bytes each.*/ + if(!info->palette) info->palette = (unsigned char*)lodepng_malloc(1024); + if(!info->palette) return; /*alloc fail*/ + for(i = 0; i != 256; ++i) { + /*Initialize all unused colors with black, the value used for invalid palette indices. + This is an error according to the PNG spec, but common PNG decoders make it black instead. + That makes color conversion slightly faster due to no error handling needed.*/ + info->palette[i * 4 + 0] = 0; + info->palette[i * 4 + 1] = 0; + info->palette[i * 4 + 2] = 0; + info->palette[i * 4 + 3] = 255; + } +} + +void lodepng_color_mode_cleanup(LodePNGColorMode* info) { + lodepng_palette_clear(info); +} + +unsigned lodepng_color_mode_copy(LodePNGColorMode* dest, const LodePNGColorMode* source) { + lodepng_color_mode_cleanup(dest); + lodepng_memcpy(dest, source, sizeof(LodePNGColorMode)); + if(source->palette) { + dest->palette = (unsigned char*)lodepng_malloc(1024); + if(!dest->palette && source->palettesize) return 83; /*alloc fail*/ + lodepng_memcpy(dest->palette, source->palette, source->palettesize * 4); + } + return 0; +} + +LodePNGColorMode lodepng_color_mode_make(LodePNGColorType colortype, unsigned bitdepth) { + LodePNGColorMode result; + lodepng_color_mode_init(&result); + result.colortype = colortype; + result.bitdepth = bitdepth; + return result; +} + +static int lodepng_color_mode_equal(const LodePNGColorMode* a, const LodePNGColorMode* b) { + size_t i; + if(a->colortype != b->colortype) return 0; + if(a->bitdepth != b->bitdepth) return 0; + if(a->key_defined != b->key_defined) return 0; + if(a->key_defined) { + if(a->key_r != b->key_r) return 0; + if(a->key_g != b->key_g) return 0; + if(a->key_b != b->key_b) return 0; + } + if(a->palettesize != b->palettesize) return 0; + for(i = 0; i != a->palettesize * 4; ++i) { + if(a->palette[i] != b->palette[i]) return 0; + } + return 1; +} + +void lodepng_palette_clear(LodePNGColorMode* info) { + if(info->palette) lodepng_free(info->palette); + info->palette = 0; + info->palettesize = 0; +} + +unsigned lodepng_palette_add(LodePNGColorMode* info, + unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + if(!info->palette) /*allocate palette if empty*/ { + lodepng_color_mode_alloc_palette(info); + if(!info->palette) return 83; /*alloc fail*/ + } + if(info->palettesize >= 256) { + return 108; /*too many palette values*/ + } + info->palette[4 * info->palettesize + 0] = r; + info->palette[4 * info->palettesize + 1] = g; + info->palette[4 * info->palettesize + 2] = b; + info->palette[4 * info->palettesize + 3] = a; + ++info->palettesize; + return 0; +} + +/*calculate bits per pixel out of colortype and bitdepth*/ +unsigned lodepng_get_bpp(const LodePNGColorMode* info) { + return lodepng_get_bpp_lct(info->colortype, info->bitdepth); +} + +unsigned lodepng_get_channels(const LodePNGColorMode* info) { + return getNumColorChannels(info->colortype); +} + +unsigned lodepng_is_greyscale_type(const LodePNGColorMode* info) { + return info->colortype == LCT_GREY || info->colortype == LCT_GREY_ALPHA; +} + +unsigned lodepng_is_alpha_type(const LodePNGColorMode* info) { + return (info->colortype & 4) != 0; /*4 or 6*/ +} + +unsigned lodepng_is_palette_type(const LodePNGColorMode* info) { + return info->colortype == LCT_PALETTE; +} + +unsigned lodepng_has_palette_alpha(const LodePNGColorMode* info) { + size_t i; + for(i = 0; i != info->palettesize; ++i) { + if(info->palette[i * 4 + 3] < 255) return 1; + } + return 0; +} + +unsigned lodepng_can_have_alpha(const LodePNGColorMode* info) { + return info->key_defined + || lodepng_is_alpha_type(info) + || lodepng_has_palette_alpha(info); +} + +static size_t lodepng_get_raw_size_lct(unsigned w, unsigned h, LodePNGColorType colortype, unsigned bitdepth) { + size_t bpp = lodepng_get_bpp_lct(colortype, bitdepth); + size_t n = (size_t)w * (size_t)h; + return ((n / 8u) * bpp) + ((n & 7u) * bpp + 7u) / 8u; +} + +size_t lodepng_get_raw_size(unsigned w, unsigned h, const LodePNGColorMode* color) { + return lodepng_get_raw_size_lct(w, h, color->colortype, color->bitdepth); +} + + +#ifdef LODEPNG_COMPILE_PNG + +/*in an idat chunk, each scanline is a multiple of 8 bits, unlike the lodepng output buffer, +and in addition has one extra byte per line: the filter byte. So this gives a larger +result than lodepng_get_raw_size. Set h to 1 to get the size of 1 row including filter byte. */ +static size_t lodepng_get_raw_size_idat(unsigned w, unsigned h, unsigned bpp) { + /* + 1 for the filter byte, and possibly plus padding bits per line. */ + /* Ignoring casts, the expression is equal to (w * bpp + 7) / 8 + 1, but avoids overflow of w * bpp */ + size_t line = ((size_t)(w / 8u) * bpp) + 1u + ((w & 7u) * bpp + 7u) / 8u; + return (size_t)h * line; +} + +#ifdef LODEPNG_COMPILE_DECODER +/*Safely checks whether size_t overflow can be caused due to amount of pixels. +This check is overcautious rather than precise. If this check indicates no overflow, +you can safely compute in a size_t (but not an unsigned): +-(size_t)w * (size_t)h * 8 +-amount of bytes in IDAT (including filter, padding and Adam7 bytes) +-amount of bytes in raw color model +Returns 1 if overflow possible, 0 if not. +*/ +static int lodepng_pixel_overflow(unsigned w, unsigned h, + const LodePNGColorMode* pngcolor, const LodePNGColorMode* rawcolor) { + size_t bpp = LODEPNG_MAX(lodepng_get_bpp(pngcolor), lodepng_get_bpp(rawcolor)); + size_t numpixels, total; + size_t line; /* bytes per line in worst case */ + + if(lodepng_mulofl((size_t)w, (size_t)h, &numpixels)) return 1; + if(lodepng_mulofl(numpixels, 8, &total)) return 1; /* bit pointer with 8-bit color, or 8 bytes per channel color */ + + /* Bytes per scanline with the expression "(w / 8u) * bpp) + ((w & 7u) * bpp + 7u) / 8u" */ + if(lodepng_mulofl((size_t)(w / 8u), bpp, &line)) return 1; + if(lodepng_addofl(line, ((w & 7u) * bpp + 7u) / 8u, &line)) return 1; + + if(lodepng_addofl(line, 5, &line)) return 1; /* 5 bytes overhead per line: 1 filterbyte, 4 for Adam7 worst case */ + if(lodepng_mulofl(line, h, &total)) return 1; /* Total bytes in worst case */ + + return 0; /* no overflow */ +} +#endif /*LODEPNG_COMPILE_DECODER*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + +static void LodePNGUnknownChunks_init(LodePNGInfo* info) { + unsigned i; + for(i = 0; i != 3; ++i) info->unknown_chunks_data[i] = 0; + for(i = 0; i != 3; ++i) info->unknown_chunks_size[i] = 0; +} + +static void LodePNGUnknownChunks_cleanup(LodePNGInfo* info) { + unsigned i; + for(i = 0; i != 3; ++i) lodepng_free(info->unknown_chunks_data[i]); +} + +static unsigned LodePNGUnknownChunks_copy(LodePNGInfo* dest, const LodePNGInfo* src) { + unsigned i; + + LodePNGUnknownChunks_cleanup(dest); + + for(i = 0; i != 3; ++i) { + size_t j; + dest->unknown_chunks_size[i] = src->unknown_chunks_size[i]; + dest->unknown_chunks_data[i] = (unsigned char*)lodepng_malloc(src->unknown_chunks_size[i]); + if(!dest->unknown_chunks_data[i] && dest->unknown_chunks_size[i]) return 83; /*alloc fail*/ + for(j = 0; j < src->unknown_chunks_size[i]; ++j) { + dest->unknown_chunks_data[i][j] = src->unknown_chunks_data[i][j]; + } + } + + return 0; +} + +/******************************************************************************/ + +static void LodePNGText_init(LodePNGInfo* info) { + info->text_num = 0; + info->text_keys = NULL; + info->text_strings = NULL; +} + +static void LodePNGText_cleanup(LodePNGInfo* info) { + size_t i; + for(i = 0; i != info->text_num; ++i) { + lodepng_free(info->text_keys[i]); + lodepng_free(info->text_strings[i]); + } + lodepng_free(info->text_keys); + lodepng_free(info->text_strings); +} + +static unsigned LodePNGText_copy(LodePNGInfo* dest, const LodePNGInfo* source) { + size_t i = 0; + dest->text_keys = NULL; + dest->text_strings = NULL; + dest->text_num = 0; + for(i = 0; i != source->text_num; ++i) { + CERROR_TRY_RETURN(lodepng_add_text(dest, source->text_keys[i], source->text_strings[i])); + } + return 0; +} + +static unsigned lodepng_add_text_sized(LodePNGInfo* info, const char* key, const char* str, size_t size) { + char** new_keys = (char**)(lodepng_realloc(info->text_keys, sizeof(char*) * (info->text_num + 1))); + char** new_strings = (char**)(lodepng_realloc(info->text_strings, sizeof(char*) * (info->text_num + 1))); + + if(new_keys) info->text_keys = new_keys; + if(new_strings) info->text_strings = new_strings; + + if(!new_keys || !new_strings) return 83; /*alloc fail*/ + + ++info->text_num; + info->text_keys[info->text_num - 1] = alloc_string(key); + info->text_strings[info->text_num - 1] = alloc_string_sized(str, size); + if(!info->text_keys[info->text_num - 1] || !info->text_strings[info->text_num - 1]) return 83; /*alloc fail*/ + + return 0; +} + +unsigned lodepng_add_text(LodePNGInfo* info, const char* key, const char* str) { + return lodepng_add_text_sized(info, key, str, lodepng_strlen(str)); +} + +void lodepng_clear_text(LodePNGInfo* info) { + LodePNGText_cleanup(info); + /*cleanup only deconstructs, need to init again to set appropriate pointers to NULL*/ + LodePNGText_init(info); +} + +/******************************************************************************/ + +static void LodePNGIText_init(LodePNGInfo* info) { + info->itext_num = 0; + info->itext_keys = NULL; + info->itext_langtags = NULL; + info->itext_transkeys = NULL; + info->itext_strings = NULL; +} + +static void LodePNGIText_cleanup(LodePNGInfo* info) { + size_t i; + for(i = 0; i != info->itext_num; ++i) { + lodepng_free(info->itext_keys[i]); + lodepng_free(info->itext_langtags[i]); + lodepng_free(info->itext_transkeys[i]); + lodepng_free(info->itext_strings[i]); + } + lodepng_free(info->itext_keys); + lodepng_free(info->itext_langtags); + lodepng_free(info->itext_transkeys); + lodepng_free(info->itext_strings); +} + +static unsigned LodePNGIText_copy(LodePNGInfo* dest, const LodePNGInfo* source) { + size_t i = 0; + dest->itext_keys = NULL; + dest->itext_langtags = NULL; + dest->itext_transkeys = NULL; + dest->itext_strings = NULL; + dest->itext_num = 0; + for(i = 0; i != source->itext_num; ++i) { + CERROR_TRY_RETURN(lodepng_add_itext(dest, source->itext_keys[i], source->itext_langtags[i], + source->itext_transkeys[i], source->itext_strings[i])); + } + return 0; +} + +void lodepng_clear_itext(LodePNGInfo* info) { + LodePNGIText_cleanup(info); + /*cleanup only deconstructs, need to init again to set appropriate pointers to NULL*/ + LodePNGIText_init(info); +} + +static unsigned lodepng_add_itext_sized(LodePNGInfo* info, const char* key, const char* langtag, + const char* transkey, const char* str, size_t size) { + char** new_keys = (char**)(lodepng_realloc(info->itext_keys, sizeof(char*) * (info->itext_num + 1))); + char** new_langtags = (char**)(lodepng_realloc(info->itext_langtags, sizeof(char*) * (info->itext_num + 1))); + char** new_transkeys = (char**)(lodepng_realloc(info->itext_transkeys, sizeof(char*) * (info->itext_num + 1))); + char** new_strings = (char**)(lodepng_realloc(info->itext_strings, sizeof(char*) * (info->itext_num + 1))); + + if(new_keys) info->itext_keys = new_keys; + if(new_langtags) info->itext_langtags = new_langtags; + if(new_transkeys) info->itext_transkeys = new_transkeys; + if(new_strings) info->itext_strings = new_strings; + + if(!new_keys || !new_langtags || !new_transkeys || !new_strings) return 83; /*alloc fail*/ + + ++info->itext_num; + + info->itext_keys[info->itext_num - 1] = alloc_string(key); + info->itext_langtags[info->itext_num - 1] = alloc_string(langtag); + info->itext_transkeys[info->itext_num - 1] = alloc_string(transkey); + info->itext_strings[info->itext_num - 1] = alloc_string_sized(str, size); + + return 0; +} + +unsigned lodepng_add_itext(LodePNGInfo* info, const char* key, const char* langtag, + const char* transkey, const char* str) { + return lodepng_add_itext_sized(info, key, langtag, transkey, str, lodepng_strlen(str)); +} + +unsigned lodepng_set_icc(LodePNGInfo* info, const char* name, const unsigned char* profile, unsigned profile_size) { + if(info->iccp_defined) lodepng_clear_icc(info); + + if(profile_size == 0) return 123; /*invalid ICC profile size*/ + + info->iccp_name = alloc_string(name); + if(!info->iccp_name) return 83; /*alloc fail*/ + + info->iccp_profile = (unsigned char*)lodepng_malloc(profile_size); + if(!info->iccp_profile) { + lodepng_free(info->iccp_name); + return 83; /*alloc fail*/ + } + + lodepng_memcpy(info->iccp_profile, profile, profile_size); + info->iccp_profile_size = profile_size; + info->iccp_defined = 1; + + return 0; /*ok*/ +} + +static void lodepng_init_icc(LodePNGInfo* info) { + info->iccp_defined = 0; + info->iccp_name = NULL; + info->iccp_profile = NULL; + info->iccp_profile_size = 0; +} + +void lodepng_clear_icc(LodePNGInfo* info) { + lodepng_free(info->iccp_name); + lodepng_free(info->iccp_profile); + lodepng_init_icc(info); +} + +unsigned lodepng_set_exif(LodePNGInfo* info, const unsigned char* exif, unsigned exif_size) { + if(info->exif_defined) lodepng_clear_exif(info); + info->exif = (unsigned char*)lodepng_malloc(exif_size); + + if(!info->exif) return 83; /*alloc fail*/ + + lodepng_memcpy(info->exif, exif, exif_size); + info->exif_size = exif_size; + info->exif_defined = 1; + + return 0; /*ok*/ +} + +static void lodepng_init_exif(LodePNGInfo* info) { + info->exif_defined = 0; + info->exif = NULL; + info->exif_size = 0; +} + +void lodepng_clear_exif(LodePNGInfo* info) { + lodepng_free(info->exif); + lodepng_init_exif(info); +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +void lodepng_info_init(LodePNGInfo* info) { + lodepng_color_mode_init(&info->color); + info->interlace_method = 0; + info->compression_method = 0; + info->filter_method = 0; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + info->background_defined = 0; + info->background_r = info->background_g = info->background_b = 0; + + LodePNGText_init(info); + LodePNGIText_init(info); + lodepng_init_icc(info); + lodepng_init_exif(info); + + info->time_defined = 0; + info->phys_defined = 0; + + info->gama_defined = 0; + info->chrm_defined = 0; + info->srgb_defined = 0; + info->cicp_defined = 0; + info->cicp_color_primaries = 0; + info->cicp_transfer_function = 0; + info->cicp_matrix_coefficients = 0; + info->cicp_video_full_range_flag = 0; + info->mdcv_defined = 0; + info->mdcv_red_x = 0; + info->mdcv_red_y = 0; + info->mdcv_green_x = 0; + info->mdcv_green_y = 0; + info->mdcv_blue_x = 0; + info->mdcv_blue_y = 0; + info->mdcv_white_x = 0; + info->mdcv_white_y = 0; + info->mdcv_max_luminance = 0; + info->mdcv_min_luminance = 0; + info->clli_defined = 0; + info->clli_max_cll = 0; + info->clli_max_fall = 0; + + info->sbit_defined = 0; + info->sbit_r = info->sbit_g = info->sbit_b = info->sbit_a = 0; + + LodePNGUnknownChunks_init(info); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} + +void lodepng_info_cleanup(LodePNGInfo* info) { + lodepng_color_mode_cleanup(&info->color); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + LodePNGText_cleanup(info); + LodePNGIText_cleanup(info); + + lodepng_clear_icc(info); + lodepng_clear_exif(info); + + LodePNGUnknownChunks_cleanup(info); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} + +unsigned lodepng_info_copy(LodePNGInfo* dest, const LodePNGInfo* source) { + lodepng_info_cleanup(dest); + lodepng_memcpy(dest, source, sizeof(LodePNGInfo)); + + /*ensure to initialize all fields pointing to allocated data to NULL first*/ + lodepng_color_mode_init(&dest->color); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + LodePNGText_init(dest); + LodePNGIText_init(dest); + lodepng_init_icc(dest); + lodepng_init_exif(dest); + LodePNGUnknownChunks_init(dest); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + + CERROR_TRY_RETURN(lodepng_color_mode_copy(&dest->color, &source->color)); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + CERROR_TRY_RETURN(LodePNGText_copy(dest, source)); + CERROR_TRY_RETURN(LodePNGIText_copy(dest, source)); + if(source->iccp_defined) { + CERROR_TRY_RETURN(lodepng_set_icc(dest, source->iccp_name, source->iccp_profile, source->iccp_profile_size)); + } + if(source->exif_defined) { + CERROR_TRY_RETURN(lodepng_set_exif(dest, source->exif, source->exif_size)); + } + CERROR_TRY_RETURN(LodePNGUnknownChunks_copy(dest, source)); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + + return 0; +} + +/* ////////////////////////////////////////////////////////////////////////// */ + +/*index: bitgroup index, bits: bitgroup size(1, 2 or 4), in: bitgroup value, out: octet array to add bits to*/ +static void addColorBits(unsigned char* out, size_t index, unsigned bits, unsigned in) { + unsigned m = bits == 1 ? 7 : bits == 2 ? 3 : 1; /*8 / bits - 1*/ + /*p = the partial index in the byte, e.g. with 4 palettebits it is 0 for first half or 1 for second half*/ + unsigned p = index & m; + in &= (1u << bits) - 1u; /*filter out any other bits of the input value*/ + in = in << (bits * (m - p)); + if(p == 0) out[index * bits / 8u] = in; + else out[index * bits / 8u] |= in; +} + +typedef struct ColorTree ColorTree; + +/* +One node of a color tree +This is the data structure used to count the number of unique colors and to get a palette +index for a color. It's like an octree, but because the alpha channel is used too, each +node has 16 instead of 8 children. +*/ +struct ColorTree { + ColorTree* children[16]; /*up to 16 pointers to ColorTree of next level*/ + int index; /*the payload. Only has a meaningful value if this is in the last level*/ +}; + +static void color_tree_init(ColorTree* tree) { + lodepng_memset(tree->children, 0, 16 * sizeof(*tree->children)); + tree->index = -1; +} + +static void color_tree_cleanup(ColorTree* tree) { + int i; + for(i = 0; i != 16; ++i) { + if(tree->children[i]) { + color_tree_cleanup(tree->children[i]); + lodepng_free(tree->children[i]); + } + } +} + +/*returns -1 if color not present, its index otherwise*/ +static int color_tree_get(ColorTree* tree, unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + int bit = 0; + for(bit = 0; bit < 8; ++bit) { + int i = 8 * ((r >> bit) & 1) + 4 * ((g >> bit) & 1) + 2 * ((b >> bit) & 1) + 1 * ((a >> bit) & 1); + if(!tree->children[i]) return -1; + else tree = tree->children[i]; + } + return tree ? tree->index : -1; +} + +#ifdef LODEPNG_COMPILE_ENCODER +static int color_tree_has(ColorTree* tree, unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + return color_tree_get(tree, r, g, b, a) >= 0; +} +#endif /*LODEPNG_COMPILE_ENCODER*/ + +/*color is not allowed to already exist. +Index should be >= 0 (it's signed to be compatible with using -1 for "doesn't exist") +Returns error code, or 0 if ok*/ +static unsigned color_tree_add(ColorTree* tree, + unsigned char r, unsigned char g, unsigned char b, unsigned char a, unsigned index) { + int bit; + for(bit = 0; bit < 8; ++bit) { + int i = 8 * ((r >> bit) & 1) + 4 * ((g >> bit) & 1) + 2 * ((b >> bit) & 1) + 1 * ((a >> bit) & 1); + if(!tree->children[i]) { + tree->children[i] = (ColorTree*)lodepng_malloc(sizeof(ColorTree)); + if(!tree->children[i]) return 83; /*alloc fail*/ + color_tree_init(tree->children[i]); + } + tree = tree->children[i]; + } + tree->index = (int)index; + return 0; +} + +/*put a pixel, given its RGBA color, into image of any color type*/ +static unsigned rgba8ToPixel(unsigned char* out, size_t i, + const LodePNGColorMode* mode, ColorTree* tree /*for palette*/, + unsigned char r, unsigned char g, unsigned char b, unsigned char a) { + if(mode->colortype == LCT_GREY) { + unsigned char gray = r; /*((unsigned short)r + g + b) / 3u;*/ + if(mode->bitdepth == 8) out[i] = gray; + else if(mode->bitdepth == 16) out[i * 2 + 0] = out[i * 2 + 1] = gray; + else { + /*take the most significant bits of gray*/ + gray = ((unsigned)gray >> (8u - mode->bitdepth)) & ((1u << mode->bitdepth) - 1u); + addColorBits(out, i, mode->bitdepth, gray); + } + } else if(mode->colortype == LCT_RGB) { + if(mode->bitdepth == 8) { + out[i * 3 + 0] = r; + out[i * 3 + 1] = g; + out[i * 3 + 2] = b; + } else { + out[i * 6 + 0] = out[i * 6 + 1] = r; + out[i * 6 + 2] = out[i * 6 + 3] = g; + out[i * 6 + 4] = out[i * 6 + 5] = b; + } + } else if(mode->colortype == LCT_PALETTE) { + int index = color_tree_get(tree, r, g, b, a); + if(index < 0) return 82; /*color not in palette*/ + if(mode->bitdepth == 8) out[i] = index; + else addColorBits(out, i, mode->bitdepth, (unsigned)index); + } else if(mode->colortype == LCT_GREY_ALPHA) { + unsigned char gray = r; /*((unsigned short)r + g + b) / 3u;*/ + if(mode->bitdepth == 8) { + out[i * 2 + 0] = gray; + out[i * 2 + 1] = a; + } else if(mode->bitdepth == 16) { + out[i * 4 + 0] = out[i * 4 + 1] = gray; + out[i * 4 + 2] = out[i * 4 + 3] = a; + } + } else if(mode->colortype == LCT_RGBA) { + if(mode->bitdepth == 8) { + out[i * 4 + 0] = r; + out[i * 4 + 1] = g; + out[i * 4 + 2] = b; + out[i * 4 + 3] = a; + } else { + out[i * 8 + 0] = out[i * 8 + 1] = r; + out[i * 8 + 2] = out[i * 8 + 3] = g; + out[i * 8 + 4] = out[i * 8 + 5] = b; + out[i * 8 + 6] = out[i * 8 + 7] = a; + } + } + + return 0; /*no error*/ +} + +/*put a pixel, given its RGBA16 color, into image of any color 16-bitdepth type*/ +static void rgba16ToPixel(unsigned char* out, size_t i, + const LodePNGColorMode* mode, + unsigned short r, unsigned short g, unsigned short b, unsigned short a) { + if(mode->colortype == LCT_GREY) { + unsigned short gray = r; /*((unsigned)r + g + b) / 3u;*/ + out[i * 2 + 0] = (gray >> 8) & 255; + out[i * 2 + 1] = gray & 255; + } else if(mode->colortype == LCT_RGB) { + out[i * 6 + 0] = (r >> 8) & 255; + out[i * 6 + 1] = r & 255; + out[i * 6 + 2] = (g >> 8) & 255; + out[i * 6 + 3] = g & 255; + out[i * 6 + 4] = (b >> 8) & 255; + out[i * 6 + 5] = b & 255; + } else if(mode->colortype == LCT_GREY_ALPHA) { + unsigned short gray = r; /*((unsigned)r + g + b) / 3u;*/ + out[i * 4 + 0] = (gray >> 8) & 255; + out[i * 4 + 1] = gray & 255; + out[i * 4 + 2] = (a >> 8) & 255; + out[i * 4 + 3] = a & 255; + } else if(mode->colortype == LCT_RGBA) { + out[i * 8 + 0] = (r >> 8) & 255; + out[i * 8 + 1] = r & 255; + out[i * 8 + 2] = (g >> 8) & 255; + out[i * 8 + 3] = g & 255; + out[i * 8 + 4] = (b >> 8) & 255; + out[i * 8 + 5] = b & 255; + out[i * 8 + 6] = (a >> 8) & 255; + out[i * 8 + 7] = a & 255; + } +} + +/*Get RGBA8 color of pixel with index i (y * width + x) from the raw image with given color type.*/ +static void getPixelColorRGBA8(unsigned char* r, unsigned char* g, + unsigned char* b, unsigned char* a, + const unsigned char* in, size_t i, + const LodePNGColorMode* mode) { + if(mode->colortype == LCT_GREY) { + if(mode->bitdepth == 8) { + *r = *g = *b = in[i]; + if(mode->key_defined && *r == mode->key_r) *a = 0; + else *a = 255; + } else if(mode->bitdepth == 16) { + *r = *g = *b = in[i * 2 + 0]; + if(mode->key_defined && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r) *a = 0; + else *a = 255; + } else { + unsigned highest = ((1U << mode->bitdepth) - 1U); /*highest possible value for this bit depth*/ + size_t j = i * mode->bitdepth; + unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); + *r = *g = *b = (value * 255) / highest; + if(mode->key_defined && value == mode->key_r) *a = 0; + else *a = 255; + } + } else if(mode->colortype == LCT_RGB) { + if(mode->bitdepth == 8) { + *r = in[i * 3 + 0]; *g = in[i * 3 + 1]; *b = in[i * 3 + 2]; + if(mode->key_defined && *r == mode->key_r && *g == mode->key_g && *b == mode->key_b) *a = 0; + else *a = 255; + } else { + *r = in[i * 6 + 0]; + *g = in[i * 6 + 2]; + *b = in[i * 6 + 4]; + if(mode->key_defined && 256U * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r + && 256U * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g + && 256U * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b) *a = 0; + else *a = 255; + } + } else if(mode->colortype == LCT_PALETTE) { + unsigned index; + if(mode->bitdepth == 8) index = in[i]; + else { + size_t j = i * mode->bitdepth; + index = readBitsFromReversedStream(&j, in, mode->bitdepth); + } + /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ + *r = mode->palette[index * 4 + 0]; + *g = mode->palette[index * 4 + 1]; + *b = mode->palette[index * 4 + 2]; + *a = mode->palette[index * 4 + 3]; + } else if(mode->colortype == LCT_GREY_ALPHA) { + if(mode->bitdepth == 8) { + *r = *g = *b = in[i * 2 + 0]; + *a = in[i * 2 + 1]; + } else { + *r = *g = *b = in[i * 4 + 0]; + *a = in[i * 4 + 2]; + } + } else if(mode->colortype == LCT_RGBA) { + if(mode->bitdepth == 8) { + *r = in[i * 4 + 0]; + *g = in[i * 4 + 1]; + *b = in[i * 4 + 2]; + *a = in[i * 4 + 3]; + } else { + *r = in[i * 8 + 0]; + *g = in[i * 8 + 2]; + *b = in[i * 8 + 4]; + *a = in[i * 8 + 6]; + } + } +} + +/*Similar to getPixelColorRGBA8, but with all the for loops inside of the color +mode test cases, optimized to convert the colors much faster, when converting +to the common case of RGBA with 8 bit per channel. buffer must be RGBA with +enough memory.*/ +static void getPixelColorsRGBA8(unsigned char* LODEPNG_RESTRICT buffer, size_t numpixels, + const unsigned char* LODEPNG_RESTRICT in, + const LodePNGColorMode* mode) { + unsigned num_channels = 4; + size_t i; + if(mode->colortype == LCT_GREY) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i]; + buffer[3] = 255; + } + if(mode->key_defined) { + buffer -= numpixels * num_channels; + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + if(buffer[0] == mode->key_r) buffer[3] = 0; + } + } + } else if(mode->bitdepth == 16) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2]; + buffer[3] = mode->key_defined && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r ? 0 : 255; + } + } else { + unsigned highest = ((1U << mode->bitdepth) - 1U); /*highest possible value for this bit depth*/ + size_t j = 0; + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); + buffer[0] = buffer[1] = buffer[2] = (value * 255) / highest; + buffer[3] = mode->key_defined && value == mode->key_r ? 0 : 255; + } + } + } else if(mode->colortype == LCT_RGB) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + lodepng_memcpy(buffer, &in[i * 3], 3); + buffer[3] = 255; + } + if(mode->key_defined) { + buffer -= numpixels * num_channels; + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + if(buffer[0] == mode->key_r && buffer[1]== mode->key_g && buffer[2] == mode->key_b) buffer[3] = 0; + } + } + } else { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 6 + 0]; + buffer[1] = in[i * 6 + 2]; + buffer[2] = in[i * 6 + 4]; + buffer[3] = mode->key_defined + && 256U * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r + && 256U * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g + && 256U * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b ? 0 : 255; + } + } + } else if(mode->colortype == LCT_PALETTE) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = in[i]; + /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 4); + } + } else { + size_t j = 0; + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = readBitsFromReversedStream(&j, in, mode->bitdepth); + /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 4); + } + } + } else if(mode->colortype == LCT_GREY_ALPHA) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2 + 0]; + buffer[3] = in[i * 2 + 1]; + } + } else { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 4 + 0]; + buffer[3] = in[i * 4 + 2]; + } + } + } else if(mode->colortype == LCT_RGBA) { + if(mode->bitdepth == 8) { + lodepng_memcpy(buffer, in, numpixels * 4); + } else { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 8 + 0]; + buffer[1] = in[i * 8 + 2]; + buffer[2] = in[i * 8 + 4]; + buffer[3] = in[i * 8 + 6]; + } + } + } +} + +/*Similar to getPixelColorsRGBA8, but with 3-channel RGB output.*/ +static void getPixelColorsRGB8(unsigned char* LODEPNG_RESTRICT buffer, size_t numpixels, + const unsigned char* LODEPNG_RESTRICT in, + const LodePNGColorMode* mode) { + const unsigned num_channels = 3; + size_t i; + if(mode->colortype == LCT_GREY) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i]; + } + } else if(mode->bitdepth == 16) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2]; + } + } else { + unsigned highest = ((1U << mode->bitdepth) - 1U); /*highest possible value for this bit depth*/ + size_t j = 0; + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); + buffer[0] = buffer[1] = buffer[2] = (value * 255) / highest; + } + } + } else if(mode->colortype == LCT_RGB) { + if(mode->bitdepth == 8) { + lodepng_memcpy(buffer, in, numpixels * 3); + } else { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 6 + 0]; + buffer[1] = in[i * 6 + 2]; + buffer[2] = in[i * 6 + 4]; + } + } + } else if(mode->colortype == LCT_PALETTE) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = in[i]; + /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 3); + } + } else { + size_t j = 0; + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + unsigned index = readBitsFromReversedStream(&j, in, mode->bitdepth); + /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ + lodepng_memcpy(buffer, &mode->palette[index * 4], 3); + } + } + } else if(mode->colortype == LCT_GREY_ALPHA) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 2 + 0]; + } + } else { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = buffer[1] = buffer[2] = in[i * 4 + 0]; + } + } + } else if(mode->colortype == LCT_RGBA) { + if(mode->bitdepth == 8) { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + lodepng_memcpy(buffer, &in[i * 4], 3); + } + } else { + for(i = 0; i != numpixels; ++i, buffer += num_channels) { + buffer[0] = in[i * 8 + 0]; + buffer[1] = in[i * 8 + 2]; + buffer[2] = in[i * 8 + 4]; + } + } + } +} + +/*Get RGBA16 color of pixel with index i (y * width + x) from the raw image with +given color type, but the given color type must be 16-bit itself.*/ +static void getPixelColorRGBA16(unsigned short* r, unsigned short* g, unsigned short* b, unsigned short* a, + const unsigned char* in, size_t i, const LodePNGColorMode* mode) { + if(mode->colortype == LCT_GREY) { + *r = *g = *b = 256 * in[i * 2 + 0] + in[i * 2 + 1]; + if(mode->key_defined && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r) *a = 0; + else *a = 65535; + } else if(mode->colortype == LCT_RGB) { + *r = 256u * in[i * 6 + 0] + in[i * 6 + 1]; + *g = 256u * in[i * 6 + 2] + in[i * 6 + 3]; + *b = 256u * in[i * 6 + 4] + in[i * 6 + 5]; + if(mode->key_defined + && 256u * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r + && 256u * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g + && 256u * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b) *a = 0; + else *a = 65535; + } else if(mode->colortype == LCT_GREY_ALPHA) { + *r = *g = *b = 256u * in[i * 4 + 0] + in[i * 4 + 1]; + *a = 256u * in[i * 4 + 2] + in[i * 4 + 3]; + } else if(mode->colortype == LCT_RGBA) { + *r = 256u * in[i * 8 + 0] + in[i * 8 + 1]; + *g = 256u * in[i * 8 + 2] + in[i * 8 + 3]; + *b = 256u * in[i * 8 + 4] + in[i * 8 + 5]; + *a = 256u * in[i * 8 + 6] + in[i * 8 + 7]; + } +} + +unsigned lodepng_convert(unsigned char* out, const unsigned char* in, + const LodePNGColorMode* mode_out, const LodePNGColorMode* mode_in, + unsigned w, unsigned h) { + size_t i; + ColorTree tree; + size_t numpixels = (size_t)w * (size_t)h; + unsigned error = 0; + + if(mode_in->colortype == LCT_PALETTE && !mode_in->palette) { + return 107; /* error: must provide palette if input mode is palette */ + } + + if(lodepng_color_mode_equal(mode_out, mode_in)) { + size_t numbytes = lodepng_get_raw_size(w, h, mode_in); + lodepng_memcpy(out, in, numbytes); + return 0; + } + + if(mode_out->colortype == LCT_PALETTE) { + size_t palettesize = mode_out->palettesize; + const unsigned char* palette = mode_out->palette; + size_t palsize = (size_t)1u << mode_out->bitdepth; + /*if the user specified output palette but did not give the values, assume + they want the values of the input color type (assuming that one is palette). + Note that we never create a new palette ourselves.*/ + if(palettesize == 0) { + palettesize = mode_in->palettesize; + palette = mode_in->palette; + /*if the input was also palette with same bitdepth, then the color types are also + equal, so copy literally. This to preserve the exact indices that were in the PNG + even in case there are duplicate colors in the palette.*/ + if(mode_in->colortype == LCT_PALETTE && mode_in->bitdepth == mode_out->bitdepth) { + size_t numbytes = lodepng_get_raw_size(w, h, mode_in); + lodepng_memcpy(out, in, numbytes); + return 0; + } + } + if(palettesize < palsize) palsize = palettesize; + color_tree_init(&tree); + for(i = 0; i != palsize; ++i) { + const unsigned char* p = &palette[i * 4]; + error = color_tree_add(&tree, p[0], p[1], p[2], p[3], (unsigned)i); + if(error) break; + } + } + + if(!error) { + if(mode_in->bitdepth == 16 && mode_out->bitdepth == 16) { + for(i = 0; i != numpixels; ++i) { + unsigned short r = 0, g = 0, b = 0, a = 0; + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + rgba16ToPixel(out, i, mode_out, r, g, b, a); + } + } else if(mode_out->bitdepth == 8 && mode_out->colortype == LCT_RGBA) { + getPixelColorsRGBA8(out, numpixels, in, mode_in); + } else if(mode_out->bitdepth == 8 && mode_out->colortype == LCT_RGB) { + getPixelColorsRGB8(out, numpixels, in, mode_in); + } else { + unsigned char r = 0, g = 0, b = 0, a = 0; + for(i = 0; i != numpixels; ++i) { + getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); + error = rgba8ToPixel(out, i, mode_out, &tree, r, g, b, a); + if(error) break; + } + } + } + + if(mode_out->colortype == LCT_PALETTE) { + color_tree_cleanup(&tree); + } + + return error; +} + + +/* Converts a single rgb color without alpha from one type to another, color bits truncated to +their bitdepth. In case of single channel (gray or palette), only the r channel is used. Slow +function, do not use to process all pixels of an image. Alpha channel not supported on purpose: +this is for bKGD, supporting alpha may prevent it from finding a color in the palette, from the +specification it looks like bKGD should ignore the alpha values of the palette since it can use +any palette index but doesn't have an alpha channel. Idem with ignoring color key. */ +unsigned lodepng_convert_rgb( + unsigned* r_out, unsigned* g_out, unsigned* b_out, + unsigned r_in, unsigned g_in, unsigned b_in, + const LodePNGColorMode* mode_out, const LodePNGColorMode* mode_in) { + unsigned r = 0, g = 0, b = 0; + unsigned mul = 65535 / ((1u << mode_in->bitdepth) - 1u); /*65535, 21845, 4369, 257, 1*/ + unsigned shift = 16 - mode_out->bitdepth; + + if(mode_in->colortype == LCT_GREY || mode_in->colortype == LCT_GREY_ALPHA) { + r = g = b = r_in * mul; + } else if(mode_in->colortype == LCT_RGB || mode_in->colortype == LCT_RGBA) { + r = r_in * mul; + g = g_in * mul; + b = b_in * mul; + } else if(mode_in->colortype == LCT_PALETTE) { + if(r_in >= mode_in->palettesize) return 82; + r = mode_in->palette[r_in * 4 + 0] * 257u; + g = mode_in->palette[r_in * 4 + 1] * 257u; + b = mode_in->palette[r_in * 4 + 2] * 257u; + } else { + return 31; + } + + /* now convert to output format */ + if(mode_out->colortype == LCT_GREY || mode_out->colortype == LCT_GREY_ALPHA) { + *r_out = r >> shift ; + } else if(mode_out->colortype == LCT_RGB || mode_out->colortype == LCT_RGBA) { + *r_out = r >> shift ; + *g_out = g >> shift ; + *b_out = b >> shift ; + } else if(mode_out->colortype == LCT_PALETTE) { + unsigned i; + /* a 16-bit color cannot be in the palette */ + if((r >> 8) != (r & 255) || (g >> 8) != (g & 255) || (b >> 8) != (b & 255)) return 82; + for(i = 0; i < mode_out->palettesize; i++) { + unsigned j = i * 4; + if((r >> 8) == mode_out->palette[j + 0] && (g >> 8) == mode_out->palette[j + 1] && + (b >> 8) == mode_out->palette[j + 2]) { + *r_out = i; + return 0; + } + } + return 82; + } else { + return 31; + } + + return 0; +} + +#ifdef LODEPNG_COMPILE_ENCODER + +void lodepng_color_stats_init(LodePNGColorStats* stats) { + /*stats*/ + stats->colored = 0; + stats->key = 0; + stats->key_r = stats->key_g = stats->key_b = 0; + stats->alpha = 0; + stats->numcolors = 0; + stats->bits = 1; + stats->numpixels = 0; + /*settings*/ + stats->allow_palette = 1; + stats->allow_greyscale = 1; +} + +/*function used for debug purposes with C++*/ +/*void printColorStats(LodePNGColorStats* p) { + std::cout << "colored: " << (int)p->colored << ", "; + std::cout << "key: " << (int)p->key << ", "; + std::cout << "key_r: " << (int)p->key_r << ", "; + std::cout << "key_g: " << (int)p->key_g << ", "; + std::cout << "key_b: " << (int)p->key_b << ", "; + std::cout << "alpha: " << (int)p->alpha << ", "; + std::cout << "numcolors: " << (int)p->numcolors << ", "; + std::cout << "bits: " << (int)p->bits << std::endl; +}*/ + +/*Returns how many bits needed to represent given value (max 8 bit)*/ +static unsigned getValueRequiredBits(unsigned char value) { + if(value == 0 || value == 255) return 1; + /*The scaling of 2-bit and 4-bit values uses multiples of 85 and 17*/ + if(value % 17 == 0) return value % 85 == 0 ? 2 : 4; + return 8; +} + +/*stats must already have been inited. */ +unsigned lodepng_compute_color_stats(LodePNGColorStats* stats, + const unsigned char* in, unsigned w, unsigned h, + const LodePNGColorMode* mode_in) { + size_t i; + ColorTree tree; + size_t numpixels = (size_t)w * (size_t)h; + unsigned error = 0; + + /* mark things as done already if it would be impossible to have a more expensive case */ + unsigned colored_done = lodepng_is_greyscale_type(mode_in) ? 1 : 0; + unsigned alpha_done = lodepng_can_have_alpha(mode_in) ? 0 : 1; + unsigned numcolors_done = 0; + unsigned bpp = lodepng_get_bpp(mode_in); + unsigned bits_done = (stats->bits == 1 && bpp == 1) ? 1 : 0; + unsigned sixteen = 0; /* whether the input image is 16 bit */ + unsigned maxnumcolors = 257; + if(bpp <= 8) maxnumcolors = LODEPNG_MIN(257, stats->numcolors + (1u << bpp)); + + stats->numpixels += numpixels; + + /*if palette not allowed, no need to compute numcolors*/ + if(!stats->allow_palette) numcolors_done = 1; + + color_tree_init(&tree); + + /*If the stats was already filled in from previous data, fill its palette in tree + and mark things as done already if we know they are the most expensive case already*/ + if(stats->alpha) alpha_done = 1; + if(stats->colored) colored_done = 1; + if(stats->bits == 16) numcolors_done = 1; + if(stats->bits >= bpp) bits_done = 1; + if(stats->numcolors >= maxnumcolors) numcolors_done = 1; + + if(!numcolors_done) { + for(i = 0; i < stats->numcolors; i++) { + const unsigned char* color = &stats->palette[i * 4]; + error = color_tree_add(&tree, color[0], color[1], color[2], color[3], (unsigned)i); + if(error) goto cleanup; + } + } + + /*Check if the 16-bit input is truly 16-bit*/ + if(mode_in->bitdepth == 16 && !sixteen) { + unsigned short r = 0, g = 0, b = 0, a = 0; + for(i = 0; i != numpixels; ++i) { + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + if((r & 255) != ((r >> 8) & 255) || (g & 255) != ((g >> 8) & 255) || + (b & 255) != ((b >> 8) & 255) || (a & 255) != ((a >> 8) & 255)) /*first and second byte differ*/ { + stats->bits = 16; + sixteen = 1; + bits_done = 1; + numcolors_done = 1; /*counting colors no longer useful, palette doesn't support 16-bit*/ + break; + } + } + } + + if(sixteen) { + unsigned short r = 0, g = 0, b = 0, a = 0; + + for(i = 0; i != numpixels; ++i) { + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + + if(!colored_done && (r != g || r != b)) { + stats->colored = 1; + colored_done = 1; + } + + if(!alpha_done) { + unsigned matchkey = (r == stats->key_r && g == stats->key_g && b == stats->key_b); + if(a != 65535 && (a != 0 || (stats->key && !matchkey))) { + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + } else if(a == 0 && !stats->alpha && !stats->key) { + stats->key = 1; + stats->key_r = r; + stats->key_g = g; + stats->key_b = b; + } else if(a == 65535 && stats->key && matchkey) { + /* Color key cannot be used if an opaque pixel also has that RGB color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + } + } + if(alpha_done && numcolors_done && colored_done && bits_done) break; + } + + if(stats->key && !stats->alpha) { + for(i = 0; i != numpixels; ++i) { + getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); + if(a != 0 && r == stats->key_r && g == stats->key_g && b == stats->key_b) { + /* Color key cannot be used if an opaque pixel also has that RGB color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + } + } + } + } else /* < 16-bit */ { + unsigned char r = 0, g = 0, b = 0, a = 0; + unsigned char pr = 0, pg = 0, pb = 0, pa = 0; + for(i = 0; i != numpixels; ++i) { + getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); + + /*skip if color same as before, this speeds up large non-photographic + images with many same colors by avoiding 'color_tree_has' below */ + if(i != 0 && r == pr && g == pg && b == pb && a == pa) continue; + pr = r; + pg = g; + pb = b; + pa = a; + + if(!bits_done && stats->bits < 8) { + /*only r is checked, < 8 bits is only relevant for grayscale*/ + unsigned bits = getValueRequiredBits(r); + if(bits > stats->bits) stats->bits = bits; + } + bits_done = (stats->bits >= bpp); + + if(!colored_done && (r != g || r != b)) { + stats->colored = 1; + colored_done = 1; + if(stats->bits < 8) stats->bits = 8; /*PNG has no colored modes with less than 8-bit per channel*/ + } + + if(!alpha_done) { + unsigned matchkey = (r == stats->key_r && g == stats->key_g && b == stats->key_b); + if(a != 255 && (a != 0 || (stats->key && !matchkey))) { + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + if(stats->bits < 8) stats->bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ + } else if(a == 0 && !stats->alpha && !stats->key) { + stats->key = 1; + stats->key_r = r; + stats->key_g = g; + stats->key_b = b; + } else if(a == 255 && stats->key && matchkey) { + /* Color key cannot be used if an opaque pixel also has that RGB color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + if(stats->bits < 8) stats->bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ + } + } + + if(!numcolors_done) { + if(!color_tree_has(&tree, r, g, b, a)) { + error = color_tree_add(&tree, r, g, b, a, stats->numcolors); + if(error) goto cleanup; + if(stats->numcolors < 256) { + unsigned char* p = stats->palette; + unsigned n = stats->numcolors; + p[n * 4 + 0] = r; + p[n * 4 + 1] = g; + p[n * 4 + 2] = b; + p[n * 4 + 3] = a; + } + ++stats->numcolors; + numcolors_done = stats->numcolors >= maxnumcolors; + } + } + + if(alpha_done && numcolors_done && colored_done && bits_done) break; + } + + if(stats->key && !stats->alpha) { + for(i = 0; i != numpixels; ++i) { + getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); + if(a != 0 && r == stats->key_r && g == stats->key_g && b == stats->key_b) { + /* Color key cannot be used if an opaque pixel also has that RGB color. */ + stats->alpha = 1; + stats->key = 0; + alpha_done = 1; + if(stats->bits < 8) stats->bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ + } + } + } + + /*make the stats's key always 16-bit for consistency - repeat each byte twice*/ + stats->key_r += (stats->key_r << 8); + stats->key_g += (stats->key_g << 8); + stats->key_b += (stats->key_b << 8); + } + +cleanup: + color_tree_cleanup(&tree); + return error; +} + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +/*Adds a single color to the color stats. The stats must already have been inited. The color must be given as 16-bit +(with 2 bytes repeating for 8-bit and 65535 for opaque alpha channel). This function is expensive, do not call it for +all pixels of an image but only for a few additional values. */ +static unsigned lodepng_color_stats_add(LodePNGColorStats* stats, + unsigned r, unsigned g, unsigned b, unsigned a) { + unsigned error = 0; + unsigned char image[8]; + LodePNGColorMode mode; + lodepng_color_mode_init(&mode); + image[0] = r >> 8; image[1] = r; image[2] = g >> 8; image[3] = g; + image[4] = b >> 8; image[5] = b; image[6] = a >> 8; image[7] = a; + mode.bitdepth = 16; + mode.colortype = LCT_RGBA; + error = lodepng_compute_color_stats(stats, image, 1, 1, &mode); + lodepng_color_mode_cleanup(&mode); + return error; +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +/*Computes a minimal PNG color model that can contain all colors as indicated by the stats. +The stats should be computed with lodepng_compute_color_stats. +mode_in is raw color profile of the image the stats were computed on, to copy palette order from when relevant. +Minimal PNG color model means the color type and bit depth that gives smallest amount of bits in the output image, +e.g. gray if only grayscale pixels, palette if less than 256 colors, color key if only single transparent color, ... +This is used if auto_convert is enabled (it is by default). +*/ +static unsigned auto_choose_color(LodePNGColorMode* mode_out, + const LodePNGColorMode* mode_in, + const LodePNGColorStats* stats) { + unsigned error = 0; + unsigned palettebits; + size_t i, n; + size_t numpixels = stats->numpixels; + unsigned palette_ok, gray_ok; + + unsigned alpha = stats->alpha; + unsigned key = stats->key; + unsigned bits = stats->bits; + + mode_out->key_defined = 0; + + if(key && numpixels <= 16) { + alpha = 1; /*too few pixels to justify tRNS chunk overhead*/ + key = 0; + if(bits < 8) bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ + } + + gray_ok = !stats->colored; + if(!stats->allow_greyscale) gray_ok = 0; + if(!gray_ok && bits < 8) bits = 8; + + n = stats->numcolors; + palettebits = n <= 2 ? 1 : (n <= 4 ? 2 : (n <= 16 ? 4 : 8)); + palette_ok = n <= 256 && bits <= 8 && n != 0; /*n==0 means likely numcolors wasn't computed*/ + if(numpixels < n * 2) palette_ok = 0; /*don't add palette overhead if image has only a few pixels*/ + if(gray_ok && !alpha && bits <= palettebits) palette_ok = 0; /*gray is less overhead*/ + if(!stats->allow_palette) palette_ok = 0; + + if(palette_ok) { + const unsigned char* p = stats->palette; + lodepng_palette_clear(mode_out); /*remove potential earlier palette*/ + for(i = 0; i != stats->numcolors; ++i) { + error = lodepng_palette_add(mode_out, p[i * 4 + 0], p[i * 4 + 1], p[i * 4 + 2], p[i * 4 + 3]); + if(error) break; + } + + mode_out->colortype = LCT_PALETTE; + mode_out->bitdepth = palettebits; + + if(mode_in->colortype == LCT_PALETTE && mode_in->palettesize >= mode_out->palettesize + && mode_in->bitdepth == mode_out->bitdepth) { + /*If input should have same palette colors, keep original to preserve its order and prevent conversion*/ + lodepng_color_mode_cleanup(mode_out); /*clears palette, keeps the above set colortype and bitdepth fields as-is*/ + lodepng_color_mode_copy(mode_out, mode_in); + } + } else /*8-bit or 16-bit per channel*/ { + mode_out->bitdepth = bits; + mode_out->colortype = alpha ? (gray_ok ? LCT_GREY_ALPHA : LCT_RGBA) + : (gray_ok ? LCT_GREY : LCT_RGB); + if(key) { + unsigned mask = (1u << mode_out->bitdepth) - 1u; /*stats always uses 16-bit, mask converts it*/ + mode_out->key_r = stats->key_r & mask; + mode_out->key_g = stats->key_g & mask; + mode_out->key_b = stats->key_b & mask; + mode_out->key_defined = 1; + } + } + + return error; +} + +#endif /* #ifdef LODEPNG_COMPILE_ENCODER */ + +/*Paeth predictor, used by PNG filter type 4*/ +static unsigned char paethPredictor(unsigned char a, unsigned char b, unsigned char c) { + /* the subtractions of unsigned char cast it to a signed type. + With gcc, short is faster than int, with clang int is as fast (as of april 2023)*/ + short pa = (b - c) < 0 ? -(b - c) : (b - c); + short pb = (a - c) < 0 ? -(a - c) : (a - c); + /* writing it out like this compiles to something faster than introducing a temp variable*/ + short pc = (a + b - c - c) < 0 ? -(a + b - c - c) : (a + b - c - c); + /* return input value associated with smallest of pa, pb, pc (with certain priority if equal) */ + if(pb < pa) { a = b; pa = pb; } + return (pc < pa) ? c : a; +} + +/*shared values used by multiple Adam7 related functions*/ + +static const unsigned ADAM7_IX[7] = { 0, 4, 0, 2, 0, 1, 0 }; /*x start values*/ +static const unsigned ADAM7_IY[7] = { 0, 0, 4, 0, 2, 0, 1 }; /*y start values*/ +static const unsigned ADAM7_DX[7] = { 8, 8, 4, 4, 2, 2, 1 }; /*x delta values*/ +static const unsigned ADAM7_DY[7] = { 8, 8, 8, 4, 4, 2, 2 }; /*y delta values*/ + +/* +Outputs various dimensions and positions in the image related to the Adam7 reduced images. +passw: output containing the width of the 7 passes +passh: output containing the height of the 7 passes +filter_passstart: output containing the index of the start and end of each + reduced image with filter bytes +padded_passstart output containing the index of the start and end of each + reduced image when without filter bytes but with padded scanlines +passstart: output containing the index of the start and end of each reduced + image without padding between scanlines, but still padding between the images +w, h: width and height of non-interlaced image +bpp: bits per pixel +"padded" is only relevant if bpp is less than 8 and a scanline or image does not + end at a full byte +*/ +static void Adam7_getpassvalues(unsigned passw[7], unsigned passh[7], size_t filter_passstart[8], + size_t padded_passstart[8], size_t passstart[8], unsigned w, unsigned h, unsigned bpp) { + /*the passstart values have 8 values: the 8th one indicates the byte after the end of the 7th (= last) pass*/ + unsigned i; + + /*calculate width and height in pixels of each pass*/ + for(i = 0; i != 7; ++i) { + passw[i] = (w + ADAM7_DX[i] - ADAM7_IX[i] - 1) / ADAM7_DX[i]; + passh[i] = (h + ADAM7_DY[i] - ADAM7_IY[i] - 1) / ADAM7_DY[i]; + if(passw[i] == 0) passh[i] = 0; + if(passh[i] == 0) passw[i] = 0; + } + + filter_passstart[0] = padded_passstart[0] = passstart[0] = 0; + for(i = 0; i != 7; ++i) { + /*if passw[i] is 0, it's 0 bytes, not 1 (no filtertype-byte)*/ + filter_passstart[i + 1] = filter_passstart[i] + + ((passw[i] && passh[i]) ? passh[i] * (1u + (passw[i] * bpp + 7u) / 8u) : 0); + /*bits padded if needed to fill full byte at end of each scanline*/ + padded_passstart[i + 1] = padded_passstart[i] + passh[i] * ((passw[i] * bpp + 7u) / 8u); + /*only padded at end of reduced image*/ + passstart[i + 1] = passstart[i] + (passh[i] * passw[i] * bpp + 7u) / 8u; + } +} + +#ifdef LODEPNG_COMPILE_DECODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / PNG Decoder / */ +/* ////////////////////////////////////////////////////////////////////////// */ + +/*read the information from the header and store it in the LodePNGInfo. return value is error*/ +unsigned lodepng_inspect(unsigned* w, unsigned* h, LodePNGState* state, + const unsigned char* in, size_t insize) { + unsigned width, height; + LodePNGInfo* info = &state->info_png; + if(insize == 0 || in == 0) { + CERROR_RETURN_ERROR(state->error, 48); /*error: the given data is empty*/ + } + if(insize < 33) { + CERROR_RETURN_ERROR(state->error, 27); /*error: the data length is smaller than the length of a PNG header*/ + } + + /*when decoding a new PNG image, make sure all parameters created after previous decoding are reset*/ + /* TODO: remove this. One should use a new LodePNGState for new sessions */ + lodepng_info_cleanup(info); + lodepng_info_init(info); + + if(in[0] != 137 || in[1] != 80 || in[2] != 78 || in[3] != 71 + || in[4] != 13 || in[5] != 10 || in[6] != 26 || in[7] != 10) { + CERROR_RETURN_ERROR(state->error, 28); /*error: the first 8 bytes are not the correct PNG signature*/ + } + if(lodepng_chunk_length(in + 8) != 13) { + CERROR_RETURN_ERROR(state->error, 94); /*error: header size must be 13 bytes*/ + } + if(!lodepng_chunk_type_equals(in + 8, "IHDR")) { + CERROR_RETURN_ERROR(state->error, 29); /*error: it doesn't start with a IHDR chunk!*/ + } + + /*read the values given in the header*/ + width = lodepng_read32bitInt(&in[16]); + height = lodepng_read32bitInt(&in[20]); + /*TODO: remove the undocumented feature that allows to give null pointers to width or height*/ + if(w) *w = width; + if(h) *h = height; + info->color.bitdepth = in[24]; + info->color.colortype = (LodePNGColorType)in[25]; + info->compression_method = in[26]; + info->filter_method = in[27]; + info->interlace_method = in[28]; + + /*errors returned only after the parsing so other values are still output*/ + + /*error: invalid image size*/ + if(width == 0 || height == 0) CERROR_RETURN_ERROR(state->error, 93); + /*error: invalid colortype or bitdepth combination*/ + state->error = checkColorValidity(info->color.colortype, info->color.bitdepth); + if(state->error) return state->error; + /*error: only compression method 0 is allowed in the specification*/ + if(info->compression_method != 0) CERROR_RETURN_ERROR(state->error, 32); + /*error: only filter method 0 is allowed in the specification*/ + if(info->filter_method != 0) CERROR_RETURN_ERROR(state->error, 33); + /*error: only interlace methods 0 and 1 exist in the specification*/ + if(info->interlace_method > 1) CERROR_RETURN_ERROR(state->error, 34); + + if(!state->decoder.ignore_crc) { + unsigned crc = lodepng_read32bitInt(&in[29]); + unsigned checksum = lodepng_crc32(&in[12], 17); + if(crc != checksum) { + CERROR_RETURN_ERROR(state->error, 57); /*invalid CRC*/ + } + } + + return state->error; +} + +static unsigned unfilterScanline(unsigned char* recon, const unsigned char* scanline, const unsigned char* precon, + size_t bytewidth, unsigned char filterType, size_t length) { + /* + For PNG filter method 0 + unfilter a PNG image scanline by scanline. when the pixels are smaller than 1 byte, + the filter works byte per byte (bytewidth = 1) + precon is the previous unfiltered scanline, recon the result, scanline the current one + the incoming scanlines do NOT include the filtertype byte, that one is given in the parameter filterType instead + recon and scanline MAY be the same memory address! precon must be disjoint. + */ + + size_t i; + switch(filterType) { + case 0: + for(i = 0; i != length; ++i) recon[i] = scanline[i]; + break; + case 1: { + size_t j = 0; + for(i = 0; i != bytewidth; ++i) recon[i] = scanline[i]; + for(i = bytewidth; i != length; ++i, ++j) recon[i] = scanline[i] + recon[j]; + break; + } + case 2: + if(precon) { + for(i = 0; i != length; ++i) recon[i] = scanline[i] + precon[i]; + } else { + for(i = 0; i != length; ++i) recon[i] = scanline[i]; + } + break; + case 3: + if(precon) { + size_t j = 0; + for(i = 0; i != bytewidth; ++i) recon[i] = scanline[i] + (precon[i] >> 1u); + /* Unroll independent paths of this predictor. A 6x and 8x version is also possible but that adds + too much code. Whether this speeds up anything depends on compiler and settings. */ + if(bytewidth >= 4) { + for(; i + 3 < length; i += 4, j += 4) { + unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1], s2 = scanline[i + 2], s3 = scanline[i + 3]; + unsigned char r0 = recon[j + 0], r1 = recon[j + 1], r2 = recon[j + 2], r3 = recon[j + 3]; + unsigned char p0 = precon[i + 0], p1 = precon[i + 1], p2 = precon[i + 2], p3 = precon[i + 3]; + recon[i + 0] = s0 + ((r0 + p0) >> 1u); + recon[i + 1] = s1 + ((r1 + p1) >> 1u); + recon[i + 2] = s2 + ((r2 + p2) >> 1u); + recon[i + 3] = s3 + ((r3 + p3) >> 1u); + } + } else if(bytewidth >= 3) { + for(; i + 2 < length; i += 3, j += 3) { + unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1], s2 = scanline[i + 2]; + unsigned char r0 = recon[j + 0], r1 = recon[j + 1], r2 = recon[j + 2]; + unsigned char p0 = precon[i + 0], p1 = precon[i + 1], p2 = precon[i + 2]; + recon[i + 0] = s0 + ((r0 + p0) >> 1u); + recon[i + 1] = s1 + ((r1 + p1) >> 1u); + recon[i + 2] = s2 + ((r2 + p2) >> 1u); + } + } else if(bytewidth >= 2) { + for(; i + 1 < length; i += 2, j += 2) { + unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1]; + unsigned char r0 = recon[j + 0], r1 = recon[j + 1]; + unsigned char p0 = precon[i + 0], p1 = precon[i + 1]; + recon[i + 0] = s0 + ((r0 + p0) >> 1u); + recon[i + 1] = s1 + ((r1 + p1) >> 1u); + } + } + for(; i != length; ++i, ++j) recon[i] = scanline[i] + ((recon[j] + precon[i]) >> 1u); + } else { + size_t j = 0; + for(i = 0; i != bytewidth; ++i) recon[i] = scanline[i]; + for(i = bytewidth; i != length; ++i, ++j) recon[i] = scanline[i] + (recon[j] >> 1u); + } + break; + case 4: + if(precon) { + /* Unroll independent paths of this predictor. Whether this speeds up + anything depends on compiler and settings. */ + if(bytewidth == 8) { + unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; + unsigned char a4, b4 = 0, c4, d4 = 0, a5, b5 = 0, c5, d5 = 0; + unsigned char a6, b6 = 0, c6, d6 = 0, a7, b7 = 0, c7, d7 = 0; + for(i = 0; i + 7 < length; i += 8) { + c0 = b0; c1 = b1; c2 = b2; c3 = b3; + c4 = b4; c5 = b5; c6 = b6; c7 = b7; + b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; b3 = precon[i + 3]; + b4 = precon[i + 4]; b5 = precon[i + 5]; b6 = precon[i + 6]; b7 = precon[i + 7]; + a0 = d0; a1 = d1; a2 = d2; a3 = d3; + a4 = d4; a5 = d5; a6 = d6; a7 = d7; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); + d4 = scanline[i + 4] + paethPredictor(a4, b4, c4); + d5 = scanline[i + 5] + paethPredictor(a5, b5, c5); + d6 = scanline[i + 6] + paethPredictor(a6, b6, c6); + d7 = scanline[i + 7] + paethPredictor(a7, b7, c7); + recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; recon[i + 3] = d3; + recon[i + 4] = d4; recon[i + 5] = d5; recon[i + 6] = d6; recon[i + 7] = d7; + } + } else if(bytewidth == 6) { + unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; + unsigned char a4, b4 = 0, c4, d4 = 0, a5, b5 = 0, c5, d5 = 0; + for(i = 0; i + 5 < length; i += 6) { + c0 = b0; c1 = b1; c2 = b2; + c3 = b3; c4 = b4; c5 = b5; + b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; + b3 = precon[i + 3]; b4 = precon[i + 4]; b5 = precon[i + 5]; + a0 = d0; a1 = d1; a2 = d2; + a3 = d3; a4 = d4; a5 = d5; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); + d4 = scanline[i + 4] + paethPredictor(a4, b4, c4); + d5 = scanline[i + 5] + paethPredictor(a5, b5, c5); + recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; + recon[i + 3] = d3; recon[i + 4] = d4; recon[i + 5] = d5; + } + } else if(bytewidth == 4) { + unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; + for(i = 0; i + 3 < length; i += 4) { + c0 = b0; c1 = b1; c2 = b2; c3 = b3; + b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; b3 = precon[i + 3]; + a0 = d0; a1 = d1; a2 = d2; a3 = d3; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); + recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; recon[i + 3] = d3; + } + } else if(bytewidth == 3) { + unsigned char a0, b0 = 0, c0, d0 = 0; + unsigned char a1, b1 = 0, c1, d1 = 0; + unsigned char a2, b2 = 0, c2, d2 = 0; + for(i = 0; i + 2 < length; i += 3) { + c0 = b0; c1 = b1; c2 = b2; + b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; + a0 = d0; a1 = d1; a2 = d2; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); + recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; + } + } else if(bytewidth == 2) { + unsigned char a0, b0 = 0, c0, d0 = 0; + unsigned char a1, b1 = 0, c1, d1 = 0; + for(i = 0; i + 1 < length; i += 2) { + c0 = b0; c1 = b1; + b0 = precon[i + 0]; + b1 = precon[i + 1]; + a0 = d0; a1 = d1; + d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); + d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); + recon[i + 0] = d0; + recon[i + 1] = d1; + } + } else if(bytewidth == 1) { + unsigned char a, b = 0, c, d = 0; + for(i = 0; i != length; ++i) { + c = b; + b = precon[i]; + a = d; + d = scanline[i] + paethPredictor(a, b, c); + recon[i] = d; + } + } else { + /* Normally not a possible case, but this would handle it correctly */ + for(i = 0; i != bytewidth; ++i) { + recon[i] = (scanline[i] + precon[i]); /*paethPredictor(0, precon[i], 0) is always precon[i]*/ + } + } + /* finish any remaining bytes */ + for(; i != length; ++i) { + recon[i] = (scanline[i] + paethPredictor(recon[i - bytewidth], precon[i], precon[i - bytewidth])); + } + } else { + size_t j = 0; + for(i = 0; i != bytewidth; ++i) { + recon[i] = scanline[i]; + } + for(i = bytewidth; i != length; ++i, ++j) { + /*paethPredictor(recon[i - bytewidth], 0, 0) is always recon[i - bytewidth]*/ + recon[i] = (scanline[i] + recon[j]); + } + } + break; + default: return 36; /*error: invalid filter type given*/ + } + return 0; +} + +static unsigned unfilter(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, unsigned bpp) { + /* + For PNG filter method 0 + this function unfilters a single image (e.g. without interlacing this is called once, with Adam7 seven times) + out must have enough bytes allocated already, in must have the scanlines + 1 filtertype byte per scanline + w and h are image dimensions or dimensions of reduced image, bpp is bits per pixel + in and out are allowed to be the same memory address (but aren't the same size since in has the extra filter bytes) + */ + + unsigned y; + unsigned char* prevline = 0; + + /*bytewidth is used for filtering, is 1 when bpp < 8, number of bytes per pixel otherwise*/ + size_t bytewidth = (bpp + 7u) / 8u; + /*the width of a scanline in bytes, not including the filter type*/ + size_t linebytes = lodepng_get_raw_size_idat(w, 1, bpp) - 1u; + + for(y = 0; y < h; ++y) { + size_t outindex = linebytes * y; + size_t inindex = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ + unsigned char filterType = in[inindex]; + + CERROR_TRY_RETURN(unfilterScanline(&out[outindex], &in[inindex + 1], prevline, bytewidth, filterType, linebytes)); + + prevline = &out[outindex]; + } + + return 0; +} + +/* +in: Adam7 interlaced image, with no padding bits between scanlines, but between + reduced images so that each reduced image starts at a byte. +out: the same pixels, but re-ordered so that they're now a non-interlaced image with size w*h +bpp: bits per pixel +out has the following size in bits: w * h * bpp. +in is possibly bigger due to padding bits between reduced images. +out must be big enough AND must be 0 everywhere if bpp < 8 in the current implementation +(because that's likely a little bit faster) +NOTE: comments about padding bits are only relevant if bpp < 8 +*/ +static void Adam7_deinterlace(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, unsigned bpp) { + unsigned passw[7], passh[7]; + size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned i; + + Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); + + if(bpp >= 8) { + for(i = 0; i != 7; ++i) { + unsigned x, y, b; + size_t bytewidth = bpp / 8u; + for(y = 0; y < passh[i]; ++y) + for(x = 0; x < passw[i]; ++x) { + size_t pixelinstart = passstart[i] + (y * passw[i] + x) * bytewidth; + size_t pixeloutstart = ((ADAM7_IY[i] + (size_t)y * ADAM7_DY[i]) * (size_t)w + + ADAM7_IX[i] + (size_t)x * ADAM7_DX[i]) * bytewidth; + for(b = 0; b < bytewidth; ++b) { + out[pixeloutstart + b] = in[pixelinstart + b]; + } + } + } + } else /*bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit pointers*/ { + for(i = 0; i != 7; ++i) { + unsigned x, y, b; + unsigned ilinebits = bpp * passw[i]; + unsigned olinebits = bpp * w; + size_t obp, ibp; /*bit pointers (for out and in buffer)*/ + for(y = 0; y < passh[i]; ++y) + for(x = 0; x < passw[i]; ++x) { + ibp = (8 * passstart[i]) + (y * ilinebits + x * bpp); + obp = (ADAM7_IY[i] + (size_t)y * ADAM7_DY[i]) * olinebits + (ADAM7_IX[i] + (size_t)x * ADAM7_DX[i]) * bpp; + for(b = 0; b < bpp; ++b) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + } + } + } +} + +static void removePaddingBits(unsigned char* out, const unsigned char* in, + size_t olinebits, size_t ilinebits, unsigned h) { + /* + After filtering there are still padding bits if scanlines have non multiple of 8 bit amounts. They need + to be removed (except at last scanline of (Adam7-reduced) image) before working with pure image buffers + for the Adam7 code, the color convert code and the output to the user. + in and out are allowed to be the same buffer, in may also be higher but still overlapping; in must + have >= ilinebits*h bits, out must have >= olinebits*h bits, olinebits must be <= ilinebits + also used to move bits after earlier such operations happened, e.g. in a sequence of reduced images from Adam7 + only useful if (ilinebits - olinebits) is a value in the range 1..7 + */ + unsigned y; + size_t diff = ilinebits - olinebits; + size_t ibp = 0, obp = 0; /*input and output bit pointers*/ + for(y = 0; y < h; ++y) { + size_t x; + for(x = 0; x < olinebits; ++x) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + ibp += diff; + } +} + +/*out must be buffer big enough to contain full image, and in must contain the full decompressed data from +the IDAT chunks (with filter index bytes and possible padding bits) +return value is error*/ +static unsigned postProcessScanlines(unsigned char* out, unsigned char* in, + unsigned w, unsigned h, const LodePNGInfo* info_png) { + /* + This function converts the filtered-padded-interlaced data into pure 2D image buffer with the PNG's colortype. + Steps: + *) if no Adam7: 1) unfilter 2) remove padding bits (= possible extra bits per scanline if bpp < 8) + *) if adam7: 1) 7x unfilter 2) 7x remove padding bits 3) Adam7_deinterlace + NOTE: the in buffer will be overwritten with intermediate data! + */ + unsigned bpp = lodepng_get_bpp(&info_png->color); + if(bpp == 0) return 31; /*error: invalid colortype*/ + + if(info_png->interlace_method == 0) { + if(bpp < 8 && w * bpp != ((w * bpp + 7u) / 8u) * 8u) { + CERROR_TRY_RETURN(unfilter(in, in, w, h, bpp)); + removePaddingBits(out, in, w * bpp, ((w * bpp + 7u) / 8u) * 8u, h); + } + /*we can immediately filter into the out buffer, no other steps needed*/ + else CERROR_TRY_RETURN(unfilter(out, in, w, h, bpp)); + } else /*interlace_method is 1 (Adam7)*/ { + unsigned passw[7], passh[7]; size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned i; + + Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); + + for(i = 0; i != 7; ++i) { + CERROR_TRY_RETURN(unfilter(&in[padded_passstart[i]], &in[filter_passstart[i]], passw[i], passh[i], bpp)); + /*TODO: possible efficiency improvement: if in this reduced image the bits fit nicely in 1 scanline, + move bytes instead of bits or move not at all*/ + if(bpp < 8) { + /*remove padding bits in scanlines; after this there still may be padding + bits between the different reduced images: each reduced image still starts nicely at a byte*/ + removePaddingBits(&in[passstart[i]], &in[padded_passstart[i]], passw[i] * bpp, + ((passw[i] * bpp + 7u) / 8u) * 8u, passh[i]); + } + } + + Adam7_deinterlace(out, in, w, h, bpp); + } + + return 0; +} + +static unsigned readChunk_PLTE(LodePNGColorMode* color, const unsigned char* data, size_t chunkLength) { + unsigned pos = 0, i; + color->palettesize = chunkLength / 3u; + if(color->palettesize == 0 || color->palettesize > 256) return 38; /*error: palette too small or big*/ + lodepng_color_mode_alloc_palette(color); + if(!color->palette && color->palettesize) { + color->palettesize = 0; + return 83; /*alloc fail*/ + } + + for(i = 0; i != color->palettesize; ++i) { + color->palette[4 * i + 0] = data[pos++]; /*R*/ + color->palette[4 * i + 1] = data[pos++]; /*G*/ + color->palette[4 * i + 2] = data[pos++]; /*B*/ + color->palette[4 * i + 3] = 255; /*alpha*/ + } + + return 0; /* OK */ +} + +static unsigned readChunk_tRNS(LodePNGColorMode* color, const unsigned char* data, size_t chunkLength) { + unsigned i; + if(color->colortype == LCT_PALETTE) { + /*error: more alpha values given than there are palette entries*/ + if(chunkLength > color->palettesize) return 39; + + for(i = 0; i != chunkLength; ++i) color->palette[4 * i + 3] = data[i]; + } else if(color->colortype == LCT_GREY) { + /*error: this chunk must be 2 bytes for grayscale image*/ + if(chunkLength != 2) return 30; + + color->key_defined = 1; + color->key_r = color->key_g = color->key_b = 256u * data[0] + data[1]; + } else if(color->colortype == LCT_RGB) { + /*error: this chunk must be 6 bytes for RGB image*/ + if(chunkLength != 6) return 41; + + color->key_defined = 1; + color->key_r = 256u * data[0] + data[1]; + color->key_g = 256u * data[2] + data[3]; + color->key_b = 256u * data[4] + data[5]; + } + else return 42; /*error: tRNS chunk not allowed for other color models*/ + + return 0; /* OK */ +} + + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +/*background color chunk (bKGD)*/ +static unsigned readChunk_bKGD(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(info->color.colortype == LCT_PALETTE) { + /*error: this chunk must be 1 byte for indexed color image*/ + if(chunkLength != 1) return 43; + + /*error: invalid palette index, or maybe this chunk appeared before PLTE*/ + if(data[0] >= info->color.palettesize) return 103; + + info->background_defined = 1; + info->background_r = info->background_g = info->background_b = data[0]; + } else if(info->color.colortype == LCT_GREY || info->color.colortype == LCT_GREY_ALPHA) { + /*error: this chunk must be 2 bytes for grayscale image*/ + if(chunkLength != 2) return 44; + + /*the values are truncated to bitdepth in the PNG file*/ + info->background_defined = 1; + info->background_r = info->background_g = info->background_b = 256u * data[0] + data[1]; + } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_RGBA) { + /*error: this chunk must be 6 bytes for grayscale image*/ + if(chunkLength != 6) return 45; + + /*the values are truncated to bitdepth in the PNG file*/ + info->background_defined = 1; + info->background_r = 256u * data[0] + data[1]; + info->background_g = 256u * data[2] + data[3]; + info->background_b = 256u * data[4] + data[5]; + } + + return 0; /* OK */ +} + +/*text chunk (tEXt)*/ +static unsigned readChunk_tEXt(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + unsigned error = 0; + char *key = 0, *str = 0; + + while(!error) /*not really a while loop, only used to break on error*/ { + unsigned length, string2_begin; + + length = 0; + while(length < chunkLength && data[length] != 0) ++length; + /*even though it's not allowed by the standard, no error is thrown if + there's no null termination char, if the text is empty*/ + if(length < 1 || length > 79) CERROR_BREAK(error, 89); /*keyword too short or long*/ + + key = (char*)lodepng_malloc(length + 1); + if(!key) CERROR_BREAK(error, 83); /*alloc fail*/ + + lodepng_memcpy(key, data, length); + key[length] = 0; + + string2_begin = length + 1; /*skip keyword null terminator*/ + + length = (unsigned)(chunkLength < string2_begin ? 0 : chunkLength - string2_begin); + str = (char*)lodepng_malloc(length + 1); + if(!str) CERROR_BREAK(error, 83); /*alloc fail*/ + + lodepng_memcpy(str, data + string2_begin, length); + str[length] = 0; + + error = lodepng_add_text(info, key, str); + + break; + } + + lodepng_free(key); + lodepng_free(str); + + return error; +} + +/*compressed text chunk (zTXt)*/ +static unsigned readChunk_zTXt(LodePNGInfo* info, const LodePNGDecoderSettings* decoder, + const unsigned char* data, size_t chunkLength) { + unsigned error = 0; + + /*copy the object to change parameters in it*/ + LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; + + unsigned length, string2_begin; + char *key = 0; + unsigned char* str = 0; + size_t size = 0; + + while(!error) /*not really a while loop, only used to break on error*/ { + for(length = 0; length < chunkLength && data[length] != 0; ++length) ; + if(length + 2 >= chunkLength) CERROR_BREAK(error, 75); /*no null termination, corrupt?*/ + if(length < 1 || length > 79) CERROR_BREAK(error, 89); /*keyword too short or long*/ + + key = (char*)lodepng_malloc(length + 1); + if(!key) CERROR_BREAK(error, 83); /*alloc fail*/ + + lodepng_memcpy(key, data, length); + key[length] = 0; + + if(data[length + 1] != 0) CERROR_BREAK(error, 72); /*the 0 byte indicating compression must be 0*/ + + string2_begin = length + 2; + if(string2_begin > chunkLength) CERROR_BREAK(error, 75); /*no null termination, corrupt?*/ + + length = (unsigned)chunkLength - string2_begin; + zlibsettings.max_output_size = decoder->max_text_size; + /*will fail if zlib error, e.g. if length is too small*/ + error = zlib_decompress(&str, &size, 0, &data[string2_begin], + length, &zlibsettings); + /*error: compressed text larger than decoder->max_text_size*/ + if(error && size > zlibsettings.max_output_size) error = 112; + if(error) break; + error = lodepng_add_text_sized(info, key, (char*)str, size); + break; + } + + lodepng_free(key); + lodepng_free(str); + + return error; +} + +/*international text chunk (iTXt)*/ +static unsigned readChunk_iTXt(LodePNGInfo* info, const LodePNGDecoderSettings* decoder, + const unsigned char* data, size_t chunkLength) { + unsigned error = 0; + unsigned i; + + /*copy the object to change parameters in it*/ + LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; + + unsigned length, begin, compressed; + char *key = 0, *langtag = 0, *transkey = 0; + + while(!error) /*not really a while loop, only used to break on error*/ { + /*Quick check if the chunk length isn't too small. Even without check + it'd still fail with other error checks below if it's too short. This just gives a different error code.*/ + if(chunkLength < 5) CERROR_BREAK(error, 30); /*iTXt chunk too short*/ + + /*read the key*/ + for(length = 0; length < chunkLength && data[length] != 0; ++length) ; + if(length + 3 >= chunkLength) CERROR_BREAK(error, 75); /*no null termination char, corrupt?*/ + if(length < 1 || length > 79) CERROR_BREAK(error, 89); /*keyword too short or long*/ + + key = (char*)lodepng_malloc(length + 1); + if(!key) CERROR_BREAK(error, 83); /*alloc fail*/ + + lodepng_memcpy(key, data, length); + key[length] = 0; + + /*read the compression method*/ + compressed = data[length + 1]; + if(data[length + 2] != 0) CERROR_BREAK(error, 72); /*the 0 byte indicating compression must be 0*/ + + /*even though it's not allowed by the standard, no error is thrown if + there's no null termination char, if the text is empty for the next 3 texts*/ + + /*read the langtag*/ + begin = length + 3; + length = 0; + for(i = begin; i < chunkLength && data[i] != 0; ++i) ++length; + + langtag = (char*)lodepng_malloc(length + 1); + if(!langtag) CERROR_BREAK(error, 83); /*alloc fail*/ + + lodepng_memcpy(langtag, data + begin, length); + langtag[length] = 0; + + /*read the transkey*/ + begin += length + 1; + length = 0; + for(i = begin; i < chunkLength && data[i] != 0; ++i) ++length; + + transkey = (char*)lodepng_malloc(length + 1); + if(!transkey) CERROR_BREAK(error, 83); /*alloc fail*/ + + lodepng_memcpy(transkey, data + begin, length); + transkey[length] = 0; + + /*read the actual text*/ + begin += length + 1; + + length = (unsigned)chunkLength < begin ? 0 : (unsigned)chunkLength - begin; + + if(compressed) { + unsigned char* str = 0; + size_t size = 0; + zlibsettings.max_output_size = decoder->max_text_size; + /*will fail if zlib error, e.g. if length is too small*/ + error = zlib_decompress(&str, &size, 0, &data[begin], + length, &zlibsettings); + /*error: compressed text larger than decoder->max_text_size*/ + if(error && size > zlibsettings.max_output_size) error = 112; + if(!error) error = lodepng_add_itext_sized(info, key, langtag, transkey, (char*)str, size); + lodepng_free(str); + } else { + error = lodepng_add_itext_sized(info, key, langtag, transkey, (const char*)(data + begin), length); + } + + break; + } + + lodepng_free(key); + lodepng_free(langtag); + lodepng_free(transkey); + + return error; +} + +static unsigned readChunk_tIME(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 7) return 73; /*invalid tIME chunk size*/ + + info->time_defined = 1; + info->time.year = 256u * data[0] + data[1]; + info->time.month = data[2]; + info->time.day = data[3]; + info->time.hour = data[4]; + info->time.minute = data[5]; + info->time.second = data[6]; + + return 0; /* OK */ +} + +static unsigned readChunk_pHYs(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 9) return 74; /*invalid pHYs chunk size*/ + + info->phys_defined = 1; + info->phys_x = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; + info->phys_y = 16777216u * data[4] + 65536u * data[5] + 256u * data[6] + data[7]; + info->phys_unit = data[8]; + + return 0; /* OK */ +} + +static unsigned readChunk_gAMA(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 4) return 96; /*invalid gAMA chunk size*/ + + info->gama_defined = 1; + info->gama_gamma = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; + + return 0; /* OK */ +} + +static unsigned readChunk_cHRM(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 32) return 97; /*invalid cHRM chunk size*/ + + info->chrm_defined = 1; + info->chrm_white_x = 16777216u * data[ 0] + 65536u * data[ 1] + 256u * data[ 2] + data[ 3]; + info->chrm_white_y = 16777216u * data[ 4] + 65536u * data[ 5] + 256u * data[ 6] + data[ 7]; + info->chrm_red_x = 16777216u * data[ 8] + 65536u * data[ 9] + 256u * data[10] + data[11]; + info->chrm_red_y = 16777216u * data[12] + 65536u * data[13] + 256u * data[14] + data[15]; + info->chrm_green_x = 16777216u * data[16] + 65536u * data[17] + 256u * data[18] + data[19]; + info->chrm_green_y = 16777216u * data[20] + 65536u * data[21] + 256u * data[22] + data[23]; + info->chrm_blue_x = 16777216u * data[24] + 65536u * data[25] + 256u * data[26] + data[27]; + info->chrm_blue_y = 16777216u * data[28] + 65536u * data[29] + 256u * data[30] + data[31]; + + return 0; /* OK */ +} + +static unsigned readChunk_sRGB(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 1) return 98; /*invalid sRGB chunk size (this one is never ignored)*/ + + info->srgb_defined = 1; + info->srgb_intent = data[0]; + + return 0; /* OK */ +} + +static unsigned readChunk_iCCP(LodePNGInfo* info, const LodePNGDecoderSettings* decoder, + const unsigned char* data, size_t chunkLength) { + unsigned error = 0; + unsigned i; + size_t size = 0; + /*copy the object to change parameters in it*/ + LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; + + unsigned length, string2_begin; + + if(info->iccp_defined) lodepng_clear_icc(info); + + for(length = 0; length < chunkLength && data[length] != 0; ++length) ; + if(length + 2 >= chunkLength) return 75; /*no null termination, corrupt?*/ + if(length < 1 || length > 79) return 89; /*keyword too short or long*/ + + info->iccp_name = (char*)lodepng_malloc(length + 1); + if(!info->iccp_name) return 83; /*alloc fail*/ + + info->iccp_name[length] = 0; + for(i = 0; i != length; ++i) info->iccp_name[i] = (char)data[i]; + + if(data[length + 1] != 0) return 72; /*the 0 byte indicating compression must be 0*/ + + string2_begin = length + 2; + if(string2_begin > chunkLength) return 75; /*no null termination, corrupt?*/ + + length = (unsigned)chunkLength - string2_begin; + zlibsettings.max_output_size = decoder->max_icc_size; + error = zlib_decompress(&info->iccp_profile, &size, 0, + &data[string2_begin], + length, &zlibsettings); + /*error: ICC profile larger than decoder->max_icc_size*/ + if(error && size > zlibsettings.max_output_size) error = 113; + info->iccp_profile_size = (unsigned)size; + if(!error && !info->iccp_profile_size) error = 123; /*invalid ICC profile size*/ + + if(!error) info->iccp_defined = 1; + return error; +} + +static unsigned readChunk_cICP(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 4) return 117; /*invalid cICP chunk size*/ + + info->cicp_defined = 1; + /* No error checking for value ranges is done here, that is up to a CICP + handling library, not the PNG decoding. Just pass on the metadata. */ + info->cicp_color_primaries = data[0]; + info->cicp_transfer_function = data[1]; + info->cicp_matrix_coefficients = data[2]; + info->cicp_video_full_range_flag = data[3]; + + return 0; /* OK */ +} + +static unsigned readChunk_mDCV(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 24) return 119; /*invalid mDCV chunk size*/ + + info->mdcv_defined = 1; + info->mdcv_red_x = 256u * data[0] + data[1]; + info->mdcv_red_y = 256u * data[2] + data[3]; + info->mdcv_green_x = 256u * data[4] + data[5]; + info->mdcv_green_y = 256u * data[6] + data[7]; + info->mdcv_blue_x = 256u * data[8] + data[9]; + info->mdcv_blue_y = 256u * data[10] + data[11]; + info->mdcv_white_x = 256u * data[12] + data[13]; + info->mdcv_white_y = 256u * data[14] + data[15]; + info->mdcv_max_luminance = 16777216u * data[16] + 65536u * data[17] + 256u * data[18] + data[19]; + info->mdcv_min_luminance = 16777216u * data[20] + 65536u * data[21] + 256u * data[22] + data[23]; + + return 0; /* OK */ +} + +static unsigned readChunk_cLLI(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + if(chunkLength != 8) return 120; /*invalid cLLI chunk size*/ + + info->clli_defined = 1; + info->clli_max_cll = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; + info->clli_max_fall = 16777216u * data[4] + 65536u * data[5] + 256u * data[6] + data[7]; + + return 0; /* OK */ +} + +static unsigned readChunk_eXIf(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + return lodepng_set_exif(info, data, (unsigned)chunkLength); +} + +/*significant bits chunk (sBIT)*/ +static unsigned readChunk_sBIT(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { + unsigned bitdepth = (info->color.colortype == LCT_PALETTE) ? 8 : info->color.bitdepth; + if(info->color.colortype == LCT_GREY) { + /*error: this chunk must be 1 bytes for grayscale image*/ + if(chunkLength != 1) return 114; + if(data[0] == 0 || data[0] > bitdepth) return 115; + info->sbit_defined = 1; + info->sbit_r = info->sbit_g = info->sbit_b = data[0]; /*setting g and b is not required, but sensible*/ + } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_PALETTE) { + /*error: this chunk must be 3 bytes for RGB and palette image*/ + if(chunkLength != 3) return 114; + if(data[0] == 0 || data[1] == 0 || data[2] == 0) return 115; + if(data[0] > bitdepth || data[1] > bitdepth || data[2] > bitdepth) return 115; + info->sbit_defined = 1; + info->sbit_r = data[0]; + info->sbit_g = data[1]; + info->sbit_b = data[2]; + } else if(info->color.colortype == LCT_GREY_ALPHA) { + /*error: this chunk must be 2 byte for grayscale with alpha image*/ + if(chunkLength != 2) return 114; + if(data[0] == 0 || data[1] == 0) return 115; + if(data[0] > bitdepth || data[1] > bitdepth) return 115; + info->sbit_defined = 1; + info->sbit_r = info->sbit_g = info->sbit_b = data[0]; /*setting g and b is not required, but sensible*/ + info->sbit_a = data[1]; + } else if(info->color.colortype == LCT_RGBA) { + /*error: this chunk must be 4 bytes for grayscale image*/ + if(chunkLength != 4) return 114; + if(data[0] == 0 || data[1] == 0 || data[2] == 0 || data[3] == 0) return 115; + if(data[0] > bitdepth || data[1] > bitdepth || data[2] > bitdepth || data[3] > bitdepth) return 115; + info->sbit_defined = 1; + info->sbit_r = data[0]; + info->sbit_g = data[1]; + info->sbit_b = data[2]; + info->sbit_a = data[3]; + } + + return 0; /* OK */ +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +unsigned lodepng_inspect_chunk(LodePNGState* state, size_t pos, + const unsigned char* in, size_t insize) { + const unsigned char* chunk = in + pos; + unsigned chunkLength; + const unsigned char* data; + unsigned unhandled = 0; + unsigned error = 0; + + if(pos + 4 > insize) return 30; + chunkLength = lodepng_chunk_length(chunk); + if(chunkLength > 2147483647) return 63; + data = lodepng_chunk_data_const(chunk); + if(chunkLength + 12 > insize - pos) return 30; + + if(lodepng_chunk_type_equals(chunk, "PLTE")) { + error = readChunk_PLTE(&state->info_png.color, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "tRNS")) { + error = readChunk_tRNS(&state->info_png.color, data, chunkLength); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + } else if(lodepng_chunk_type_equals(chunk, "bKGD")) { + error = readChunk_bKGD(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "tEXt")) { + error = readChunk_tEXt(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "zTXt")) { + error = readChunk_zTXt(&state->info_png, &state->decoder, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "iTXt")) { + error = readChunk_iTXt(&state->info_png, &state->decoder, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "tIME")) { + error = readChunk_tIME(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "pHYs")) { + error = readChunk_pHYs(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "gAMA")) { + error = readChunk_gAMA(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "cHRM")) { + error = readChunk_cHRM(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "sRGB")) { + error = readChunk_sRGB(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "iCCP")) { + error = readChunk_iCCP(&state->info_png, &state->decoder, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "cICP")) { + error = readChunk_cICP(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "mDCV")) { + error = readChunk_mDCV(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "cLLI")) { + error = readChunk_cLLI(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "eXIf")) { + error = readChunk_eXIf(&state->info_png, data, chunkLength); + } else if(lodepng_chunk_type_equals(chunk, "sBIT")) { + error = readChunk_sBIT(&state->info_png, data, chunkLength); +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else { + /* unhandled chunk is ok (is not an error) */ + unhandled = 1; + } + + if(!error && !unhandled && !state->decoder.ignore_crc) { + if(lodepng_chunk_check_crc(chunk)) return 57; /*invalid CRC*/ + } + + return error; +} + +/*read a PNG, the result will be in the same color type as the PNG (hence "generic")*/ +static void decodeGeneric(unsigned char** out, unsigned* w, unsigned* h, + LodePNGState* state, + const unsigned char* in, size_t insize) { + unsigned char IEND = 0; + const unsigned char* chunk; /*points to beginning of next chunk*/ + unsigned char* idat; /*the data from idat chunks, zlib compressed*/ + size_t idatsize = 0; + unsigned char* scanlines = 0; + size_t scanlines_size = 0, expected_size = 0; + size_t outsize = 0; + + /*for unknown chunk order*/ + unsigned unknown = 0; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + unsigned critical_pos = 1; /*1 = after IHDR, 2 = after PLTE, 3 = after IDAT*/ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + + + /* safe output values in case error happens */ + *out = 0; + *w = *h = 0; + + state->error = lodepng_inspect(w, h, state, in, insize); /*reads header and resets other parameters in state->info_png*/ + if(state->error) return; + + if(lodepng_pixel_overflow(*w, *h, &state->info_png.color, &state->info_raw)) { + CERROR_RETURN(state->error, 92); /*overflow possible due to amount of pixels*/ + } + + /*the input filesize is a safe upper bound for the sum of idat chunks size*/ + idat = (unsigned char*)lodepng_malloc(insize); + if(!idat) CERROR_RETURN(state->error, 83); /*alloc fail*/ + + chunk = &in[33]; /*first byte of the first chunk after the header*/ + + /*loop through the chunks, ignoring unknown chunks and stopping at IEND chunk. + IDAT data is put at the start of the in buffer*/ + while(!IEND && !state->error) { + unsigned chunkLength; + const unsigned char* data; /*the data in the chunk*/ + size_t pos = (size_t)(chunk - in); + + /*error: next chunk out of bounds of the in buffer*/ + if(chunk < in || pos + 12 > insize) { + if(state->decoder.ignore_end) break; /*other errors may still happen though*/ + CERROR_BREAK(state->error, 30); + } + + /*length of the data of the chunk, excluding the 12 bytes for length, chunk type and CRC*/ + chunkLength = lodepng_chunk_length(chunk); + /*error: chunk length larger than the max PNG chunk size*/ + if(chunkLength > 2147483647) { + if(state->decoder.ignore_end) break; /*other errors may still happen though*/ + CERROR_BREAK(state->error, 63); + } + + if(pos + (size_t)chunkLength + 12 > insize || pos + (size_t)chunkLength + 12 < pos) { + CERROR_BREAK(state->error, 64); /*error: size of the in buffer too small to contain next chunk (or int overflow)*/ + } + + data = lodepng_chunk_data_const(chunk); + + unknown = 0; + + /*IDAT chunk, containing compressed image data*/ + if(lodepng_chunk_type_equals(chunk, "IDAT")) { + size_t newsize; + if(lodepng_addofl(idatsize, chunkLength, &newsize)) CERROR_BREAK(state->error, 95); + if(newsize > insize) CERROR_BREAK(state->error, 95); + lodepng_memcpy(idat + idatsize, data, chunkLength); + idatsize += chunkLength; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + critical_pos = 3; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else if(lodepng_chunk_type_equals(chunk, "IEND")) { + /*IEND chunk*/ + IEND = 1; + } else if(lodepng_chunk_type_equals(chunk, "PLTE")) { + /*palette chunk (PLTE)*/ + state->error = readChunk_PLTE(&state->info_png.color, data, chunkLength); + if(state->error) break; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + critical_pos = 2; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else if(lodepng_chunk_type_equals(chunk, "tRNS")) { + /*palette transparency chunk (tRNS). Even though this one is an ancillary chunk , it is still compiled + in without 'LODEPNG_COMPILE_ANCILLARY_CHUNKS' because it contains essential color information that + affects the alpha channel of pixels. */ + state->error = readChunk_tRNS(&state->info_png.color, data, chunkLength); + if(state->error) break; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*background color chunk (bKGD)*/ + } else if(lodepng_chunk_type_equals(chunk, "bKGD")) { + state->error = readChunk_bKGD(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "tEXt")) { + /*text chunk (tEXt)*/ + if(state->decoder.read_text_chunks) { + state->error = readChunk_tEXt(&state->info_png, data, chunkLength); + if(state->error) break; + } + } else if(lodepng_chunk_type_equals(chunk, "zTXt")) { + /*compressed text chunk (zTXt)*/ + if(state->decoder.read_text_chunks) { + state->error = readChunk_zTXt(&state->info_png, &state->decoder, data, chunkLength); + if(state->error) break; + } + } else if(lodepng_chunk_type_equals(chunk, "iTXt")) { + /*international text chunk (iTXt)*/ + if(state->decoder.read_text_chunks) { + state->error = readChunk_iTXt(&state->info_png, &state->decoder, data, chunkLength); + if(state->error) break; + } + } else if(lodepng_chunk_type_equals(chunk, "tIME")) { + state->error = readChunk_tIME(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "pHYs")) { + state->error = readChunk_pHYs(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "gAMA")) { + state->error = readChunk_gAMA(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "cHRM")) { + state->error = readChunk_cHRM(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "sRGB")) { + state->error = readChunk_sRGB(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "iCCP")) { + state->error = readChunk_iCCP(&state->info_png, &state->decoder, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "cICP")) { + state->error = readChunk_cICP(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "mDCV")) { + state->error = readChunk_mDCV(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "cLLI")) { + state->error = readChunk_cLLI(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "eXIf")) { + state->error = readChunk_eXIf(&state->info_png, data, chunkLength); + if(state->error) break; + } else if(lodepng_chunk_type_equals(chunk, "sBIT")) { + state->error = readChunk_sBIT(&state->info_png, data, chunkLength); + if(state->error) break; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } else /*it's not an implemented chunk type, so ignore it: skip over the data*/ { + if(!lodepng_chunk_type_name_valid(chunk)) { + CERROR_BREAK(state->error, 121); /* invalid chunk type name */ + } + if(lodepng_chunk_reserved(chunk)) { + CERROR_BREAK(state->error, 122); /* invalid third lowercase character */ + } + + /*error: unknown critical chunk (5th bit of first byte of chunk type is 0)*/ + if(!state->decoder.ignore_critical && !lodepng_chunk_ancillary(chunk)) { + CERROR_BREAK(state->error, 69); + } + + unknown = 1; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if(state->decoder.remember_unknown_chunks) { + state->error = lodepng_chunk_append(&state->info_png.unknown_chunks_data[critical_pos - 1], + &state->info_png.unknown_chunks_size[critical_pos - 1], chunk); + if(state->error) break; + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + } + + if(!state->decoder.ignore_crc && !unknown) /*check CRC if wanted, only on known chunk types*/ { + if(lodepng_chunk_check_crc(chunk)) CERROR_BREAK(state->error, 57); /*invalid CRC*/ + } + + if(!IEND) chunk = lodepng_chunk_next_const(chunk, in + insize); + } + + if(!state->error && state->info_png.color.colortype == LCT_PALETTE && !state->info_png.color.palette) { + state->error = 106; /* error: PNG file must have PLTE chunk if color type is palette */ + } + + if(!state->error) { + /*predict output size, to allocate exact size for output buffer to avoid more dynamic allocation. + If the decompressed size does not match the prediction, the image must be corrupt.*/ + if(state->info_png.interlace_method == 0) { + unsigned bpp = lodepng_get_bpp(&state->info_png.color); + expected_size = lodepng_get_raw_size_idat(*w, *h, bpp); + } else { + unsigned bpp = lodepng_get_bpp(&state->info_png.color); + /*Adam-7 interlaced: expected size is the sum of the 7 sub-images sizes*/ + expected_size = 0; + expected_size += lodepng_get_raw_size_idat((*w + 7) >> 3, (*h + 7) >> 3, bpp); + if(*w > 4) expected_size += lodepng_get_raw_size_idat((*w + 3) >> 3, (*h + 7) >> 3, bpp); + expected_size += lodepng_get_raw_size_idat((*w + 3) >> 2, (*h + 3) >> 3, bpp); + if(*w > 2) expected_size += lodepng_get_raw_size_idat((*w + 1) >> 2, (*h + 3) >> 2, bpp); + expected_size += lodepng_get_raw_size_idat((*w + 1) >> 1, (*h + 1) >> 2, bpp); + if(*w > 1) expected_size += lodepng_get_raw_size_idat((*w + 0) >> 1, (*h + 1) >> 1, bpp); + expected_size += lodepng_get_raw_size_idat((*w + 0), (*h + 0) >> 1, bpp); + } + + state->error = zlib_decompress(&scanlines, &scanlines_size, expected_size, idat, idatsize, &state->decoder.zlibsettings); + } + if(!state->error && scanlines_size != expected_size) state->error = 91; /*decompressed size doesn't match prediction*/ + lodepng_free(idat); + + if(!state->error) { + outsize = lodepng_get_raw_size(*w, *h, &state->info_png.color); + *out = (unsigned char*)lodepng_malloc(outsize); + if(!*out) state->error = 83; /*alloc fail*/ + } + if(!state->error) { + lodepng_memset(*out, 0, outsize); + state->error = postProcessScanlines(*out, scanlines, *w, *h, &state->info_png); + } + lodepng_free(scanlines); +} + +unsigned lodepng_decode(unsigned char** out, unsigned* w, unsigned* h, + LodePNGState* state, + const unsigned char* in, size_t insize) { + *out = 0; + decodeGeneric(out, w, h, state, in, insize); + if(state->error) return state->error; + if(!state->decoder.color_convert || lodepng_color_mode_equal(&state->info_raw, &state->info_png.color)) { + /*same color type, no copying or converting of data needed*/ + /*store the info_png color settings on the info_raw so that the info_raw still reflects what colortype + the raw image has to the end user*/ + if(!state->decoder.color_convert) { + state->error = lodepng_color_mode_copy(&state->info_raw, &state->info_png.color); + if(state->error) return state->error; + } + } else { /*color conversion needed*/ + unsigned char* data = *out; + size_t outsize; + + /*TODO: check if this works according to the statement in the documentation: "The converter can convert + from grayscale input color type, to 8-bit grayscale or grayscale with alpha"*/ + if(!(state->info_raw.colortype == LCT_RGB || state->info_raw.colortype == LCT_RGBA) + && !(state->info_raw.bitdepth == 8)) { + return 56; /*unsupported color mode conversion*/ + } + + outsize = lodepng_get_raw_size(*w, *h, &state->info_raw); + *out = (unsigned char*)lodepng_malloc(outsize); + if(!(*out)) { + state->error = 83; /*alloc fail*/ + } + else state->error = lodepng_convert(*out, data, &state->info_raw, + &state->info_png.color, *w, *h); + lodepng_free(data); + } + return state->error; +} + +unsigned lodepng_decode_memory(unsigned char** out, unsigned* w, unsigned* h, const unsigned char* in, + size_t insize, LodePNGColorType colortype, unsigned bitdepth) { + unsigned error; + LodePNGState state; + lodepng_state_init(&state); + state.info_raw.colortype = colortype; + state.info_raw.bitdepth = bitdepth; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*disable reading things that this function doesn't output*/ + state.decoder.read_text_chunks = 0; + state.decoder.remember_unknown_chunks = 0; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + error = lodepng_decode(out, w, h, &state, in, insize); + lodepng_state_cleanup(&state); + return error; +} + +unsigned lodepng_decode32(unsigned char** out, unsigned* w, unsigned* h, const unsigned char* in, size_t insize) { + return lodepng_decode_memory(out, w, h, in, insize, LCT_RGBA, 8); +} + +unsigned lodepng_decode24(unsigned char** out, unsigned* w, unsigned* h, const unsigned char* in, size_t insize) { + return lodepng_decode_memory(out, w, h, in, insize, LCT_RGB, 8); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned lodepng_decode_file(unsigned char** out, unsigned* w, unsigned* h, const char* filename, + LodePNGColorType colortype, unsigned bitdepth) { + unsigned char* buffer = 0; + size_t buffersize; + unsigned error; + /* safe output values in case error happens */ + *out = 0; + *w = *h = 0; + error = lodepng_load_file(&buffer, &buffersize, filename); + if(!error) error = lodepng_decode_memory(out, w, h, buffer, buffersize, colortype, bitdepth); + lodepng_free(buffer); + return error; +} + +unsigned lodepng_decode32_file(unsigned char** out, unsigned* w, unsigned* h, const char* filename) { + return lodepng_decode_file(out, w, h, filename, LCT_RGBA, 8); +} + +unsigned lodepng_decode24_file(unsigned char** out, unsigned* w, unsigned* h, const char* filename) { + return lodepng_decode_file(out, w, h, filename, LCT_RGB, 8); +} +#endif /*LODEPNG_COMPILE_DISK*/ + +void lodepng_decoder_settings_init(LodePNGDecoderSettings* settings) { + settings->color_convert = 1; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + settings->read_text_chunks = 1; + settings->remember_unknown_chunks = 0; + settings->max_text_size = 16777216; + settings->max_icc_size = 16777216; /* 16MB is much more than enough for any reasonable ICC profile */ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + settings->ignore_crc = 0; + settings->ignore_critical = 0; + settings->ignore_end = 0; + lodepng_decompress_settings_init(&settings->zlibsettings); +} + +#endif /*LODEPNG_COMPILE_DECODER*/ + +#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) + +void lodepng_state_init(LodePNGState* state) { +#ifdef LODEPNG_COMPILE_DECODER + lodepng_decoder_settings_init(&state->decoder); +#endif /*LODEPNG_COMPILE_DECODER*/ +#ifdef LODEPNG_COMPILE_ENCODER + lodepng_encoder_settings_init(&state->encoder); +#endif /*LODEPNG_COMPILE_ENCODER*/ + lodepng_color_mode_init(&state->info_raw); + lodepng_info_init(&state->info_png); + state->error = 1; +} + +void lodepng_state_cleanup(LodePNGState* state) { + lodepng_color_mode_cleanup(&state->info_raw); + lodepng_info_cleanup(&state->info_png); +} + +unsigned lodepng_state_copy(LodePNGState* dest, const LodePNGState* source) { + lodepng_state_cleanup(dest); + *dest = *source; + lodepng_color_mode_init(&dest->info_raw); + lodepng_info_init(&dest->info_png); + dest->error = lodepng_color_mode_copy(&dest->info_raw, &source->info_raw); + if(dest->error) return dest->error; + dest->error = lodepng_info_copy(&dest->info_png, &source->info_png); + return dest->error; +} + +#endif /* defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) */ + +#ifdef LODEPNG_COMPILE_ENCODER + +/* ////////////////////////////////////////////////////////////////////////// */ +/* / PNG Encoder / */ +/* ////////////////////////////////////////////////////////////////////////// */ + + +static unsigned writeSignature(ucvector* out) { + size_t pos = out->size; + const unsigned char signature[] = {137, 80, 78, 71, 13, 10, 26, 10}; + /*8 bytes PNG signature, aka the magic bytes*/ + if(!ucvector_resize(out, out->size + 8)) return 83; /*alloc fail*/ + lodepng_memcpy(out->data + pos, signature, 8); + return 0; +} + +static unsigned addChunk_IHDR(ucvector* out, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth, unsigned interlace_method) { + unsigned char *chunk, *data; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 13, "IHDR")); + data = chunk + 8; + + lodepng_set32bitInt(data + 0, w); /*width*/ + lodepng_set32bitInt(data + 4, h); /*height*/ + data[8] = (unsigned char)bitdepth; /*bit depth*/ + data[9] = (unsigned char)colortype; /*color type*/ + data[10] = 0; /*compression method*/ + data[11] = 0; /*filter method*/ + data[12] = interlace_method; /*interlace method*/ + + lodepng_chunk_generate_crc(chunk); + return 0; +} + +/* only adds the chunk if needed (there is a key or palette with alpha) */ +static unsigned addChunk_PLTE(ucvector* out, const LodePNGColorMode* info) { + unsigned char* chunk; + size_t i, j = 8; + + if(info->palettesize == 0 || info->palettesize > 256) { + return 68; /*invalid palette size, it is only allowed to be 1-256*/ + } + + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, info->palettesize * 3, "PLTE")); + + for(i = 0; i != info->palettesize; ++i) { + /*add all channels except alpha channel*/ + chunk[j++] = info->palette[i * 4 + 0]; + chunk[j++] = info->palette[i * 4 + 1]; + chunk[j++] = info->palette[i * 4 + 2]; + } + + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_tRNS(ucvector* out, const LodePNGColorMode* info) { + unsigned char* chunk = 0; + + if(info->colortype == LCT_PALETTE) { + size_t i, amount = info->palettesize; + /*the tail of palette values that all have 255 as alpha, does not have to be encoded*/ + for(i = info->palettesize; i != 0; --i) { + if(info->palette[4 * (i - 1) + 3] != 255) break; + --amount; + } + if(amount) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, amount, "tRNS")); + /*add the alpha channel values from the palette*/ + for(i = 0; i != amount; ++i) chunk[8 + i] = info->palette[4 * i + 3]; + } + } else if(info->colortype == LCT_GREY) { + if(info->key_defined) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "tRNS")); + chunk[8] = (unsigned char)(info->key_r >> 8); + chunk[9] = (unsigned char)(info->key_r & 255); + } + } else if(info->colortype == LCT_RGB) { + if(info->key_defined) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 6, "tRNS")); + chunk[8] = (unsigned char)(info->key_r >> 8); + chunk[9] = (unsigned char)(info->key_r & 255); + chunk[10] = (unsigned char)(info->key_g >> 8); + chunk[11] = (unsigned char)(info->key_g & 255); + chunk[12] = (unsigned char)(info->key_b >> 8); + chunk[13] = (unsigned char)(info->key_b & 255); + } + } + + if(chunk) lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_IDAT(ucvector* out, const unsigned char* data, size_t datasize, + const LodePNGCompressSettings* zlibsettings) { + unsigned error = 0; + unsigned char* zlib = 0; + size_t pos = 0; + size_t zlibsize = 0; + /* max chunk length allowed by the specification is 2147483647 bytes */ + const size_t max_chunk_length = 2147483647u; + + error = zlib_compress(&zlib, &zlibsize, data, datasize, zlibsettings); + while(!error) { + if(zlibsize - pos > max_chunk_length) { + error = lodepng_chunk_createv(out, max_chunk_length, "IDAT", zlib + pos); + pos += max_chunk_length; + } else { + error = lodepng_chunk_createv(out, zlibsize - pos, "IDAT", zlib + pos); + break; + } + } + lodepng_free(zlib); + return error; +} + +static unsigned addChunk_IEND(ucvector* out) { + return lodepng_chunk_createv(out, 0, "IEND", 0); +} + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + +static unsigned addChunk_tEXt(ucvector* out, const char* keyword, const char* textstring) { + unsigned char* chunk = 0; + size_t keysize = lodepng_strlen(keyword), textsize = lodepng_strlen(textstring); + size_t size = keysize + 1 + textsize; + if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, size, "tEXt")); + lodepng_memcpy(chunk + 8, keyword, keysize); + chunk[8 + keysize] = 0; /*null termination char*/ + lodepng_memcpy(chunk + 9 + keysize, textstring, textsize); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_zTXt(ucvector* out, const char* keyword, const char* textstring, + const LodePNGCompressSettings* zlibsettings) { + unsigned error = 0; + unsigned char* chunk = 0; + unsigned char* compressed = 0; + size_t compressedsize = 0; + size_t textsize = lodepng_strlen(textstring); + size_t keysize = lodepng_strlen(keyword); + if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ + + error = zlib_compress(&compressed, &compressedsize, + (const unsigned char*)textstring, textsize, zlibsettings); + if(!error) { + size_t size = keysize + 2 + compressedsize; + error = lodepng_chunk_init(&chunk, out, size, "zTXt"); + } + if(!error) { + lodepng_memcpy(chunk + 8, keyword, keysize); + chunk[8 + keysize] = 0; /*null termination char*/ + chunk[9 + keysize] = 0; /*compression method: 0*/ + lodepng_memcpy(chunk + 10 + keysize, compressed, compressedsize); + lodepng_chunk_generate_crc(chunk); + } + + lodepng_free(compressed); + return error; +} + +static unsigned addChunk_iTXt(ucvector* out, unsigned compress, const char* keyword, const char* langtag, + const char* transkey, const char* textstring, const LodePNGCompressSettings* zlibsettings) { + unsigned error = 0; + unsigned char* chunk = 0; + unsigned char* compressed = 0; + size_t compressedsize = 0; + size_t textsize = lodepng_strlen(textstring); + size_t keysize = lodepng_strlen(keyword), langsize = lodepng_strlen(langtag), transsize = lodepng_strlen(transkey); + + if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ + + if(compress) { + error = zlib_compress(&compressed, &compressedsize, + (const unsigned char*)textstring, textsize, zlibsettings); + } + if(!error) { + size_t size = keysize + 3 + langsize + 1 + transsize + 1 + (compress ? compressedsize : textsize); + error = lodepng_chunk_init(&chunk, out, size, "iTXt"); + } + if(!error) { + size_t pos = 8; + lodepng_memcpy(chunk + pos, keyword, keysize); + pos += keysize; + chunk[pos++] = 0; /*null termination char*/ + chunk[pos++] = (compress ? 1 : 0); /*compression flag*/ + chunk[pos++] = 0; /*compression method: 0*/ + lodepng_memcpy(chunk + pos, langtag, langsize); + pos += langsize; + chunk[pos++] = 0; /*null termination char*/ + lodepng_memcpy(chunk + pos, transkey, transsize); + pos += transsize; + chunk[pos++] = 0; /*null termination char*/ + if(compress) { + lodepng_memcpy(chunk + pos, compressed, compressedsize); + } else { + lodepng_memcpy(chunk + pos, textstring, textsize); + } + lodepng_chunk_generate_crc(chunk); + } + + lodepng_free(compressed); + return error; +} + +static unsigned addChunk_bKGD(ucvector* out, const LodePNGInfo* info) { + unsigned char* chunk = 0; + if(info->color.colortype == LCT_GREY || info->color.colortype == LCT_GREY_ALPHA) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "bKGD")); + chunk[8] = (unsigned char)(info->background_r >> 8); + chunk[9] = (unsigned char)(info->background_r & 255); + } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_RGBA) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 6, "bKGD")); + chunk[8] = (unsigned char)(info->background_r >> 8); + chunk[9] = (unsigned char)(info->background_r & 255); + chunk[10] = (unsigned char)(info->background_g >> 8); + chunk[11] = (unsigned char)(info->background_g & 255); + chunk[12] = (unsigned char)(info->background_b >> 8); + chunk[13] = (unsigned char)(info->background_b & 255); + } else if(info->color.colortype == LCT_PALETTE) { + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 1, "bKGD")); + chunk[8] = (unsigned char)(info->background_r & 255); /*palette index*/ + } + if(chunk) lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_tIME(ucvector* out, const LodePNGTime* time) { + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 7, "tIME")); + chunk[8] = (unsigned char)(time->year >> 8); + chunk[9] = (unsigned char)(time->year & 255); + chunk[10] = (unsigned char)time->month; + chunk[11] = (unsigned char)time->day; + chunk[12] = (unsigned char)time->hour; + chunk[13] = (unsigned char)time->minute; + chunk[14] = (unsigned char)time->second; + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_pHYs(ucvector* out, const LodePNGInfo* info) { + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 9, "pHYs")); + lodepng_set32bitInt(chunk + 8, info->phys_x); + lodepng_set32bitInt(chunk + 12, info->phys_y); + chunk[16] = info->phys_unit; + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_gAMA(ucvector* out, const LodePNGInfo* info) { + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "gAMA")); + lodepng_set32bitInt(chunk + 8, info->gama_gamma); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_cHRM(ucvector* out, const LodePNGInfo* info) { + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 32, "cHRM")); + lodepng_set32bitInt(chunk + 8, info->chrm_white_x); + lodepng_set32bitInt(chunk + 12, info->chrm_white_y); + lodepng_set32bitInt(chunk + 16, info->chrm_red_x); + lodepng_set32bitInt(chunk + 20, info->chrm_red_y); + lodepng_set32bitInt(chunk + 24, info->chrm_green_x); + lodepng_set32bitInt(chunk + 28, info->chrm_green_y); + lodepng_set32bitInt(chunk + 32, info->chrm_blue_x); + lodepng_set32bitInt(chunk + 36, info->chrm_blue_y); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_sRGB(ucvector* out, const LodePNGInfo* info) { + unsigned char data = info->srgb_intent; + return lodepng_chunk_createv(out, 1, "sRGB", &data); +} + +static unsigned addChunk_iCCP(ucvector* out, const LodePNGInfo* info, const LodePNGCompressSettings* zlibsettings) { + unsigned error = 0; + unsigned char* chunk = 0; + unsigned char* compressed = 0; + size_t compressedsize = 0; + size_t keysize = lodepng_strlen(info->iccp_name); + + if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ + error = zlib_compress(&compressed, &compressedsize, + info->iccp_profile, info->iccp_profile_size, zlibsettings); + if(!error) { + size_t size = keysize + 2 + compressedsize; + error = lodepng_chunk_init(&chunk, out, size, "iCCP"); + } + if(!error) { + lodepng_memcpy(chunk + 8, info->iccp_name, keysize); + chunk[8 + keysize] = 0; /*null termination char*/ + chunk[9 + keysize] = 0; /*compression method: 0*/ + lodepng_memcpy(chunk + 10 + keysize, compressed, compressedsize); + lodepng_chunk_generate_crc(chunk); + } + + lodepng_free(compressed); + return error; +} + +static unsigned addChunk_cICP(ucvector* out, const LodePNGInfo* info) { + unsigned char* chunk; + /* Allow up to 255 since they are bytes. The ITU-R-BT.709 spec has a more + restricted set of valid values for each field, but that's up to the error + handling of a CICP library, not the PNG encoding/decoding, to manage. */ + if(info->cicp_color_primaries > 255) return 116; + if(info->cicp_transfer_function > 255) return 116; + if(info->cicp_matrix_coefficients > 255) return 116; + if(info->cicp_video_full_range_flag > 255) return 116; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "cICP")); + chunk[8 + 0] = (unsigned char)info->cicp_color_primaries; + chunk[8 + 1] = (unsigned char)info->cicp_transfer_function; + chunk[8 + 2] = (unsigned char)info->cicp_matrix_coefficients; + chunk[8 + 3] = (unsigned char)info->cicp_video_full_range_flag; + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_mDCV(ucvector* out, const LodePNGInfo* info) { + unsigned char* chunk; + /* Allow up to 65535 since they are 16-bit ints. */ + if(info->mdcv_red_x > 65535) return 118; + if(info->mdcv_red_y > 65535) return 118; + if(info->mdcv_green_x > 65535) return 118; + if(info->mdcv_green_y > 65535) return 118; + if(info->mdcv_blue_x > 65535) return 118; + if(info->mdcv_blue_y > 65535) return 118; + if(info->mdcv_white_x > 65535) return 118; + if(info->mdcv_white_y > 65535) return 118; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 24, "mDCV")); + chunk[8 + 0] = (unsigned char)((info->mdcv_red_x) >> 8u); + chunk[8 + 1] = (unsigned char)(info->mdcv_red_x); + chunk[8 + 2] = (unsigned char)((info->mdcv_red_y) >> 8u); + chunk[8 + 3] = (unsigned char)(info->mdcv_red_y); + chunk[8 + 4] = (unsigned char)((info->mdcv_green_x) >> 8u); + chunk[8 + 5] = (unsigned char)(info->mdcv_green_x); + chunk[8 + 6] = (unsigned char)((info->mdcv_green_y) >> 8u); + chunk[8 + 7] = (unsigned char)(info->mdcv_green_y); + chunk[8 + 8] = (unsigned char)((info->mdcv_blue_x) >> 8u); + chunk[8 + 9] = (unsigned char)(info->mdcv_blue_x); + chunk[8 + 10] = (unsigned char)((info->mdcv_blue_y) >> 8u); + chunk[8 + 11] = (unsigned char)(info->mdcv_blue_y); + chunk[8 + 12] = (unsigned char)((info->mdcv_white_x) >> 8u); + chunk[8 + 13] = (unsigned char)(info->mdcv_white_x); + chunk[8 + 14] = (unsigned char)((info->mdcv_white_y) >> 8u); + chunk[8 + 15] = (unsigned char)(info->mdcv_white_y); + lodepng_set32bitInt(chunk + 8 + 16, info->mdcv_max_luminance); + lodepng_set32bitInt(chunk + 8 + 20, info->mdcv_min_luminance); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_cLLI(ucvector* out, const LodePNGInfo* info) { + unsigned char* chunk; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 8, "cLLI")); + lodepng_set32bitInt(chunk + 8 + 0, info->clli_max_cll); + lodepng_set32bitInt(chunk + 8 + 4, info->clli_max_fall); + lodepng_chunk_generate_crc(chunk); + return 0; +} + +static unsigned addChunk_eXIf(ucvector* out, const LodePNGInfo* info) { + return lodepng_chunk_createv(out, info->exif_size, "eXIf", info->exif); +} + +static unsigned addChunk_sBIT(ucvector* out, const LodePNGInfo* info) { + unsigned bitdepth = (info->color.colortype == LCT_PALETTE) ? 8 : info->color.bitdepth; + unsigned char* chunk = 0; + if(info->color.colortype == LCT_GREY) { + if(info->sbit_r == 0 || info->sbit_r > bitdepth) return 115; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 1, "sBIT")); + chunk[8] = info->sbit_r; + } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_PALETTE) { + if(info->sbit_r == 0 || info->sbit_g == 0 || info->sbit_b == 0) return 115; + if(info->sbit_r > bitdepth || info->sbit_g > bitdepth || info->sbit_b > bitdepth) return 115; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 3, "sBIT")); + chunk[8] = info->sbit_r; + chunk[9] = info->sbit_g; + chunk[10] = info->sbit_b; + } else if(info->color.colortype == LCT_GREY_ALPHA) { + if(info->sbit_r == 0 || info->sbit_a == 0) return 115; + if(info->sbit_r > bitdepth || info->sbit_a > bitdepth) return 115; + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "sBIT")); + chunk[8] = info->sbit_r; + chunk[9] = info->sbit_a; + } else if(info->color.colortype == LCT_RGBA) { + if(info->sbit_r == 0 || info->sbit_g == 0 || info->sbit_b == 0 || info->sbit_a == 0 || + info->sbit_r > bitdepth || info->sbit_g > bitdepth || + info->sbit_b > bitdepth || info->sbit_a > bitdepth) { + return 115; + } + CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "sBIT")); + chunk[8] = info->sbit_r; + chunk[9] = info->sbit_g; + chunk[10] = info->sbit_b; + chunk[11] = info->sbit_a; + } + if(chunk) lodepng_chunk_generate_crc(chunk); + return 0; +} + +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +static void filterScanline(unsigned char* out, const unsigned char* scanline, const unsigned char* prevline, + size_t length, size_t bytewidth, unsigned char filterType) { + size_t i; + switch(filterType) { + case 0: /*None*/ + for(i = 0; i != length; ++i) out[i] = scanline[i]; + break; + case 1: /*Sub*/ + for(i = 0; i != bytewidth; ++i) out[i] = scanline[i]; + for(i = bytewidth; i < length; ++i) out[i] = scanline[i] - scanline[i - bytewidth]; + break; + case 2: /*Up*/ + if(prevline) { + for(i = 0; i != length; ++i) out[i] = scanline[i] - prevline[i]; + } else { + for(i = 0; i != length; ++i) out[i] = scanline[i]; + } + break; + case 3: /*Average*/ + if(prevline) { + for(i = 0; i != bytewidth; ++i) out[i] = scanline[i] - (prevline[i] >> 1); + for(i = bytewidth; i < length; ++i) out[i] = scanline[i] - ((scanline[i - bytewidth] + prevline[i]) >> 1); + } else { + for(i = 0; i != bytewidth; ++i) out[i] = scanline[i]; + for(i = bytewidth; i < length; ++i) out[i] = scanline[i] - (scanline[i - bytewidth] >> 1); + } + break; + case 4: /*Paeth*/ + if(prevline) { + /*paethPredictor(0, prevline[i], 0) is always prevline[i]*/ + for(i = 0; i != bytewidth; ++i) out[i] = (scanline[i] - prevline[i]); + for(i = bytewidth; i < length; ++i) { + out[i] = (scanline[i] - paethPredictor(scanline[i - bytewidth], prevline[i], prevline[i - bytewidth])); + } + } else { + for(i = 0; i != bytewidth; ++i) out[i] = scanline[i]; + /*paethPredictor(scanline[i - bytewidth], 0, 0) is always scanline[i - bytewidth]*/ + for(i = bytewidth; i < length; ++i) out[i] = (scanline[i] - scanline[i - bytewidth]); + } + break; + default: return; /*invalid filter type given*/ + } +} + +/* integer binary logarithm, max return value is 31 */ +static size_t ilog2(size_t i) { + size_t result = 0; + if(i >= 65536) { result += 16; i >>= 16; } + if(i >= 256) { result += 8; i >>= 8; } + if(i >= 16) { result += 4; i >>= 4; } + if(i >= 4) { result += 2; i >>= 2; } + if(i >= 2) { result += 1; /*i >>= 1;*/ } + return result; +} + +/* integer approximation for i * log2(i), helper function for LFS_ENTROPY */ +static size_t ilog2i(size_t i) { + size_t l; + if(i == 0) return 0; + l = ilog2(i); + /* approximate i*log2(i): l is integer logarithm, ((i - (1u << l)) << 1u) + linearly approximates the missing fractional part multiplied by i */ + return i * l + ((i - (((size_t)1) << l)) << 1u); +} + +static unsigned filter(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, + const LodePNGColorMode* color, const LodePNGEncoderSettings* settings) { + /* + For PNG filter method 0 + out must be a buffer with as size: h + (w * h * bpp + 7u) / 8u, because there are + the scanlines with 1 extra byte per scanline + */ + + unsigned bpp = lodepng_get_bpp(color); + /*the width of a scanline in bytes, not including the filter type*/ + size_t linebytes = lodepng_get_raw_size_idat(w, 1, bpp) - 1u; + + /*bytewidth is used for filtering, is 1 when bpp < 8, number of bytes per pixel otherwise*/ + size_t bytewidth = (bpp + 7u) / 8u; + const unsigned char* prevline = 0; + unsigned x, y; + unsigned error = 0; + LodePNGFilterStrategy strategy = settings->filter_strategy; + + if(settings->filter_palette_zero && (color->colortype == LCT_PALETTE || color->bitdepth < 8)) { + /*if the filter_palette_zero setting is enabled, override the filter strategy with + zero for all scanlines for palette and less-than-8-bitdepth images*/ + strategy = LFS_ZERO; + } + + if(bpp == 0) return 31; /*error: invalid color type*/ + + if(strategy >= LFS_ZERO && strategy <= LFS_FOUR) { + unsigned char type = (unsigned char)strategy; + for(y = 0; y != h; ++y) { + size_t outindex = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ + size_t inindex = linebytes * y; + out[outindex] = type; /*filter type byte*/ + filterScanline(&out[outindex + 1], &in[inindex], prevline, linebytes, bytewidth, type); + prevline = &in[inindex]; + } + } else if(strategy == LFS_MINSUM) { + /*adaptive filtering: independently for each row, try all five filter types and select the one that produces the + smallest sum of absolute values per row.*/ + unsigned char* attempt[5]; /*five filtering attempts, one for each filter type*/ + size_t smallest = 0; + unsigned char type, bestType = 0; + + for(type = 0; type != 5; ++type) { + attempt[type] = (unsigned char*)lodepng_malloc(linebytes); + if(!attempt[type]) error = 83; /*alloc fail*/ + } + + if(!error) { + for(y = 0; y != h; ++y) { + /*try the 5 filter types*/ + for(type = 0; type != 5; ++type) { + size_t sum = 0; + filterScanline(attempt[type], &in[y * linebytes], prevline, linebytes, bytewidth, type); + + /*calculate the sum of the result*/ + if(type == 0) { + for(x = 0; x != linebytes; ++x) sum += (unsigned char)(attempt[type][x]); + } else { + for(x = 0; x != linebytes; ++x) { + /*For differences, each byte should be treated as signed, values above 127 are negative + (converted to signed char). Filtertype 0 isn't a difference though, so use unsigned there. + This means filtertype 0 is almost never chosen, but that is justified.*/ + unsigned char s = attempt[type][x]; + sum += s < 128 ? s : (255U - s); + } + } + + /*check if this is smallest sum (or if type == 0 it's the first case so always store the values)*/ + if(type == 0 || sum < smallest) { + bestType = type; + smallest = sum; + } + } + + prevline = &in[y * linebytes]; + + /*now fill the out values*/ + out[y * (linebytes + 1)] = bestType; /*the first byte of a scanline will be the filter type*/ + for(x = 0; x != linebytes; ++x) out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; + } + } + + for(type = 0; type != 5; ++type) lodepng_free(attempt[type]); + } else if(strategy == LFS_ENTROPY) { + unsigned char* attempt[5]; /*five filtering attempts, one for each filter type*/ + size_t bestSum = 0; + unsigned type, bestType = 0; + unsigned count[256]; + + for(type = 0; type != 5; ++type) { + attempt[type] = (unsigned char*)lodepng_malloc(linebytes); + if(!attempt[type]) error = 83; /*alloc fail*/ + } + + if(!error) { + for(y = 0; y != h; ++y) { + /*try the 5 filter types*/ + for(type = 0; type != 5; ++type) { + size_t sum = 0; + filterScanline(attempt[type], &in[y * linebytes], prevline, linebytes, bytewidth, type); + lodepng_memset(count, 0, 256 * sizeof(*count)); + for(x = 0; x != linebytes; ++x) ++count[attempt[type][x]]; + ++count[type]; /*the filter type itself is part of the scanline*/ + for(x = 0; x != 256; ++x) { + sum += ilog2i(count[x]); + } + /*check if this is smallest sum (or if type == 0 it's the first case so always store the values)*/ + if(type == 0 || sum > bestSum) { + bestType = type; + bestSum = sum; + } + } + + prevline = &in[y * linebytes]; + + /*now fill the out values*/ + out[y * (linebytes + 1)] = bestType; /*the first byte of a scanline will be the filter type*/ + for(x = 0; x != linebytes; ++x) out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; + } + } + + for(type = 0; type != 5; ++type) lodepng_free(attempt[type]); + } else if(strategy == LFS_PREDEFINED) { + for(y = 0; y != h; ++y) { + size_t outindex = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ + size_t inindex = linebytes * y; + unsigned char type = settings->predefined_filters[y]; + out[outindex] = type; /*filter type byte*/ + filterScanline(&out[outindex + 1], &in[inindex], prevline, linebytes, bytewidth, type); + prevline = &in[inindex]; + } + } else if(strategy == LFS_BRUTE_FORCE) { + /*brute force filter chooser. + deflate the scanline after every filter attempt to see which one deflates best. + This is very slow and gives only slightly smaller, sometimes even larger, result*/ + size_t size[5]; + unsigned char* attempt[5]; /*five filtering attempts, one for each filter type*/ + size_t smallest = 0; + unsigned type = 0, bestType = 0; + unsigned char* dummy; + LodePNGCompressSettings zlibsettings; + lodepng_memcpy(&zlibsettings, &settings->zlibsettings, sizeof(LodePNGCompressSettings)); + /*use fixed tree on the attempts so that the tree is not adapted to the filtertype on purpose, + to simulate the true case where the tree is the same for the whole image. Sometimes it gives + better result with dynamic tree anyway. Using the fixed tree sometimes gives worse, but in rare + cases better compression. It does make this a bit less slow, so it's worth doing this.*/ + zlibsettings.btype = 1; + /*a custom encoder likely doesn't read the btype setting and is optimized for complete PNG + images only, so disable it*/ + zlibsettings.custom_zlib = 0; + zlibsettings.custom_deflate = 0; + for(type = 0; type != 5; ++type) { + attempt[type] = (unsigned char*)lodepng_malloc(linebytes); + if(!attempt[type]) error = 83; /*alloc fail*/ + } + if(!error) { + for(y = 0; y != h; ++y) /*try the 5 filter types*/ { + for(type = 0; type != 5; ++type) { + unsigned testsize = (unsigned)linebytes; + /*if(testsize > 8) testsize /= 8;*/ /*it already works good enough by testing a part of the row*/ + + filterScanline(attempt[type], &in[y * linebytes], prevline, linebytes, bytewidth, type); + size[type] = 0; + dummy = 0; + zlib_compress(&dummy, &size[type], attempt[type], testsize, &zlibsettings); + lodepng_free(dummy); + /*check if this is smallest size (or if type == 0 it's the first case so always store the values)*/ + if(type == 0 || size[type] < smallest) { + bestType = type; + smallest = size[type]; + } + } + prevline = &in[y * linebytes]; + out[y * (linebytes + 1)] = bestType; /*the first byte of a scanline will be the filter type*/ + for(x = 0; x != linebytes; ++x) out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; + } + } + for(type = 0; type != 5; ++type) lodepng_free(attempt[type]); + } + else return 88; /* unknown filter strategy */ + + return error; +} + +static void addPaddingBits(unsigned char* out, const unsigned char* in, + size_t olinebits, size_t ilinebits, unsigned h) { + /*The opposite of the removePaddingBits function + olinebits must be >= ilinebits*/ + unsigned y; + size_t diff = olinebits - ilinebits; + size_t obp = 0, ibp = 0; /*bit pointers*/ + for(y = 0; y != h; ++y) { + size_t x; + for(x = 0; x < ilinebits; ++x) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + /*obp += diff; --> no, fill in some value in the padding bits too, to avoid + "Use of uninitialised value of size ###" warning from valgrind*/ + for(x = 0; x != diff; ++x) setBitOfReversedStream(&obp, out, 0); + } +} + +/* +in: non-interlaced image with size w*h +out: the same pixels, but re-ordered according to PNG's Adam7 interlacing, with + no padding bits between scanlines, but between reduced images so that each + reduced image starts at a byte. +bpp: bits per pixel +there are no padding bits, not between scanlines, not between reduced images +in has the following size in bits: w * h * bpp. +out is possibly bigger due to padding bits between reduced images +NOTE: comments about padding bits are only relevant if bpp < 8 +*/ +static void Adam7_interlace(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, unsigned bpp) { + unsigned passw[7], passh[7]; + size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned i; + + Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); + + if(bpp >= 8) { + for(i = 0; i != 7; ++i) { + unsigned x, y, b; + size_t bytewidth = bpp / 8u; + for(y = 0; y < passh[i]; ++y) + for(x = 0; x < passw[i]; ++x) { + size_t pixelinstart = ((ADAM7_IY[i] + y * ADAM7_DY[i]) * w + ADAM7_IX[i] + x * ADAM7_DX[i]) * bytewidth; + size_t pixeloutstart = passstart[i] + (y * passw[i] + x) * bytewidth; + for(b = 0; b < bytewidth; ++b) { + out[pixeloutstart + b] = in[pixelinstart + b]; + } + } + } + } else /*bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit pointers*/ { + for(i = 0; i != 7; ++i) { + unsigned x, y, b; + unsigned ilinebits = bpp * passw[i]; + unsigned olinebits = bpp * w; + size_t obp, ibp; /*bit pointers (for out and in buffer)*/ + for(y = 0; y < passh[i]; ++y) + for(x = 0; x < passw[i]; ++x) { + ibp = (ADAM7_IY[i] + y * ADAM7_DY[i]) * olinebits + (ADAM7_IX[i] + x * ADAM7_DX[i]) * bpp; + obp = (8 * passstart[i]) + (y * ilinebits + x * bpp); + for(b = 0; b < bpp; ++b) { + unsigned char bit = readBitFromReversedStream(&ibp, in); + setBitOfReversedStream(&obp, out, bit); + } + } + } + } +} + +/*out must be buffer big enough to contain uncompressed IDAT chunk data, and in must contain the full image. +return value is error**/ +static unsigned preProcessScanlines(unsigned char** out, size_t* outsize, const unsigned char* in, + unsigned w, unsigned h, + const LodePNGInfo* info_png, const LodePNGEncoderSettings* settings) { + /* + This function converts the pure 2D image with the PNG's colortype, into filtered-padded-interlaced data. Steps: + *) if no Adam7: 1) add padding bits (= possible extra bits per scanline if bpp < 8) 2) filter + *) if adam7: 1) Adam7_interlace 2) 7x add padding bits 3) 7x filter + */ + size_t bpp = lodepng_get_bpp(&info_png->color); + unsigned error = 0; + if(info_png->interlace_method == 0) { + /*image size plus an extra byte per scanline + possible padding bits*/ + *outsize = (size_t)h + ((size_t)h * (((size_t)w * bpp + 7u) / 8u)); + *out = (unsigned char*)lodepng_malloc(*outsize); + if(!(*out) && (*outsize)) error = 83; /*alloc fail*/ + + if(!error) { + /*non multiple of 8 bits per scanline, padding bits needed per scanline*/ + if(bpp < 8 && (size_t)w * bpp != (((size_t)w * bpp + 7u) / 8u) * 8u) { + unsigned char* padded = (unsigned char*)lodepng_malloc(h * ((w * bpp + 7u) / 8u)); + if(!padded) error = 83; /*alloc fail*/ + if(!error) { + addPaddingBits(padded, in, (((size_t)w * bpp + 7u) / 8u) * 8u, (size_t)w * bpp, h); + error = filter(*out, padded, w, h, &info_png->color, settings); + } + lodepng_free(padded); + } else { + /*we can immediately filter into the out buffer, no other steps needed*/ + error = filter(*out, in, w, h, &info_png->color, settings); + } + } + } else /*interlace_method is 1 (Adam7)*/ { + unsigned passw[7], passh[7]; + size_t filter_passstart[8], padded_passstart[8], passstart[8]; + unsigned char* adam7; + + Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, (unsigned)bpp); + + *outsize = filter_passstart[7]; /*image size plus an extra byte per scanline + possible padding bits*/ + *out = (unsigned char*)lodepng_malloc(*outsize); + if(!(*out)) error = 83; /*alloc fail*/ + + adam7 = (unsigned char*)lodepng_malloc(passstart[7]); + if(!adam7 && passstart[7]) error = 83; /*alloc fail*/ + + if(!error) { + unsigned i; + + Adam7_interlace(adam7, in, w, h, (unsigned)bpp); + for(i = 0; i != 7; ++i) { + if(bpp < 8) { + unsigned char* padded = (unsigned char*)lodepng_malloc(padded_passstart[i + 1] - padded_passstart[i]); + if(!padded) ERROR_BREAK(83); /*alloc fail*/ + addPaddingBits(padded, &adam7[passstart[i]], + (((size_t)passw[i] * bpp + 7u) / 8u) * 8u, (size_t)passw[i] * bpp, passh[i]); + error = filter(&(*out)[filter_passstart[i]], padded, + passw[i], passh[i], &info_png->color, settings); + lodepng_free(padded); + } else { + error = filter(&(*out)[filter_passstart[i]], &adam7[padded_passstart[i]], + passw[i], passh[i], &info_png->color, settings); + } + + if(error) break; + } + } + + lodepng_free(adam7); + } + + return error; +} + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +static unsigned addUnknownChunks(ucvector* out, unsigned char* data, size_t datasize) { + unsigned char* inchunk = data; + while((size_t)(inchunk - data) < datasize) { + CERROR_TRY_RETURN(lodepng_chunk_append(&out->data, &out->size, inchunk)); + out->allocsize = out->size; /*fix the allocsize again*/ + inchunk = lodepng_chunk_next(inchunk, data + datasize); + } + return 0; +} + +static unsigned isGrayICCProfile(const unsigned char* profile, unsigned size) { + /* + It is a gray profile if bytes 16-19 are "GRAY", rgb profile if bytes 16-19 + are "RGB ". We do not perform any full parsing of the ICC profile here, other + than check those 4 bytes to grayscale profile. Other than that, validity of + the profile is not checked. This is needed only because the PNG specification + requires using a non-gray color model if there is an ICC profile with "RGB " + (sadly limiting compression opportunities if the input data is grayscale RGB + data), and requires using a gray color model if it is "GRAY". + */ + if(size < 20) return 0; + return profile[16] == 'G' && profile[17] == 'R' && profile[18] == 'A' && profile[19] == 'Y'; +} + +static unsigned isRGBICCProfile(const unsigned char* profile, unsigned size) { + /* See comment in isGrayICCProfile*/ + if(size < 20) return 0; + return profile[16] == 'R' && profile[17] == 'G' && profile[18] == 'B' && profile[19] == ' '; +} +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +unsigned lodepng_encode(unsigned char** out, size_t* outsize, + const unsigned char* image, unsigned w, unsigned h, + LodePNGState* state) { + unsigned char* data = 0; /*uncompressed version of the IDAT chunk data*/ + size_t datasize = 0; + ucvector outv = ucvector_init(NULL, 0); + LodePNGInfo info; + const LodePNGInfo* info_png = &state->info_png; + LodePNGColorMode auto_color; + unsigned error = 0; + + lodepng_info_init(&info); + lodepng_color_mode_init(&auto_color); + + /*provide some proper output values if error will happen*/ + *out = 0; + *outsize = 0; + + /*check input values validity*/ + if((info_png->color.colortype == LCT_PALETTE || state->encoder.force_palette) + && (info_png->color.palettesize == 0 || info_png->color.palettesize > 256)) { + /*this error is returned even if auto_convert is enabled and thus encoder could + generate the palette by itself: while allowing this could be possible in theory, + it may complicate the code or edge cases, and always requiring to give a palette + when setting this color type is a simpler contract*/ + error = 68; /*invalid palette size, it is only allowed to be 1-256*/ + goto cleanup; + } + if(state->encoder.zlibsettings.btype > 2) { + error = 61; /*error: invalid btype*/ + goto cleanup; + } + if(info_png->interlace_method > 1) { + error = 71; /*error: invalid interlace mode*/ + goto cleanup; + } + error = checkColorValidity(info_png->color.colortype, info_png->color.bitdepth); + if(error) goto cleanup; /*error: invalid color type given*/ + error = checkColorValidity(state->info_raw.colortype, state->info_raw.bitdepth); + if(error) goto cleanup; /*error: invalid color type given*/ + + /* color convert and compute scanline filter types */ + CERROR_TRY_RETURN(lodepng_info_copy(&info, &state->info_png)); + if(state->encoder.auto_convert) { + LodePNGColorStats stats; + unsigned allow_convert = 1; + lodepng_color_stats_init(&stats); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if(info_png->iccp_defined && + isGrayICCProfile(info_png->iccp_profile, info_png->iccp_profile_size)) { + /*the PNG specification does not allow to use palette with a GRAY ICC profile, even + if the palette has only gray colors, so disallow it.*/ + stats.allow_palette = 0; + } + if(info_png->iccp_defined && + isRGBICCProfile(info_png->iccp_profile, info_png->iccp_profile_size)) { + /*the PNG specification does not allow to use grayscale color with RGB ICC profile, so disallow gray.*/ + stats.allow_greyscale = 0; + } +#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ + error = lodepng_compute_color_stats(&stats, image, w, h, &state->info_raw); + if(error) goto cleanup; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if(info_png->background_defined) { + /*the background chunk's color must be taken into account as well*/ + unsigned r = 0, g = 0, b = 0; + LodePNGColorMode mode16 = lodepng_color_mode_make(LCT_RGB, 16); + lodepng_convert_rgb(&r, &g, &b, + info_png->background_r, info_png->background_g, info_png->background_b, &mode16, &info_png->color); + error = lodepng_color_stats_add(&stats, r, g, b, 65535); + if(error) goto cleanup; + } +#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ + error = auto_choose_color(&auto_color, &state->info_raw, &stats); + if(error) goto cleanup; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if(info_png->sbit_defined) { + /*if sbit is defined, due to strict requirements of which sbit values can be present for which color modes, + auto_convert can't be done in many cases. However, do support a few cases here. + TODO: more conversions may be possible, and it may also be possible to get a more appropriate color type out of + auto_choose_color if knowledge about sbit is used beforehand + */ + unsigned sbit_max = LODEPNG_MAX(LODEPNG_MAX(LODEPNG_MAX(info_png->sbit_r, info_png->sbit_g), + info_png->sbit_b), info_png->sbit_a); + unsigned equal = (!info_png->sbit_g || info_png->sbit_g == info_png->sbit_r) + && (!info_png->sbit_b || info_png->sbit_b == info_png->sbit_r) + && (!info_png->sbit_a || info_png->sbit_a == info_png->sbit_r); + allow_convert = 0; + if(info.color.colortype == LCT_PALETTE && + auto_color.colortype == LCT_PALETTE) { + /* input and output are palette, and in this case it may happen that palette data is + expected to be copied from info_raw into the info_png */ + allow_convert = 1; + } + /*going from 8-bit RGB to palette (or 16-bit as long as sbit_max <= 8) is possible + since both are 8-bit RGB for sBIT's purposes*/ + if(info.color.colortype == LCT_RGB && + auto_color.colortype == LCT_PALETTE && sbit_max <= 8) { + allow_convert = 1; + } + /*going from 8-bit RGBA to palette is also ok but only if sbit_a is exactly 8*/ + if(info.color.colortype == LCT_RGBA && auto_color.colortype == LCT_PALETTE && + info_png->sbit_a == 8 && sbit_max <= 8) { + allow_convert = 1; + } + /*going from 16-bit RGB(A) to 8-bit RGB(A) is ok if all sbit values are <= 8*/ + if((info.color.colortype == LCT_RGB || info.color.colortype == LCT_RGBA) && info.color.bitdepth == 16 && + auto_color.colortype == info.color.colortype && auto_color.bitdepth == 8 && + sbit_max <= 8) { + allow_convert = 1; + } + /*going to less channels is ok if all bit values are equal (all possible values in sbit, + as well as the chosen bitdepth of the result). Due to how auto_convert works, + we already know that auto_color.colortype has less than or equal amount of channels than + info.colortype. Palette is not used here. This conversion is not allowed if + info_png->sbit_r < auto_color.bitdepth, because specifically for alpha, non-presence of + an sbit value heavily implies that alpha's bit depth is equal to the PNG bit depth (rather + than the bit depths set in the r, g and b sbit values, by how the PNG specification describes + handling tRNS chunk case with sBIT), so be conservative here about ignoring user input.*/ + if(info.color.colortype != LCT_PALETTE && auto_color.colortype != LCT_PALETTE && + equal && info_png->sbit_r == auto_color.bitdepth) { + allow_convert = 1; + } + } +#endif + if(state->encoder.force_palette) { + if(info.color.colortype != LCT_GREY && info.color.colortype != LCT_GREY_ALPHA && + (auto_color.colortype == LCT_GREY || auto_color.colortype == LCT_GREY_ALPHA)) { + /*user specifically forced a PLTE palette, so cannot convert to grayscale types because + the PNG specification only allows writing a suggested palette in PLTE for truecolor types*/ + allow_convert = 0; + } + } + if(allow_convert) { + lodepng_color_mode_copy(&info.color, &auto_color); +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*also convert the background chunk*/ + if(info_png->background_defined) { + if(lodepng_convert_rgb(&info.background_r, &info.background_g, &info.background_b, + info_png->background_r, info_png->background_g, info_png->background_b, &info.color, &info_png->color)) { + error = 104; + goto cleanup; + } + } +#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ + } + } +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + if(info_png->iccp_defined) { + unsigned gray_icc = isGrayICCProfile(info_png->iccp_profile, info_png->iccp_profile_size); + unsigned rgb_icc = isRGBICCProfile(info_png->iccp_profile, info_png->iccp_profile_size); + unsigned gray_png = info.color.colortype == LCT_GREY || info.color.colortype == LCT_GREY_ALPHA; + if(!gray_icc && !rgb_icc) { + error = 100; /* Disallowed profile color type for PNG */ + goto cleanup; + } + if(gray_icc != gray_png) { + /*Not allowed to use RGB/RGBA/palette with GRAY ICC profile or vice versa, + or in case of auto_convert, it wasn't possible to find appropriate model*/ + error = state->encoder.auto_convert ? 102 : 101; + goto cleanup; + } + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + if(!lodepng_color_mode_equal(&state->info_raw, &info.color)) { + unsigned char* converted; + size_t size = ((size_t)w * (size_t)h * (size_t)lodepng_get_bpp(&info.color) + 7u) / 8u; + + converted = (unsigned char*)lodepng_malloc(size); + if(!converted && size) error = 83; /*alloc fail*/ + if(!error) { + error = lodepng_convert(converted, image, &info.color, &state->info_raw, w, h); + } + if(!error) { + error = preProcessScanlines(&data, &datasize, converted, w, h, &info, &state->encoder); + } + lodepng_free(converted); + if(error) goto cleanup; + } else { + error = preProcessScanlines(&data, &datasize, image, w, h, &info, &state->encoder); + if(error) goto cleanup; + } + + /* output all PNG chunks */ { +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + size_t i; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + /*write signature and chunks*/ + error = writeSignature(&outv); + if(error) goto cleanup; + /*IHDR*/ + error = addChunk_IHDR(&outv, w, h, info.color.colortype, info.color.bitdepth, info.interlace_method); + if(error) goto cleanup; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*unknown chunks between IHDR and PLTE*/ + if(info.unknown_chunks_data[0]) { + error = addUnknownChunks(&outv, info.unknown_chunks_data[0], info.unknown_chunks_size[0]); + if(error) goto cleanup; + } + /*color profile chunks must come before PLTE */ + if(info.cicp_defined) { + error = addChunk_cICP(&outv, &info); + if(error) goto cleanup; + } + if(info.mdcv_defined) { + error = addChunk_mDCV(&outv, &info); + if(error) goto cleanup; + } + if(info.clli_defined) { + error = addChunk_cLLI(&outv, &info); + if(error) goto cleanup; + } + if(info.iccp_defined) { + error = addChunk_iCCP(&outv, &info, &state->encoder.zlibsettings); + if(error) goto cleanup; + } + if(info.srgb_defined) { + error = addChunk_sRGB(&outv, &info); + if(error) goto cleanup; + } + if(info.gama_defined) { + error = addChunk_gAMA(&outv, &info); + if(error) goto cleanup; + } + if(info.chrm_defined) { + error = addChunk_cHRM(&outv, &info); + if(error) goto cleanup; + } + if(info_png->sbit_defined) { + error = addChunk_sBIT(&outv, &info); + if(error) goto cleanup; + } + if(info.exif_defined) { + error = addChunk_eXIf(&outv, &info); + if(error) goto cleanup; + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + /*PLTE*/ + if(info.color.colortype == LCT_PALETTE) { + error = addChunk_PLTE(&outv, &info.color); + if(error) goto cleanup; + } + if(state->encoder.force_palette && (info.color.colortype == LCT_RGB || info.color.colortype == LCT_RGBA)) { + /*force_palette means: write suggested palette for truecolor in PLTE chunk*/ + error = addChunk_PLTE(&outv, &info.color); + if(error) goto cleanup; + } + /*tRNS (this will only add if when necessary) */ + error = addChunk_tRNS(&outv, &info.color); + if(error) goto cleanup; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*bKGD (must come between PLTE and the IDAt chunks*/ + if(info.background_defined) { + error = addChunk_bKGD(&outv, &info); + if(error) goto cleanup; + } + /*pHYs (must come before the IDAT chunks)*/ + if(info.phys_defined) { + error = addChunk_pHYs(&outv, &info); + if(error) goto cleanup; + } + + /*unknown chunks between PLTE and IDAT*/ + if(info.unknown_chunks_data[1]) { + error = addUnknownChunks(&outv, info.unknown_chunks_data[1], info.unknown_chunks_size[1]); + if(error) goto cleanup; + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + /*IDAT (multiple IDAT chunks must be consecutive)*/ + error = addChunk_IDAT(&outv, data, datasize, &state->encoder.zlibsettings); + if(error) goto cleanup; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*tIME*/ + if(info.time_defined) { + error = addChunk_tIME(&outv, &info.time); + if(error) goto cleanup; + } + /*tEXt and/or zTXt*/ + for(i = 0; i != info.text_num; ++i) { + if(lodepng_strlen(info.text_keys[i]) > 79) { + error = 66; /*text chunk too large*/ + goto cleanup; + } + if(lodepng_strlen(info.text_keys[i]) < 1) { + error = 67; /*text chunk too small*/ + goto cleanup; + } + if(state->encoder.text_compression) { + error = addChunk_zTXt(&outv, info.text_keys[i], info.text_strings[i], &state->encoder.zlibsettings); + if(error) goto cleanup; + } else { + error = addChunk_tEXt(&outv, info.text_keys[i], info.text_strings[i]); + if(error) goto cleanup; + } + } + /*LodePNG version id in text chunk*/ + if(state->encoder.add_id) { + unsigned already_added_id_text = 0; + for(i = 0; i != info.text_num; ++i) { + const char* k = info.text_keys[i]; + /* Could use strcmp, but we're not calling or reimplementing this C library function for this use only */ + if(k[0] == 'L' && k[1] == 'o' && k[2] == 'd' && k[3] == 'e' && + k[4] == 'P' && k[5] == 'N' && k[6] == 'G' && k[7] == '\0') { + already_added_id_text = 1; + break; + } + } + if(already_added_id_text == 0) { + error = addChunk_tEXt(&outv, "LodePNG", LODEPNG_VERSION_STRING); /*it's shorter as tEXt than as zTXt chunk*/ + if(error) goto cleanup; + } + } + /*iTXt*/ + for(i = 0; i != info.itext_num; ++i) { + if(lodepng_strlen(info.itext_keys[i]) > 79) { + error = 66; /*text chunk too large*/ + goto cleanup; + } + if(lodepng_strlen(info.itext_keys[i]) < 1) { + error = 67; /*text chunk too small*/ + goto cleanup; + } + error = addChunk_iTXt( + &outv, state->encoder.text_compression, + info.itext_keys[i], info.itext_langtags[i], info.itext_transkeys[i], info.itext_strings[i], + &state->encoder.zlibsettings); + if(error) goto cleanup; + } + + /*unknown chunks between IDAT and IEND*/ + if(info.unknown_chunks_data[2]) { + error = addUnknownChunks(&outv, info.unknown_chunks_data[2], info.unknown_chunks_size[2]); + if(error) goto cleanup; + } +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + error = addChunk_IEND(&outv); + if(error) goto cleanup; + } + +cleanup: + lodepng_info_cleanup(&info); + lodepng_free(data); + lodepng_color_mode_cleanup(&auto_color); + + /*instead of cleaning the vector up, give it to the output*/ + *out = outv.data; + *outsize = outv.size; + + state->error = error; /*TODO: remove this and make input state const*/ + + return error; +} + +unsigned lodepng_encode_memory(unsigned char** out, size_t* outsize, const unsigned char* image, + unsigned w, unsigned h, LodePNGColorType colortype, unsigned bitdepth) { + unsigned error; + LodePNGState state; + lodepng_state_init(&state); + state.info_raw.colortype = colortype; + state.info_raw.bitdepth = bitdepth; + state.info_png.color.colortype = colortype; + state.info_png.color.bitdepth = bitdepth; + error = lodepng_encode(out, outsize, image, w, h, &state); + lodepng_state_cleanup(&state); + return error; +} + +unsigned lodepng_encode32(unsigned char** out, size_t* outsize, const unsigned char* image, unsigned w, unsigned h) { + return lodepng_encode_memory(out, outsize, image, w, h, LCT_RGBA, 8); +} + +unsigned lodepng_encode24(unsigned char** out, size_t* outsize, const unsigned char* image, unsigned w, unsigned h) { + return lodepng_encode_memory(out, outsize, image, w, h, LCT_RGB, 8); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned lodepng_encode_file(const char* filename, const unsigned char* image, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth) { + unsigned char* buffer; + size_t buffersize; + unsigned error = lodepng_encode_memory(&buffer, &buffersize, image, w, h, colortype, bitdepth); + if(!error) error = lodepng_save_file(buffer, buffersize, filename); + lodepng_free(buffer); + return error; +} + +unsigned lodepng_encode32_file(const char* filename, const unsigned char* image, unsigned w, unsigned h) { + return lodepng_encode_file(filename, image, w, h, LCT_RGBA, 8); +} + +unsigned lodepng_encode24_file(const char* filename, const unsigned char* image, unsigned w, unsigned h) { + return lodepng_encode_file(filename, image, w, h, LCT_RGB, 8); +} +#endif /*LODEPNG_COMPILE_DISK*/ + +void lodepng_encoder_settings_init(LodePNGEncoderSettings* settings) { + lodepng_compress_settings_init(&settings->zlibsettings); + settings->filter_palette_zero = 1; + settings->filter_strategy = LFS_MINSUM; + settings->auto_convert = 1; + settings->force_palette = 0; + settings->predefined_filters = 0; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + settings->add_id = 0; + settings->text_compression = 1; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} + +#endif /*LODEPNG_COMPILE_ENCODER*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +#ifdef LODEPNG_COMPILE_ERROR_TEXT +/* +This returns the description of a numerical error code in English. This is also +the documentation of all the error codes. +*/ +const char* lodepng_error_text(unsigned code) { + switch(code) { + case 0: return "no error, everything went ok"; + case 1: return "nothing done yet"; /*the Encoder/Decoder has done nothing yet, error checking makes no sense yet*/ + case 10: return "end of input memory reached without huffman end code"; /*while huffman decoding*/ + case 11: return "error in code tree made it jump outside of huffman tree"; /*while huffman decoding*/ + case 13: return "problem while processing dynamic deflate block"; + case 14: return "problem while processing dynamic deflate block"; + case 15: return "problem while processing dynamic deflate block"; + /*this error could happen if there are only 0 or 1 symbols present in the huffman code:*/ + case 16: return "invalid code while processing dynamic deflate block"; + case 17: return "end of out buffer memory reached while inflating"; + case 18: return "invalid distance code while inflating"; + case 19: return "end of out buffer memory reached while inflating"; + case 20: return "invalid deflate block BTYPE encountered while decoding"; + case 21: return "NLEN is not ones complement of LEN in a deflate block"; + + /*end of out buffer memory reached while inflating: + This can happen if the inflated deflate data is longer than the amount of bytes required to fill up + all the pixels of the image, given the color depth and image dimensions. Something that doesn't + happen in a normal, well encoded, PNG image.*/ + case 22: return "end of out buffer memory reached while inflating"; + case 23: return "end of in buffer memory reached while inflating"; + case 24: return "invalid FCHECK in zlib header"; + case 25: return "invalid compression method in zlib header"; + case 26: return "FDICT encountered in zlib header while it's not used for PNG"; + case 27: return "PNG file is smaller than a PNG header"; + /*Checks the magic file header, the first 8 bytes of the PNG file*/ + case 28: return "incorrect PNG signature, it's no PNG or corrupted"; + case 29: return "first chunk is not the header chunk"; + case 30: return "chunk length too large, chunk broken off at end of file"; + case 31: return "illegal PNG color type or bpp"; + case 32: return "illegal PNG compression method"; + case 33: return "illegal PNG filter method"; + case 34: return "illegal PNG interlace method"; + case 35: return "chunk length of a chunk is too large or the chunk too small"; + case 36: return "illegal PNG filter type encountered"; + case 37: return "illegal bit depth for this color type given"; + case 38: return "the palette is too small or too big"; /*0, or more than 256 colors*/ + case 39: return "tRNS chunk before PLTE or has more entries than palette size"; + case 40: return "tRNS chunk has wrong size for grayscale image"; + case 41: return "tRNS chunk has wrong size for RGB image"; + case 42: return "tRNS chunk appeared while it was not allowed for this color type"; + case 43: return "bKGD chunk has wrong size for palette image"; + case 44: return "bKGD chunk has wrong size for grayscale image"; + case 45: return "bKGD chunk has wrong size for RGB image"; + case 48: return "empty input buffer given to decoder. Maybe caused by non-existing file?"; + case 49: return "jumped past memory while generating dynamic huffman tree"; + case 50: return "jumped past memory while generating dynamic huffman tree"; + case 51: return "jumped past memory while inflating huffman block"; + case 52: return "jumped past memory while inflating"; + case 53: return "size of zlib data too small"; + case 54: return "repeat symbol in tree while there was no value symbol yet"; + /*jumped past tree while generating huffman tree, this could be when the + tree will have more leaves than symbols after generating it out of the + given lengths. They call this an oversubscribed dynamic bit lengths tree in zlib.*/ + case 55: return "jumped past tree while generating huffman tree"; + case 56: return "given output image colortype or bitdepth not supported for color conversion"; + case 57: return "invalid CRC encountered (checking CRC can be disabled)"; + case 58: return "invalid ADLER32 encountered (checking ADLER32 can be disabled)"; + case 59: return "requested color conversion not supported"; + case 60: return "invalid window size given in the settings of the encoder (must be 0-32768)"; + case 61: return "invalid BTYPE given in the settings of the encoder (only 0, 1 and 2 are allowed)"; + /*LodePNG leaves the choice of RGB to grayscale conversion formula to the user.*/ + case 62: return "conversion from color to grayscale not supported"; + /*(2^31-1)*/ + case 63: return "length of a chunk too long, max allowed for PNG is 2147483647 bytes per chunk"; + /*this would result in the inability of a deflated block to ever contain an end code. It must be at least 1.*/ + case 64: return "the length of the END symbol 256 in the Huffman tree is 0"; + case 66: return "the length of a text chunk keyword given to the encoder is longer than the maximum of 79 bytes"; + case 67: return "the length of a text chunk keyword given to the encoder is smaller than the minimum of 1 byte"; + case 68: return "tried to encode a PLTE chunk with a palette that has less than 1 or more than 256 colors"; + case 69: return "unknown chunk type with 'critical' flag encountered by the decoder"; + case 71: return "invalid interlace mode given to encoder (must be 0 or 1)"; + case 72: return "while decoding, invalid compression method encountered in zTXt, iTXt or iCCP chunk (it must be 0)"; + case 73: return "invalid tIME chunk size"; + case 74: return "invalid pHYs chunk size"; + /*length could be wrong, or data chopped off*/ + case 75: return "no null termination char found while decoding text chunk"; + case 76: return "iTXt chunk too short to contain required bytes"; + case 77: return "integer overflow in buffer size"; + case 78: return "failed to open file for reading"; /*file doesn't exist or couldn't be opened for reading*/ + case 79: return "failed to open file for writing"; + case 80: return "tried creating a tree of 0 symbols"; + case 81: return "lazy matching at pos 0 is impossible"; + case 82: return "color conversion to palette requested while a color isn't in palette, or index out of bounds"; + case 83: return "memory allocation failed"; + case 84: return "given image too small to contain all pixels to be encoded"; + case 86: return "impossible offset in lz77 encoding (internal bug)"; + case 87: return "must provide custom zlib function pointer if LODEPNG_COMPILE_ZLIB is not defined"; + case 88: return "invalid filter strategy given for LodePNGEncoderSettings.filter_strategy"; + case 89: return "text chunk keyword too short or long: must have size 1-79"; + /*the windowsize in the LodePNGCompressSettings. Requiring POT(==> & instead of %) makes encoding 12% faster.*/ + case 90: return "windowsize must be a power of two"; + case 91: return "invalid decompressed idat size"; + case 92: return "integer overflow due to too many pixels"; + case 93: return "zero width or height is invalid"; + case 94: return "header chunk must have a size of 13 bytes"; + case 95: return "integer overflow with combined idat chunk size"; + case 96: return "invalid gAMA chunk size"; + case 97: return "invalid cHRM chunk size"; + case 98: return "invalid sRGB chunk size"; + case 99: return "invalid sRGB rendering intent"; + case 100: return "invalid ICC profile color type, the PNG specification only allows RGB or GRAY"; + case 101: return "PNG specification does not allow RGB ICC profile on gray color types and vice versa"; + case 102: return "not allowed to set grayscale ICC profile with colored pixels by PNG specification"; + case 103: return "invalid palette index in bKGD chunk. Maybe it came before PLTE chunk?"; + case 104: return "invalid bKGD color while encoding (e.g. palette index out of range)"; + case 105: return "integer overflow of bitsize"; + case 106: return "PNG file must have PLTE chunk if color type is palette"; + case 107: return "color convert from palette mode requested without setting the palette data in it"; + case 108: return "tried to add more than 256 values to a palette"; + /*this limit can be configured in LodePNGDecompressSettings*/ + case 109: return "tried to decompress zlib or deflate data larger than desired max_output_size"; + case 110: return "custom zlib or inflate decompression failed"; + case 111: return "custom zlib or deflate compression failed"; + /*max text size limit can be configured in LodePNGDecoderSettings. This error prevents + unreasonable memory consumption when decoding due to impossibly large text sizes.*/ + case 112: return "compressed text unreasonably large"; + /*max ICC size limit can be configured in LodePNGDecoderSettings. This error prevents + unreasonable memory consumption when decoding due to impossibly large ICC profile*/ + case 113: return "ICC profile unreasonably large"; + case 114: return "sBIT chunk has wrong size for the color type of the image"; + case 115: return "sBIT value out of range"; + case 116: return "cICP value out of range"; + case 117: return "invalid cICP chunk size"; + case 118: return "mDCV value out of range"; + case 119: return "invalid mDCV chunk size"; + case 120: return "invalid cLLI chunk size"; + case 121: return "invalid chunk type name: may only contain [a-zA-Z]"; + case 122: return "invalid chunk type name: third character must be uppercase"; + case 123: return "invalid ICC profile size"; + } + return "unknown error code"; +} +#endif /*LODEPNG_COMPILE_ERROR_TEXT*/ + +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* // C++ Wrapper // */ +/* ////////////////////////////////////////////////////////////////////////// */ +/* ////////////////////////////////////////////////////////////////////////// */ + +#ifdef LODEPNG_COMPILE_CPP +namespace lodepng { + +#ifdef LODEPNG_COMPILE_DISK +/* Resizes the vector to the file size and reads the file into it. Returns error code.*/ +static unsigned load_file_(std::vector& buffer, FILE* file) { + long size = lodepng_filesize(file); + if(size < 0) return 78; + buffer.resize((size_t)size); + if(size == 0) return 0; /*ok*/ + if(fread(&buffer[0], 1, buffer.size(), file) != buffer.size()) return 78; + return 0; /*ok*/ +} + +unsigned load_file(std::vector& buffer, const std::string& filename) { + unsigned error; + FILE* file = fopen(filename.c_str(), "rb"); + if(!file) return 78; + error = load_file_(buffer, file); + fclose(file); + return error; +} + +/*write given buffer to the file, overwriting the file, it doesn't append to it.*/ +unsigned save_file(const std::vector& buffer, const std::string& filename) { + return lodepng_save_file(buffer.empty() ? 0 : &buffer[0], buffer.size(), filename.c_str()); +} +#endif /* LODEPNG_COMPILE_DISK */ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_DECODER +unsigned decompress(std::vector& out, const unsigned char* in, size_t insize, + const LodePNGDecompressSettings& settings) { + unsigned char* buffer = 0; + size_t buffersize = 0; + unsigned error = zlib_decompress(&buffer, &buffersize, 0, in, insize, &settings); + if(buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned decompress(std::vector& out, const std::vector& in, + const LodePNGDecompressSettings& settings) { + return decompress(out, in.empty() ? 0 : &in[0], in.size(), settings); +} +#endif /* LODEPNG_COMPILE_DECODER */ + +#ifdef LODEPNG_COMPILE_ENCODER +unsigned compress(std::vector& out, const unsigned char* in, size_t insize, + const LodePNGCompressSettings& settings) { + unsigned char* buffer = 0; + size_t buffersize = 0; + unsigned error = zlib_compress(&buffer, &buffersize, in, insize, &settings); + if(buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned compress(std::vector& out, const std::vector& in, + const LodePNGCompressSettings& settings) { + return compress(out, in.empty() ? 0 : &in[0], in.size(), settings); +} +#endif /* LODEPNG_COMPILE_ENCODER */ +#endif /* LODEPNG_COMPILE_ZLIB */ + + +#ifdef LODEPNG_COMPILE_PNG + +State::State() { + lodepng_state_init(this); +} + +State::State(const State& other) { + lodepng_state_init(this); + lodepng_state_copy(this, &other); +} + +State::~State() { + lodepng_state_cleanup(this); +} + +State& State::operator=(const State& other) { + lodepng_state_copy(this, &other); + return *this; +} + +#ifdef LODEPNG_COMPILE_DECODER + +unsigned decode(std::vector& out, unsigned& w, unsigned& h, const unsigned char* in, + size_t insize, LodePNGColorType colortype, unsigned bitdepth) { + unsigned char* buffer = 0; + unsigned error = lodepng_decode_memory(&buffer, &w, &h, in, insize, colortype, bitdepth); + if(buffer && !error) { + State state; + state.info_raw.colortype = colortype; + state.info_raw.bitdepth = bitdepth; + size_t buffersize = lodepng_get_raw_size(w, h, &state.info_raw); + out.insert(out.end(), buffer, &buffer[buffersize]); + } + lodepng_free(buffer); + return error; +} + +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + const std::vector& in, LodePNGColorType colortype, unsigned bitdepth) { + return decode(out, w, h, in.empty() ? 0 : &in[0], (unsigned)in.size(), colortype, bitdepth); +} + +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + State& state, + const unsigned char* in, size_t insize) { + unsigned char* buffer = NULL; + unsigned error = lodepng_decode(&buffer, &w, &h, &state, in, insize); + if(buffer && !error) { + size_t buffersize = lodepng_get_raw_size(w, h, &state.info_raw); + out.insert(out.end(), buffer, &buffer[buffersize]); + } + lodepng_free(buffer); + return error; +} + +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + State& state, + const std::vector& in) { + return decode(out, w, h, state, in.empty() ? 0 : &in[0], in.size()); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned decode(std::vector& out, unsigned& w, unsigned& h, const std::string& filename, + LodePNGColorType colortype, unsigned bitdepth) { + std::vector buffer; + /* safe output values in case error happens */ + w = h = 0; + unsigned error = load_file(buffer, filename); + if(error) return error; + return decode(out, w, h, buffer, colortype, bitdepth); +} +#endif /* LODEPNG_COMPILE_DECODER */ +#endif /* LODEPNG_COMPILE_DISK */ + +#ifdef LODEPNG_COMPILE_ENCODER +unsigned encode(std::vector& out, const unsigned char* in, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth) { + unsigned char* buffer; + size_t buffersize; + unsigned error = lodepng_encode_memory(&buffer, &buffersize, in, w, h, colortype, bitdepth); + if(buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned encode(std::vector& out, + const std::vector& in, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth) { + if(lodepng_get_raw_size_lct(w, h, colortype, bitdepth) > in.size()) return 84; + return encode(out, in.empty() ? 0 : &in[0], w, h, colortype, bitdepth); +} + +unsigned encode(std::vector& out, + const unsigned char* in, unsigned w, unsigned h, + State& state) { + unsigned char* buffer; + size_t buffersize; + unsigned error = lodepng_encode(&buffer, &buffersize, in, w, h, &state); + if(buffer) { + out.insert(out.end(), buffer, &buffer[buffersize]); + lodepng_free(buffer); + } + return error; +} + +unsigned encode(std::vector& out, + const std::vector& in, unsigned w, unsigned h, + State& state) { + if(lodepng_get_raw_size(w, h, &state.info_raw) > in.size()) return 84; + return encode(out, in.empty() ? 0 : &in[0], w, h, state); +} + +#ifdef LODEPNG_COMPILE_DISK +unsigned encode(const std::string& filename, + const unsigned char* in, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth) { + std::vector buffer; + unsigned error = encode(buffer, in, w, h, colortype, bitdepth); + if(!error) error = save_file(buffer, filename); + return error; +} + +unsigned encode(const std::string& filename, + const std::vector& in, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth) { + if(lodepng_get_raw_size_lct(w, h, colortype, bitdepth) > in.size()) return 84; + return encode(filename, in.empty() ? 0 : &in[0], w, h, colortype, bitdepth); +} +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_ENCODER */ +#endif /* LODEPNG_COMPILE_PNG */ +} /* namespace lodepng */ +#endif /*LODEPNG_COMPILE_CPP*/ diff --git a/server/deps/lodepng/lodepng.h b/server/deps/lodepng/lodepng.h new file mode 100644 index 000000000..8517eaeb8 --- /dev/null +++ b/server/deps/lodepng/lodepng.h @@ -0,0 +1,2188 @@ +/* +LodePNG version 20260119 + +Copyright (c) 2005-2026 Lode Vandevenne + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 3. This notice may not be removed or altered from any source + distribution. +*/ + +#ifndef LODEPNG_H +#define LODEPNG_H + +#include /*for size_t*/ + +extern const char* LODEPNG_VERSION_STRING; + +/* +The following #defines are used to create code sections. They can be disabled +to disable code sections, which can give faster compile time and smaller binary. +The "NO_COMPILE" defines are designed to be used to pass as defines to the +compiler command to disable them without modifying this header, e.g. +-DLODEPNG_NO_COMPILE_ZLIB for gcc or clang. +*/ +/*deflate & zlib. If disabled, you must specify alternative zlib functions in +the custom_zlib field of the compress and decompress settings*/ +#ifndef LODEPNG_NO_COMPILE_ZLIB +/*pass -DLODEPNG_NO_COMPILE_ZLIB to the compiler to disable this, or comment out LODEPNG_COMPILE_ZLIB below*/ +#define LODEPNG_COMPILE_ZLIB +#endif + +/*png encoder and png decoder*/ +#ifndef LODEPNG_NO_COMPILE_PNG +/*pass -DLODEPNG_NO_COMPILE_PNG to the compiler to disable this, or comment out LODEPNG_COMPILE_PNG below*/ +#define LODEPNG_COMPILE_PNG +#endif + +/*deflate&zlib decoder and png decoder*/ +#ifndef LODEPNG_NO_COMPILE_DECODER +/*pass -DLODEPNG_NO_COMPILE_DECODER to the compiler to disable this, or comment out LODEPNG_COMPILE_DECODER below*/ +#define LODEPNG_COMPILE_DECODER +#endif + +/*deflate&zlib encoder and png encoder*/ +#ifndef LODEPNG_NO_COMPILE_ENCODER +/*pass -DLODEPNG_NO_COMPILE_ENCODER to the compiler to disable this, or comment out LODEPNG_COMPILE_ENCODER below*/ +#define LODEPNG_COMPILE_ENCODER +#endif + +/*the optional built in harddisk file loading and saving functions*/ +#ifndef LODEPNG_NO_COMPILE_DISK +/*pass -DLODEPNG_NO_COMPILE_DISK to the compiler to disable this, or comment out LODEPNG_COMPILE_DISK below*/ +#define LODEPNG_COMPILE_DISK +#endif + +/*support for chunks other than IHDR, IDAT, PLTE, tRNS, IEND: ancillary and unknown chunks*/ +#ifndef LODEPNG_NO_COMPILE_ANCILLARY_CHUNKS +/*pass -DLODEPNG_NO_COMPILE_ANCILLARY_CHUNKS to the compiler to disable this, +or comment out LODEPNG_COMPILE_ANCILLARY_CHUNKS below*/ +#define LODEPNG_COMPILE_ANCILLARY_CHUNKS +#endif + +/*ability to convert error numerical codes to English text string*/ +#ifndef LODEPNG_NO_COMPILE_ERROR_TEXT +/*pass -DLODEPNG_NO_COMPILE_ERROR_TEXT to the compiler to disable this, +or comment out LODEPNG_COMPILE_ERROR_TEXT below*/ +#define LODEPNG_COMPILE_ERROR_TEXT +#endif + +/*Compile the default allocators (C's free, malloc and realloc). If you disable this, +you can define the functions lodepng_free, lodepng_malloc and lodepng_realloc in your +source files with custom allocators.*/ +#ifndef LODEPNG_NO_COMPILE_ALLOCATORS +/*pass -DLODEPNG_NO_COMPILE_ALLOCATORS to the compiler to disable the built-in ones, +or comment out LODEPNG_COMPILE_ALLOCATORS below*/ +#define LODEPNG_COMPILE_ALLOCATORS +#endif + +/*Disable built-in CRC function, in that case a custom implementation of +lodepng_crc32 must be defined externally so that it can be linked in. +The default built-in CRC code comes with 8KB of lookup tables, so for memory constrained environment you may want it +disabled and provide a much smaller implementation externally as said above. You can find such an example implementation +in a comment in the lodepng.c(pp) file in the 'else' case of the searchable LODEPNG_COMPILE_CRC section.*/ +#ifndef LODEPNG_NO_COMPILE_CRC +/*pass -DLODEPNG_NO_COMPILE_CRC to the compiler to disable the built-in one, +or comment out LODEPNG_COMPILE_CRC below*/ +#define LODEPNG_COMPILE_CRC +#endif + +/*compile the C++ version (you can disable the C++ wrapper here even when compiling for C++)*/ +#ifdef __cplusplus +#ifndef LODEPNG_NO_COMPILE_CPP +/*pass -DLODEPNG_NO_COMPILE_CPP to the compiler to disable C++ (not needed if a C-only compiler), +or comment out LODEPNG_COMPILE_CPP below*/ +#define LODEPNG_COMPILE_CPP +#endif +#endif + +#ifdef LODEPNG_COMPILE_CPP +#include +#include +#endif /*LODEPNG_COMPILE_CPP*/ + +#ifdef LODEPNG_COMPILE_PNG +/*The PNG color types (also used for raw image).*/ +typedef enum LodePNGColorType { + LCT_GREY = 0, /*grayscale: 1,2,4,8,16 bit*/ + LCT_RGB = 2, /*RGB: 8,16 bit*/ + LCT_PALETTE = 3, /*palette: 1,2,4,8 bit*/ + LCT_GREY_ALPHA = 4, /*grayscale with alpha: 8,16 bit*/ + LCT_RGBA = 6, /*RGB with alpha: 8,16 bit*/ + /*LCT_MAX_OCTET_VALUE lets the compiler allow this enum to represent any invalid + byte value from 0 to 255 that could be present in an invalid PNG file header. Do + not use, compare with or set the name LCT_MAX_OCTET_VALUE, instead either use + the valid color type names above, or numeric values like 1 or 7 when checking for + particular disallowed color type byte values, or cast to integer to print it.*/ + LCT_MAX_OCTET_VALUE = 255 +} LodePNGColorType; + +#ifdef LODEPNG_COMPILE_DECODER +/* +Converts PNG data in memory to raw pixel data. +out: Output parameter. Pointer to buffer that will contain the raw pixel data. + After decoding, its size is w * h * (bytes per pixel) bytes larger than + initially. Bytes per pixel depends on colortype and bitdepth. + Must be freed after usage with free(*out). + Note: for 16-bit per channel colors, uses big endian format like PNG does. +w: Output parameter. Pointer to width of pixel data. +h: Output parameter. Pointer to height of pixel data. +in: Memory buffer with the PNG file. +insize: size of the in buffer. +colortype: the desired color type for the raw output image. See explanation on PNG color types. +bitdepth: the desired bit depth for the raw output image. See explanation on PNG color types. +Return value: LodePNG error code (0 means no error). +*/ +unsigned lodepng_decode_memory(unsigned char** out, unsigned* w, unsigned* h, + const unsigned char* in, size_t insize, + LodePNGColorType colortype, unsigned bitdepth); + +/*Same as lodepng_decode_memory, but always decodes to 32-bit RGBA raw image*/ +unsigned lodepng_decode32(unsigned char** out, unsigned* w, unsigned* h, + const unsigned char* in, size_t insize); + +/*Same as lodepng_decode_memory, but always decodes to 24-bit RGB raw image*/ +unsigned lodepng_decode24(unsigned char** out, unsigned* w, unsigned* h, + const unsigned char* in, size_t insize); + +#ifdef LODEPNG_COMPILE_DISK +/* +Load PNG from disk, from file with given name. +Same as the other decode functions, but instead takes a filename as input. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory.*/ +unsigned lodepng_decode_file(unsigned char** out, unsigned* w, unsigned* h, + const char* filename, + LodePNGColorType colortype, unsigned bitdepth); + +/*Same as lodepng_decode_file, but always decodes to 32-bit RGBA raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory.*/ +unsigned lodepng_decode32_file(unsigned char** out, unsigned* w, unsigned* h, + const char* filename); + +/*Same as lodepng_decode_file, but always decodes to 24-bit RGB raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory.*/ +unsigned lodepng_decode24_file(unsigned char** out, unsigned* w, unsigned* h, + const char* filename); +#endif /*LODEPNG_COMPILE_DISK*/ +#endif /*LODEPNG_COMPILE_DECODER*/ + + +#ifdef LODEPNG_COMPILE_ENCODER +/* +Converts raw pixel data into a PNG image in memory. The colortype and bitdepth + of the output PNG image cannot be chosen, they are automatically determined + by the colortype, bitdepth and content of the input pixel data. + Note: for 16-bit per channel colors, needs big endian format like PNG does. +out: Output parameter. Pointer to buffer that will contain the PNG image data. + Must be freed after usage with free(*out). +outsize: Output parameter. Pointer to the size in bytes of the out buffer. +image: The raw pixel data to encode. The size of this buffer should be + w * h * (bytes per pixel), bytes per pixel depends on colortype and bitdepth. +w: width of the raw pixel data in pixels. +h: height of the raw pixel data in pixels. +colortype: the color type of the raw input image. See explanation on PNG color types. +bitdepth: the bit depth of the raw input image. See explanation on PNG color types. +Return value: LodePNG error code (0 means no error). +*/ +unsigned lodepng_encode_memory(unsigned char** out, size_t* outsize, + const unsigned char* image, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth); + +/*Same as lodepng_encode_memory, but always encodes from 32-bit RGBA raw image.*/ +unsigned lodepng_encode32(unsigned char** out, size_t* outsize, + const unsigned char* image, unsigned w, unsigned h); + +/*Same as lodepng_encode_memory, but always encodes from 24-bit RGB raw image.*/ +unsigned lodepng_encode24(unsigned char** out, size_t* outsize, + const unsigned char* image, unsigned w, unsigned h); + +#ifdef LODEPNG_COMPILE_DISK +/* +Converts raw pixel data into a PNG file on disk. +Same as the other encode functions, but instead takes a filename as output. + +NOTE: This overwrites existing files without warning! + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory.*/ +unsigned lodepng_encode_file(const char* filename, + const unsigned char* image, unsigned w, unsigned h, + LodePNGColorType colortype, unsigned bitdepth); + +/*Same as lodepng_encode_file, but always encodes from 32-bit RGBA raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory.*/ +unsigned lodepng_encode32_file(const char* filename, + const unsigned char* image, unsigned w, unsigned h); + +/*Same as lodepng_encode_file, but always encodes from 24-bit RGB raw image. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory.*/ +unsigned lodepng_encode24_file(const char* filename, + const unsigned char* image, unsigned w, unsigned h); +#endif /*LODEPNG_COMPILE_DISK*/ +#endif /*LODEPNG_COMPILE_ENCODER*/ + + +#ifdef LODEPNG_COMPILE_CPP +namespace lodepng { +#ifdef LODEPNG_COMPILE_DECODER +/*Same as lodepng_decode_memory, but decodes to an std::vector. The colortype +is the format to output the pixels to. Default is RGBA 8-bit per channel.*/ +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + const unsigned char* in, size_t insize, + LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + const std::vector& in, + LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); +#ifdef LODEPNG_COMPILE_DISK +/* +Converts PNG file from disk to raw pixel data in memory. +Same as the other decode functions, but instead takes a filename as input. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory. +*/ +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + const std::string& filename, + LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_DECODER */ + +#ifdef LODEPNG_COMPILE_ENCODER +/*Same as lodepng_encode_memory, but encodes to an std::vector. colortype +is that of the raw input data. The output PNG color type will be auto chosen.*/ +unsigned encode(std::vector& out, + const unsigned char* in, unsigned w, unsigned h, + LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); +unsigned encode(std::vector& out, + const std::vector& in, unsigned w, unsigned h, + LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); +#ifdef LODEPNG_COMPILE_DISK +/* +Converts 32-bit RGBA raw pixel data into a PNG file on disk. +Same as the other encode functions, but instead takes a filename as output. + +NOTE: This overwrites existing files without warning! + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory. +*/ +unsigned encode(const std::string& filename, + const unsigned char* in, unsigned w, unsigned h, + LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); +unsigned encode(const std::string& filename, + const std::vector& in, unsigned w, unsigned h, + LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_ENCODER */ +} /* namespace lodepng */ +#endif /*LODEPNG_COMPILE_CPP*/ +#endif /*LODEPNG_COMPILE_PNG*/ + +#ifdef LODEPNG_COMPILE_ERROR_TEXT +/*Returns an English description of the numerical error code.*/ +const char* lodepng_error_text(unsigned code); +#endif /*LODEPNG_COMPILE_ERROR_TEXT*/ + +#ifdef LODEPNG_COMPILE_DECODER +/*Settings for zlib decompression*/ +typedef struct LodePNGDecompressSettings LodePNGDecompressSettings; +struct LodePNGDecompressSettings { + /* Check LodePNGDecoderSettings for more ignorable errors such as ignore_crc */ + unsigned ignore_adler32; /*if 1, continue and don't give an error message if the Adler32 checksum is corrupted*/ + unsigned ignore_nlen; /*ignore complement of len checksum in uncompressed blocks*/ + + /*Maximum decompressed size, beyond this the decoder may (and is encouraged to) stop decoding, + return an error, output a data size > max_output_size and all the data up to that point. This is + not hard limit nor a guarantee, but can prevent excessive memory usage. This setting is + ignored by the PNG decoder, but is used by the deflate/zlib decoder and can be used by custom ones. + Set to 0 to impose no limit (the default).*/ + size_t max_output_size; + + /*use custom zlib decoder instead of built in one (default: null). + Should return 0 if success, any non-0 if error (numeric value not exposed).*/ + unsigned (*custom_zlib)(unsigned char**, size_t*, + const unsigned char*, size_t, + const LodePNGDecompressSettings*); + /*use custom deflate decoder instead of built in one (default: null) + if custom_zlib is not null, custom_inflate is ignored (the zlib format uses deflate). + Should return 0 if success, any non-0 if error (numeric value not exposed).*/ + unsigned (*custom_inflate)(unsigned char**, size_t*, + const unsigned char*, size_t, + const LodePNGDecompressSettings*); + + const void* custom_context; /*optional custom settings for custom functions*/ +}; + +extern const LodePNGDecompressSettings lodepng_default_decompress_settings; +void lodepng_decompress_settings_init(LodePNGDecompressSettings* settings); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/* +Settings for zlib compression. Tweaking these settings tweaks the balance +between speed and compression ratio. +*/ +typedef struct LodePNGCompressSettings LodePNGCompressSettings; +struct LodePNGCompressSettings /*deflate = compress*/ { + /*LZ77 related settings*/ + unsigned btype; /*the block type for LZ (0, 1, 2 or 3, see zlib standard). Should be 2 for proper compression.*/ + unsigned use_lz77; /*whether or not to use LZ77. Should be 1 for proper compression.*/ + unsigned windowsize; /*must be a power of two <= 32768. higher compresses more but is slower. Default value: 2048.*/ + unsigned minmatch; /*minimum lz77 length. 3 is normally best, 6 can be better for some PNGs. Default: 0*/ + unsigned nicematch; /*stop searching if >= this length found. Set to 258 for best compression. Default: 128*/ + unsigned lazymatching; /*use lazy matching: better compression but a bit slower. Default: true*/ + + /*use custom zlib encoder instead of built in one (default: null)*/ + unsigned (*custom_zlib)(unsigned char**, size_t*, + const unsigned char*, size_t, + const LodePNGCompressSettings*); + /*use custom deflate encoder instead of built in one (default: null) + if custom_zlib is used, custom_deflate is ignored since only the built in + zlib function will call custom_deflate*/ + unsigned (*custom_deflate)(unsigned char**, size_t*, + const unsigned char*, size_t, + const LodePNGCompressSettings*); + + const void* custom_context; /*optional custom settings for custom functions*/ +}; + +extern const LodePNGCompressSettings lodepng_default_compress_settings; +void lodepng_compress_settings_init(LodePNGCompressSettings* settings); +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_PNG +/* +Color mode of an image. Contains all information required to decode the pixel +bits to RGBA colors. This information is the same as used in the PNG file +format, and is used both for PNG and raw image data in LodePNG. +*/ +typedef struct LodePNGColorMode { + /*header (IHDR)*/ + LodePNGColorType colortype; /*color type, see PNG standard or documentation further in this header file*/ + unsigned bitdepth; /*bits per sample, see PNG standard or documentation further in this header file*/ + + /* + palette (PLTE and tRNS) + + Dynamically allocated with the colors of the palette, including alpha. + This field may not be allocated directly, use lodepng_color_mode_init first, + then lodepng_palette_add per color to correctly initialize it (to ensure size + of exactly 1024 bytes). + + The alpha channels must be set as well, set them to 255 for opaque images. + + When decoding, with the default settings you can ignore this palette, since + LodePNG already fills the palette colors in the pixels of the raw RGBA output, + but when decoding to the original PNG color mode it is needed to reconstruct + the colors. + + The palette is only supported for color type 3. + */ + unsigned char* palette; /*palette in RGBARGBA... order. Must be either 0, or when allocated must have 1024 bytes*/ + size_t palettesize; /*palette size in number of colors (amount of used bytes is 4 * palettesize)*/ + + /* + transparent color key (tRNS) + + This color uses the same bit depth as the bitdepth value in this struct, which can be 1-bit to 16-bit. + For grayscale PNGs, r, g and b will all 3 be set to the same. + + When decoding, by default you can ignore this information, since LodePNG sets + pixels with this key to transparent already in the raw RGBA output. + + The color key is only supported for color types 0 and 2. + */ + unsigned key_defined; /*is a transparent color key given? 0 = false, 1 = true*/ + unsigned key_r; /*red/grayscale component of color key*/ + unsigned key_g; /*green component of color key*/ + unsigned key_b; /*blue component of color key*/ +} LodePNGColorMode; + +/*init, cleanup and copy functions to use with this struct*/ +void lodepng_color_mode_init(LodePNGColorMode* info); +void lodepng_color_mode_cleanup(LodePNGColorMode* info); +/*return value is error code (0 means no error)*/ +unsigned lodepng_color_mode_copy(LodePNGColorMode* dest, const LodePNGColorMode* source); +/* Makes a temporary LodePNGColorMode that does not need cleanup (no palette) */ +LodePNGColorMode lodepng_color_mode_make(LodePNGColorType colortype, unsigned bitdepth); + +void lodepng_palette_clear(LodePNGColorMode* info); +/*add 1 color to the palette*/ +unsigned lodepng_palette_add(LodePNGColorMode* info, + unsigned char r, unsigned char g, unsigned char b, unsigned char a); + +/*get the total amount of bits per pixel, based on colortype and bitdepth in the struct*/ +unsigned lodepng_get_bpp(const LodePNGColorMode* info); +/*get the amount of color channels used, based on colortype in the struct. +If a palette is used, it counts as 1 channel.*/ +unsigned lodepng_get_channels(const LodePNGColorMode* info); +/*is it a grayscale type? (only colortype 0 or 4)*/ +unsigned lodepng_is_greyscale_type(const LodePNGColorMode* info); +/*has it got an alpha channel? (only colortype 2 or 6)*/ +unsigned lodepng_is_alpha_type(const LodePNGColorMode* info); +/*has it got a palette? (only colortype 3)*/ +unsigned lodepng_is_palette_type(const LodePNGColorMode* info); +/*only returns true if there is a palette and there is a value in the palette with alpha < 255. +Loops through the palette to check this.*/ +unsigned lodepng_has_palette_alpha(const LodePNGColorMode* info); +/* +Check if the given color info indicates the possibility of having non-opaque pixels in the PNG image. +Returns true if the image can have translucent or invisible pixels (it still be opaque if it doesn't use such pixels). +Returns false if the image can only have opaque pixels. +In detail, it returns true only if it's a color type with alpha, or has a palette with non-opaque values, +or if "key_defined" is true. +*/ +unsigned lodepng_can_have_alpha(const LodePNGColorMode* info); +/*Returns the byte size of a raw image buffer with given width, height and color mode*/ +size_t lodepng_get_raw_size(unsigned w, unsigned h, const LodePNGColorMode* color); + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +/*The information of a Time chunk in PNG.*/ +typedef struct LodePNGTime { + unsigned year; /*2 bytes used (0-65535)*/ + unsigned month; /*1-12*/ + unsigned day; /*1-31*/ + unsigned hour; /*0-23*/ + unsigned minute; /*0-59*/ + unsigned second; /*0-60 (to allow for leap seconds)*/ +} LodePNGTime; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +/*Information about the PNG image, except pixels, width and height.*/ +typedef struct LodePNGInfo { + /*header (IHDR), palette (PLTE) and transparency (tRNS) chunks*/ + unsigned compression_method;/*compression method of the original file. Always 0.*/ + unsigned filter_method; /*filter method of the original file*/ + unsigned interlace_method; /*interlace method of the original file: 0=none, 1=Adam7*/ + LodePNGColorMode color; /*color type and bits, palette and transparency of the PNG file*/ + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /* + Suggested background color chunk (bKGD) + + This uses the same color mode and bit depth as the PNG (except no alpha channel), + with values truncated to the bit depth in the unsigned integer. + + For grayscale and palette PNGs, the value is stored in background_r. The values + in background_g and background_b are then unused. The decoder will set them + equal to background_r, the encoder ignores them in this case. + + When decoding, you may get these in a different color mode than the one you requested + for the raw pixels: the colortype and bitdepth defined by info_png.color, that is the + ones defined in the header of the PNG image, are used. + + When encoding with auto_convert, you must use the color model defined in info_png.color for + these values. The encoder normally ignores info_png.color when auto_convert is on, but will + use it to interpret these values (and convert copies of them to its chosen color model). + + When encoding, avoid setting this to an expensive color, such as a non-gray value + when the image is gray, or the compression will be worse since it will be forced to + write the PNG with a more expensive color mode (when auto_convert is on). + + The decoder does not use this background color to edit the color of pixels. This is a + completely optional metadata feature. + */ + unsigned background_defined; /*is a suggested background color given?*/ + unsigned background_r; /*red/gray/palette component of suggested background color*/ + unsigned background_g; /*green component of suggested background color*/ + unsigned background_b; /*blue component of suggested background color*/ + + /* + Non-international text chunks (tEXt and zTXt) + + The char** arrays each contain num strings. The actual messages are in + text_strings, while text_keys are keywords that give a short description what + the actual text represents, e.g. Title, Author, Description, or anything else. + + All the string fields below including strings, keys, names and language tags are null terminated. + The PNG specification uses null characters for the keys, names and tags, and forbids null + characters to appear in the main text which is why we can use null termination everywhere here. + + A keyword is minimum 1 character and maximum 79 characters long (plus the + additional null terminator). It's discouraged to use a single line length + longer than 79 characters for texts. + + Don't allocate these text buffers yourself. Use the init/cleanup functions + correctly and use lodepng_add_text and lodepng_clear_text. + + Standard text chunk keywords and strings are encoded using Latin-1. + */ + size_t text_num; /*the amount of texts in these char** buffers (there may be more texts in itext)*/ + char** text_keys; /*the keyword of a text chunk (e.g. "Comment")*/ + char** text_strings; /*the actual text*/ + + /* + International text chunks (iTXt) + Similar to the non-international text chunks, but with additional strings + "langtags" and "transkeys", and the following text encodings are used: + keys: Latin-1, langtags: ASCII, transkeys and strings: UTF-8. + keys must be 1-79 characters (plus the additional null terminator), the other + strings are any length. + */ + size_t itext_num; /*the amount of international texts in this PNG*/ + char** itext_keys; /*the English keyword of the text chunk (e.g. "Comment")*/ + char** itext_langtags; /*language tag for this text's language, ISO/IEC 646 string, e.g. ISO 639 language tag*/ + char** itext_transkeys; /*keyword translated to the international language - UTF-8 string*/ + char** itext_strings; /*the actual international text - UTF-8 string*/ + + /* + Optional exif metadata in exif_size bytes. + Don't allocate this buffer yourself. Use the init/cleanup functions + correctly and use lodepng_set_exif and lodepng_clear_exif. + The exif data is in exif-encoded form but without JPEG markers, starting with the 'II' or 'MM' marker that indicates + endianness. It's up to an exif handling library to encode/decode its information. + */ + unsigned exif_defined; /* Whether exif metadata is present, that is, the PNG image has an eXIf chunk */ + unsigned char* exif; /* The bytes of the exif metadata, if present */ + unsigned exif_size; /* The size of the exif data in bytes */ + + + /*time chunk (tIME)*/ + unsigned time_defined; /*set to 1 to make the encoder generate a tIME chunk*/ + LodePNGTime time; + + /*phys chunk (pHYs)*/ + unsigned phys_defined; /*if 0, there is no pHYs chunk and the values below are undefined, if 1 else there is one*/ + unsigned phys_x; /*pixels per unit in x direction*/ + unsigned phys_y; /*pixels per unit in y direction*/ + unsigned phys_unit; /*may be 0 (unknown unit) or 1 (metre)*/ + + /* + Color profile related chunk types: cICP, iCPP, sRGB, gAMA, cHRM, sBIT + + LodePNG does not apply any color conversions on pixels in the encoder or decoder and does not interpret these color + profile values. It merely passes on the information. If you wish to use color profiles and convert colors, a separate + color management library should be used. There is also a limited library for this in lodepng_util.h. + + There are 4 types of (sets of) chunks providing color information. If multiple are present, each will be decoded by + LodePNG, but only one should be handled by the user, with the following order of priority depending on what the user + supports: + 1: cICP: Coding-independent code points (CICP) + 2: iCCP: ICC profile + 3: sRGB: indicates the image is in the sRGB color profile + 4: gAMA and cHRM: indicates a gamma and chromaticity value to define the color profile + */ + + /* + gAMA chunk: Image gamma + Optional, overridden by cICP, iCCP or sRGB if those are present. + Together with cHRM, this is a primitive way of specifying the image color profile. + */ + unsigned gama_defined; /* Whether a gAMA chunk is present (0 = not present, 1 = present). */ + unsigned gama_gamma; /* Gamma exponent times 100000 */ + + /* + cHRM chunk: Primary chromaticities and white point + Optional, overridden by cICP, iCCP or sRGB if those are present. + Together with gAMA, this is a primitive way of specifying the image color profile. + */ + unsigned chrm_defined; /* Whether a cHRM chunk is present (0 = not present, 1 = present). */ + unsigned chrm_white_x; /* White Point x times 100000 */ + unsigned chrm_white_y; /* White Point y times 100000 */ + unsigned chrm_red_x; /* Red x times 100000 */ + unsigned chrm_red_y; /* Red y times 100000 */ + unsigned chrm_green_x; /* Green x times 100000 */ + unsigned chrm_green_y; /* Green y times 100000 */ + unsigned chrm_blue_x; /* Blue x times 100000 */ + unsigned chrm_blue_y; /* Blue y times 100000 */ + + /* + sRGB chunk: Indicates the image is in the sRGB color space. + Optional. Should not appear at the same time as iCCP. + If gAMA is also present gAMA must contain value 45455. + If cHRM is also present cHRM must contain respectively 31270,32900,64000,33000,30000,60000,15000,6000. + */ + unsigned srgb_defined; /* Whether an sRGB chunk is present (0 = not present, 1 = present). */ + unsigned srgb_intent; /* Rendering intent: 0=perceptual, 1=rel. colorimetric, 2=saturation, 3=abs. colorimetric */ + + /* + iCCP chunk: Embedded ICC profile. + Optional. Should not appear at the same time as sRGB. + + Contains ICC profile, which can use any version of the ICC.1 specification by the International Color Consortium. See + its specification for more details. LodePNG does not parse or use the ICC profile (except its color space header + field for "RGB" or "GRAY", see below), a separate library to handle the ICC data format is needed to use it for color + management and conversions. + + For encoding, if iCCP is present, the PNG specification recommends to also add gAMA and cHRM chunks that approximate + the ICC profile, for compatibility with applications that don't use the ICC chunk. This is not required, and it's up + to the user to compute approximate values and set then in the appropriate gama_ and chrm_ fields, LodePNG does not do + this automatically since it does not interpret the ICC profile. + + For encoding, the ICC profile is required by the PNG specification to be an "RGB" profile for non-gray PNG color + types (types 2, 3 and 6) and a "GRAY" profile for gray PNG color types (types 1 and 4). If you disable auto_convert, + you must ensure the ICC profile type matches your requested color type, else the encoder gives an error. If + auto_convert is enabled (the default), and the ICC profile is not a correct match for the pixel data, this will result + in an encoder error if the pixel data has non-gray pixels for a GRAY profile, or a silent less-optimal compression of + the pixel data if the pixels could be encoded as grayscale but the ICC profile is RGB. + + To avoid this do not set an ICC profile in the image unless there is a good reason for it, and when doing so + make sure you compute it carefully to avoid the above problems. + */ + unsigned iccp_defined; /* Whether an iCCP chunk is present (0 = not present, 1 = present). */ + char* iccp_name; /* Null terminated string with profile name, 1-79 bytes */ + /* + The ICC profile in iccp_profile_size bytes. + Don't allocate this buffer yourself. Use the init/cleanup functions + correctly and use lodepng_set_icc and lodepng_clear_icc. + */ + unsigned char* iccp_profile; + unsigned iccp_profile_size; /* The size of iccp_profile in bytes */ + + /* + cICP chunk: Coding-independent code points for video signal type identification. + Optional. If present, and supported, overrides iCCP, sRGB, gAMA and cHRM. + The meaning of the values are as defined in the specification ITU-T-H.273. LodePNG does not + use these values, only passes on the metadata. The meaning of the values is they are enum + values representing certain color spaces, including HDR color spaces, such as Display P3, + PQ and HLG. The video full range flag value should typically be 1 for the use cases of PNG + images, but can be 0 for narrow-range images in certain video editing workflows. + */ + unsigned cicp_defined; /* Whether an cICP chunk is present (0 = not present, 1 = present). */ + unsigned cicp_color_primaries; /* Colour primaries value */ + unsigned cicp_transfer_function; /* Transfer characteristics value */ + unsigned cicp_matrix_coefficients; /* Matrix coefficients value */ + unsigned cicp_video_full_range_flag; /* Video full range flag value */ + + /* + mDCV chunk: Mastering Display Color Volume. + Optional, typically used in conjunction with certain HDR color spaces that can + be represented by the cICP chunk. + See the PNG specification, third edition, for more information on this chunk. + All the red, green, blue and white x and y values are encoded as 16-bit + integers and therefore must be in range 0-65536. The min and max luminance + values are 32-bit integers. + */ + unsigned mdcv_defined; /* Whether an mDCV chunk is present (0 = not present, 1 = present). */ + /* Mastering display color primary chromaticities (CIE 1931 x,y of R,G,B) */ + unsigned mdcv_red_x; /* Red x times 50000 */ + unsigned mdcv_red_y; /* Red y times 50000 */ + unsigned mdcv_green_x; /* Green x times 50000 */ + unsigned mdcv_green_y; /* Green y times 50000 */ + unsigned mdcv_blue_x; /* Blue x times 50000 */ + unsigned mdcv_blue_y; /* Blue y times 50000 */ + /* Mastering display white point chromaticity (CIE 1931 x,y) */ + unsigned mdcv_white_x; /* White Point x times 50000 */ + unsigned mdcv_white_y; /* White Point y times 50000 */ + /* Mastering display luminance */ + unsigned mdcv_max_luminance; /* Max luminance in cd/m^2 times 10000 */ + unsigned mdcv_min_luminance; /* Min luminance in cd/m^2 times 10000 */ + + /* + cLLI chunk: Content Light Level Information. + Optional, typically used in conjunction with certain HDR color spaces that can + be represented by the cICP chunk. + See the PNG specification, third edition, for more information on this chunk. + The clli_max_cll and clli_max_fall values are 32-bit integers. + */ + unsigned clli_defined; /* Whether a cLLI chunk is present (0 = not present, 1 = present). */ + unsigned clli_max_cll; /* Maximum Content Light Level (MaxCLL) in cd/m^2 times 10000 */ + unsigned clli_max_fall; /* Maximum Frame-Average Light Level (MaxFALL) in cd/m^2 times 10000 */ + + /* + sBIT chunk: significant bits. + Optional metadata, only set this if needed. + + If defined, these values give the bit depth of the original data. Since PNG only stores 1, 2, 4, 8 or 16-bit + per channel data, the significant bits value can be used to indicate the original encoded data has another + sample depth, such as 10 or 12. + + Encoders using this value, when storing the pixel data, should use the most significant bits + of the data to store the original bits, and use a good sample depth scaling method such as + "left bit replication" to fill in the least significant bits, rather than fill zeroes. + + Decoders using this value, if able to work with data that's e.g. 10-bit or 12-bit, should right + shift the data to go back to the original bit depth, but decoders are also allowed to ignore + sbit and work e.g. with the 8-bit or 16-bit data from the PNG directly, since thanks + to the encoder contract, the values encoded in PNG are in valid range for the PNG bit depth. + + For grayscale images, sbit_g and sbit_b are not used, and for images that don't use color + type RGBA or grayscale+alpha, sbit_a is not used (it's not used even for palette images with + translucent palette values, or images with color key). The values that are used must be + greater than zero and smaller than or equal to the PNG bit depth. + + The color type from the header in the PNG image defines these used and unused fields: if + decoding with a color mode conversion, such as always decoding to RGBA, this metadata still + only uses the color type of the original PNG, and may e.g. lack the alpha channel info + if the PNG was RGB. When encoding with auto_convert (as well as without), also always the + color model defined in info_png.color determines this. + + NOTE: enabling sbit can hurt compression, because the encoder can then not always use + auto_convert to choose a more optimal color mode for the data, because the PNG format has + strict requirements for the allowed sbit values in combination with color modes. + For example, setting these fields to 10-bit will force the encoder to keep using a 16-bit per channel + color mode, even if the pixel data would in fact fit in a more efficient 8-bit mode. + */ + unsigned sbit_defined; /*is significant bits given? if not, the values below are unused*/ + unsigned sbit_r; /*red or gray component of significant bits*/ + unsigned sbit_g; /*green component of significant bits*/ + unsigned sbit_b; /*blue component of significant bits*/ + unsigned sbit_a; /*alpha component of significant bits*/ + + /* End of color profile related chunks */ + + + /* + unknown chunks: chunks not known by LodePNG, passed on byte for byte. + + There are 3 buffers, one for each position in the PNG where unknown chunks can appear. + Each buffer contains all unknown chunks for that position consecutively. + The 3 positions are: + 0: between IHDR and PLTE, 1: between PLTE and IDAT, 2: between IDAT and IEND. + + For encoding, do not store critical chunks or known chunks that are enabled with a "_defined" flag + above in here, since the encoder will blindly follow this and could then encode an invalid PNG file + (such as one with two IHDR chunks or the disallowed combination of sRGB with iCCP). But do use + this if you wish to store an ancillary chunk that is not supported by LodePNG (such as sPLT or hIST), + or any non-standard PNG chunk. + + Do not allocate or traverse this data yourself. Use the chunk traversing functions declared + later, such as lodepng_chunk_next and lodepng_chunk_append, to read/write this struct. + */ + unsigned char* unknown_chunks_data[3]; + size_t unknown_chunks_size[3]; /*size in bytes of the unknown chunks, given for protection*/ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} LodePNGInfo; + +/*init, cleanup and copy functions to use with this struct*/ +void lodepng_info_init(LodePNGInfo* info); +/*destructs the LodePNGInfo and brings it to invalid state, requiring lodepng_info_init again before reusing it*/ +void lodepng_info_cleanup(LodePNGInfo* info); +/*return value is error code (0 means no error)*/ +unsigned lodepng_info_copy(LodePNGInfo* dest, const LodePNGInfo* source); + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS +unsigned lodepng_add_text(LodePNGInfo* info, const char* key, const char* str); /*push back both texts at once*/ +void lodepng_clear_text(LodePNGInfo* info); /*use this to clear the texts again after you filled them in*/ + +unsigned lodepng_add_itext(LodePNGInfo* info, const char* key, const char* langtag, + const char* transkey, const char* str); /*push back the 4 texts of 1 chunk at once*/ +void lodepng_clear_itext(LodePNGInfo* info); /*use this to clear the itexts again after you filled them in*/ + +/*replaces if exists*/ +unsigned lodepng_set_icc(LodePNGInfo* info, const char* name, const unsigned char* profile, unsigned profile_size); +void lodepng_clear_icc(LodePNGInfo* info); /*use this to clear the profile again after you filled it in*/ + +/*replaces if exists*/ +unsigned lodepng_set_exif(LodePNGInfo* info, const unsigned char* exif, unsigned exif_size); +void lodepng_clear_exif(LodePNGInfo* info); /*use this to clear the exif metadata again after you filled it in*/ +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ + +/* +Converts raw buffer from one color type to another color type, based on +LodePNGColorMode structs to describe the input and output color type. +See the reference manual at the end of this header file to see which color conversions are supported. +return value = LodePNG error code (0 if all went ok, an error if the conversion isn't supported) +The out buffer must have size (w * h * bpp + 7) / 8, where bpp is the bits per pixel +of the output color type (lodepng_get_bpp). +For < 8 bpp images, there should not be padding bits at the end of scanlines. +For 16-bit per channel colors, uses big endian format like PNG does. +Return value is LodePNG error code +*/ +unsigned lodepng_convert(unsigned char* out, const unsigned char* in, + const LodePNGColorMode* mode_out, const LodePNGColorMode* mode_in, + unsigned w, unsigned h); + +#ifdef LODEPNG_COMPILE_DECODER +/* +Settings for the decoder. This contains settings for the PNG and the Zlib +decoder, but not the Info settings from the Info structs. +*/ +typedef struct LodePNGDecoderSettings { + LodePNGDecompressSettings zlibsettings; /*in here is the setting to ignore Adler32 checksums*/ + + /* Check LodePNGDecompressSettings for more ignorable errors such as ignore_adler32 */ + unsigned ignore_crc; /*ignore CRC checksums*/ + unsigned ignore_critical; /*ignore unknown critical chunks*/ + unsigned ignore_end; /*ignore issues at end of file if possible (missing IEND chunk, too large chunk, ...)*/ + /* TODO: make a system involving warnings with levels and a strict mode instead. Other potentially recoverable + errors: srgb rendering intent value, size of content of ancillary chunks, more than 79 characters for some + strings, placement/combination rules for ancillary chunks, crc of unknown chunks, allowed characters + in string keys, invalid characters in chunk types names, etc... */ + + unsigned color_convert; /*whether to convert the PNG to the color type you want. Default: yes*/ + +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + unsigned read_text_chunks; /*if false but remember_unknown_chunks is true, they're stored in the unknown chunks*/ + + /*store all bytes from unknown chunks in the LodePNGInfo (off by default, useful for a png editor)*/ + unsigned remember_unknown_chunks; + + /* maximum size for decompressed text chunks. If a text chunk's text is larger than this, an error is returned, + unless reading text chunks is disabled or this limit is set higher or disabled. Set to 0 to allow any size. + By default it is a value that prevents unreasonably large strings from hogging memory. */ + size_t max_text_size; + + /* maximum size for compressed ICC chunks. If the ICC profile is larger than this, an error will be returned. Set to + 0 to allow any size. By default this is a value that prevents ICC profiles that would be much larger than any + legitimate profile could be to hog memory. */ + size_t max_icc_size; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} LodePNGDecoderSettings; + +void lodepng_decoder_settings_init(LodePNGDecoderSettings* settings); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/*strategy to use to choose the PNG filter per scanline. Strategies 0-4 correspond +to each of the 5 filter types PNG supports, the next values are adaptive strategies*/ +typedef enum LodePNGFilterStrategy { + /*every filter at zero*/ + LFS_ZERO = 0, + /*every filter at 1, 2, 3 or 4 (paeth), unlike LFS_ZERO not a good choice, but for testing*/ + LFS_ONE = 1, + LFS_TWO = 2, + LFS_THREE = 3, + LFS_FOUR = 4, + /*Use the filter out of the 5 above types that gives minimum sum, by trying each one. This is the adaptive filtering + suggested heuristic in the PNG standard chapter 'Filter selection'.*/ + LFS_MINSUM, + /*Use the filter type that gives smallest Shannon entropy for this scanline. Depending + on the image, this is better or worse than minsum.*/ + LFS_ENTROPY, + /* + Brute-force-search PNG filters by compressing each filter for each scanline. + Experimental, very slow, and only rarely gives better compression than MINSUM. + */ + LFS_BRUTE_FORCE, + /*use predefined_filters buffer: you specify the filter type for each scanline*/ + LFS_PREDEFINED +} LodePNGFilterStrategy; + +/*Gives characteristics about the integer RGBA colors of the image (count, alpha channel usage, bit depth, ...), +which helps decide which color model to use for encoding. +Used internally by default if "auto_convert" is enabled. Public because it's useful for custom algorithms.*/ +typedef struct LodePNGColorStats { + unsigned colored; /*not grayscale*/ + unsigned key; /*image is not opaque and color key is possible instead of full alpha*/ + unsigned short key_r; /*key values, always as 16-bit, in 8-bit case the byte is duplicated, e.g. 65535 means 255*/ + unsigned short key_g; + unsigned short key_b; + unsigned alpha; /*image is not opaque and alpha channel or alpha palette required*/ + unsigned numcolors; /*amount of colors, up to 257. Not valid if bits == 16 or allow_palette is disabled.*/ + unsigned char palette[1024]; /*Remembers up to the first 256 RGBA colors, in no particular order, only valid when numcolors is valid*/ + unsigned bits; /*bits per channel (not for palette). 1,2 or 4 for grayscale only. 16 if 16-bit per channel required.*/ + size_t numpixels; + + /*user settings for computing/using the stats*/ + unsigned allow_palette; /*default 1. if 0, disallow choosing palette colortype in auto_choose_color, and don't count numcolors*/ + unsigned allow_greyscale; /*default 1. if 0, choose RGB or RGBA even if the image only has gray colors*/ +} LodePNGColorStats; + +void lodepng_color_stats_init(LodePNGColorStats* stats); + +/*Get a LodePNGColorStats of the image. The stats must already have been inited. +Returns error code (e.g. alloc fail) or 0 if ok.*/ +unsigned lodepng_compute_color_stats(LodePNGColorStats* stats, + const unsigned char* image, unsigned w, unsigned h, + const LodePNGColorMode* mode_in); + +/*Settings for the encoder.*/ +typedef struct LodePNGEncoderSettings { + LodePNGCompressSettings zlibsettings; /*settings for the zlib encoder, such as window size, ...*/ + + /*automatically choose output PNG color type. If false, must explicitly choose the output color + type in state.info_png.color.colortype, info_png.color.bitdepth and optionally its palette. + Default: true*/ + unsigned auto_convert; + + /*If true, follows the suggestion in the PNG standard in chapter 'Filter selection': if the PNG uses + a palette or lower than 8 bit depth, set all filters to zero. + In other cases this will use the heuristic from the chosen filter_strategy. The PNG standard + suggests LFS_MINSUM for those cases.*/ + unsigned filter_palette_zero; + /*Which filter strategy to use when not using zeroes due to filter_palette_zero. + Set filter_palette_zero to 0 to ensure always using your chosen strategy. Default: LFS_MINSUM*/ + LodePNGFilterStrategy filter_strategy; + /*used if filter_strategy is LFS_PREDEFINED. In that case, this must point to a buffer with + the same length as the amount of scanlines in the image, and each value must <= 5. You + have to cleanup this buffer, LodePNG will never free it. Don't forget that filter_palette_zero + must be set to 0 to ensure this is also used on palette or low bitdepth images.*/ + const unsigned char* predefined_filters; + + /*force creating a PLTE chunk if colortype is 2 or 6 (= a suggested palette). + If colortype is 3, PLTE is always created. If color type is explicitly set + to a grayscale type (1 or 4), this is not done and is ignored. If enabling this, + a palette must be present in the info_png. + NOTE: enabling this may worsen compression if auto_convert is used to choose + optimal color mode, because it cannot use grayscale color modes in this case*/ + unsigned force_palette; +#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS + /*add LodePNG identifier and version as a text chunk, for debugging*/ + unsigned add_id; + /*encode text chunks as zTXt chunks instead of tEXt chunks, and use compression in iTXt chunks*/ + unsigned text_compression; +#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ +} LodePNGEncoderSettings; + +void lodepng_encoder_settings_init(LodePNGEncoderSettings* settings); +#endif /*LODEPNG_COMPILE_ENCODER*/ + + +#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) +/*The settings, state and information for extended encoding and decoding. + +Using this struct requires using lodepng_state_init to initialize it +and using lodepng_state_cleanup to deconstruct it. If using C++, you can +use lodepng::State instead which does those things automatically with RAII. + +While a LodePNGState can be reused once in a chain of lodepng_decode followed by +lodepng_encode, it's not recommended to reuse it for multiple encode, decode +or inspect calls, and if any such function returns an error code, the +LodePNGState should not be reused at all as it can be in an unexpected state.. +*/ +typedef struct LodePNGState { +#ifdef LODEPNG_COMPILE_DECODER + LodePNGDecoderSettings decoder; /*the decoding settings*/ +#endif /*LODEPNG_COMPILE_DECODER*/ +#ifdef LODEPNG_COMPILE_ENCODER + LodePNGEncoderSettings encoder; /*the encoding settings*/ +#endif /*LODEPNG_COMPILE_ENCODER*/ + LodePNGColorMode info_raw; /*specifies the format in which you would like to get the raw pixel buffer*/ + LodePNGInfo info_png; /*info of the PNG image obtained after decoding*/ + unsigned error; /*deprecated, use the return value of the encode/decode functions to check errors instead*/ +} LodePNGState; + +/*init, cleanup and copy functions to use with this struct*/ +void lodepng_state_init(LodePNGState* state); +/*destructs the LodePNGState and brings it to invalid state, requiring lodepng_info_init again before reusing it*/ +void lodepng_state_cleanup(LodePNGState* state); +/*return value is error code (0 means no error)*/ +unsigned lodepng_state_copy(LodePNGState* dest, const LodePNGState* source); +#endif /* defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) */ + +#ifdef LODEPNG_COMPILE_DECODER +/* +Same as lodepng_decode_memory, but uses a LodePNGState to allow custom settings and +getting much more information about the PNG image and color mode. +*/ +unsigned lodepng_decode(unsigned char** out, unsigned* w, unsigned* h, + LodePNGState* state, + const unsigned char* in, size_t insize); + +/* +Read the PNG header, but not the actual data. This returns only the information +that is in the IHDR chunk of the PNG, such as width, height and color type. The +information is placed in the info_png field of the LodePNGState. +*/ +unsigned lodepng_inspect(unsigned* w, unsigned* h, + LodePNGState* state, + const unsigned char* in, size_t insize); +#endif /*LODEPNG_COMPILE_DECODER*/ + +/* +Reads one metadata chunk (other than IHDR, which is handled by lodepng_inspect) +of the PNG file and outputs what it read in the state. Returns error code on failure. +Use lodepng_inspect first with a new state, then e.g. lodepng_chunk_find_const +to find the desired chunk type, and if non null use lodepng_inspect_chunk (with +chunk_pointer - start_of_file as pos). +Supports most metadata chunks from the PNG standard (gAMA, bKGD, tEXt, ...). +Ignores unsupported, unknown, non-metadata or IHDR chunks (without error). +Requirements: &in[pos] must point to start of a chunk, must use regular +lodepng_inspect first since format of most other chunks depends on IHDR, and if +there is a PLTE chunk, that one must be inspected before tRNS or bKGD. +*/ +unsigned lodepng_inspect_chunk(LodePNGState* state, size_t pos, + const unsigned char* in, size_t insize); + +#ifdef LODEPNG_COMPILE_ENCODER +/*This function allocates the out buffer with standard malloc and stores the size in *outsize.*/ +unsigned lodepng_encode(unsigned char** out, size_t* outsize, + const unsigned char* image, unsigned w, unsigned h, + LodePNGState* state); +#endif /*LODEPNG_COMPILE_ENCODER*/ + +/* +The lodepng_chunk functions are normally not needed, except to traverse the +unknown chunks stored in the LodePNGInfo struct, or add new ones to it. +It also allows traversing the chunks of an encoded PNG file yourself. + +The chunk pointer always points to the beginning of the chunk itself, that is +the first byte of the 4 length bytes. + +In the PNG file format, chunks have the following format: +-4 bytes length: length of the data of the chunk in bytes (chunk itself is 12 bytes longer) +-4 bytes chunk type (ASCII a-z,A-Z only, see below) +-length bytes of data (may be 0 bytes if length was 0) +-4 bytes of CRC, computed on chunk name + data + +The first chunk starts at the 8th byte of the PNG file, the entire rest of the file +exists out of concatenated chunks with the above format. + +PNG standard chunk ASCII naming conventions: +-First byte: uppercase = critical, lowercase = ancillary +-Second byte: uppercase = public, lowercase = private +-Third byte: must be uppercase +-Fourth byte: uppercase = unsafe to copy, lowercase = safe to copy +*/ + +/* +Gets the length of the data of the chunk. Total chunk length has 12 bytes more. +There must be at least 4 bytes to read from. If the result value is too large, +it may be corrupt data. +*/ +unsigned lodepng_chunk_length(const unsigned char* chunk); + +/*puts the 4-byte type in null terminated string*/ +void lodepng_chunk_type(char type[5], const unsigned char* chunk); + +/*check if the type is the given type*/ +unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, const char* type); + +/*0: it's one of the critical chunk types, 1: it's an ancillary chunk (see PNG standard)*/ +unsigned char lodepng_chunk_ancillary(const unsigned char* chunk); + +/*0: public, 1: private (see PNG standard)*/ +unsigned char lodepng_chunk_private(const unsigned char* chunk); + +/*0: the chunk is unsafe to copy, 1: the chunk is safe to copy (see PNG standard)*/ +unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk); + +/*get pointer to the data of the chunk, where the input points to the header of the chunk*/ +unsigned char* lodepng_chunk_data(unsigned char* chunk); +const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk); + +/*returns 0 if the crc is correct, 1 if it's incorrect (0 for OK as usual!)*/ +unsigned lodepng_chunk_check_crc(const unsigned char* chunk); + +/*generates the correct CRC from the data and puts it in the last 4 bytes of the chunk*/ +void lodepng_chunk_generate_crc(unsigned char* chunk); + +/* +Iterate to next chunks, allows iterating through all chunks of the PNG file. +Input must be at the beginning of a chunk (result of a previous lodepng_chunk_next call, +or the 8th byte of a PNG file which always has the first chunk), or alternatively may +point to the first byte of the PNG file (which is not a chunk but the magic header, the +function will then skip over it and return the first real chunk). +Will output pointer to the start of the next chunk, or at or beyond end of the file if there +is no more chunk after this or possibly if the chunk is corrupt. +Start this process at the 8th byte of the PNG file. +In a non-corrupt PNG file, the last chunk should have name "IEND". +*/ +unsigned char* lodepng_chunk_next(unsigned char* chunk, unsigned char* end); +const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk, const unsigned char* end); + +/*Finds the first chunk with the given type in the range [chunk, end), or returns NULL if not found.*/ +unsigned char* lodepng_chunk_find(unsigned char* chunk, unsigned char* end, const char type[5]); +const unsigned char* lodepng_chunk_find_const(const unsigned char* chunk, const unsigned char* end, const char type[5]); + +/* +Appends chunk to the data in out. The given chunk should already have its chunk header. +The out variable and outsize are updated to reflect the new reallocated buffer. +Returns error code (0 if it went ok) +*/ +unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsigned char* chunk); + +/* +Appends new chunk to out. The chunk to append is given by giving its length, type +and data separately. The type is a 4-letter string. +The out variable and outsize are updated to reflect the new reallocated buffer. +Returns error code (0 if it went ok) +*/ +unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, size_t length, + const char* type, const unsigned char* data); + + +/*Calculate CRC32 of buffer*/ +unsigned lodepng_crc32(const unsigned char* buf, size_t len); +#endif /*LODEPNG_COMPILE_PNG*/ + + +#ifdef LODEPNG_COMPILE_ZLIB +/* +This zlib part can be used independently to zlib compress and decompress a +buffer. It cannot be used to create gzip files however, and it only supports the +part of zlib that is required for PNG, it does not support dictionaries. +*/ + +#ifdef LODEPNG_COMPILE_DECODER +/*Inflate a buffer. Inflate is the decompression step of deflate. Out buffer must be freed after use.*/ +unsigned lodepng_inflate(unsigned char** out, size_t* outsize, + const unsigned char* in, size_t insize, + const LodePNGDecompressSettings* settings); + +/* +Decompresses Zlib data. Reallocates the out buffer and appends the data. The +data must be according to the zlib specification. +Either, *out must be NULL and *outsize must be 0, or, *out must be a valid +buffer and *outsize its size in bytes. out must be freed by user after usage. +*/ +unsigned lodepng_zlib_decompress(unsigned char** out, size_t* outsize, + const unsigned char* in, size_t insize, + const LodePNGDecompressSettings* settings); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/* +Compresses data with Zlib. Reallocates the out buffer and appends the data. +Zlib adds a small header and trailer around the deflate data. +The data is output in the format of the zlib specification. +Either, *out must be NULL and *outsize must be 0, or, *out must be a valid +buffer and *outsize its size in bytes. out must be freed by user after usage. +*/ +unsigned lodepng_zlib_compress(unsigned char** out, size_t* outsize, + const unsigned char* in, size_t insize, + const LodePNGCompressSettings* settings); + +/* +Find length-limited Huffman code for given frequencies. This function is in the +public interface only for tests, it's used internally by lodepng_deflate. +*/ +unsigned lodepng_huffman_code_lengths(unsigned* lengths, const unsigned* frequencies, + size_t numcodes, unsigned maxbitlen); + +/*Compress a buffer with deflate. See RFC 1951. Out buffer must be freed after use.*/ +unsigned lodepng_deflate(unsigned char** out, size_t* outsize, + const unsigned char* in, size_t insize, + const LodePNGCompressSettings* settings); + +#endif /*LODEPNG_COMPILE_ENCODER*/ +#endif /*LODEPNG_COMPILE_ZLIB*/ + +#ifdef LODEPNG_COMPILE_DISK +/* +Load a file from disk into buffer. The function allocates the out buffer, and +after usage you should free it. +out: output parameter, contains pointer to loaded buffer. +outsize: output parameter, size of the allocated out buffer +filename: the path to the file to load +return value: error code (0 means ok) + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory. +*/ +unsigned lodepng_load_file(unsigned char** out, size_t* outsize, const char* filename); + +/* +Save a file from buffer to disk. Warning, if it exists, this function overwrites +the file without warning! +buffer: the buffer to write +buffersize: size of the buffer to write +filename: the path to the file to save to +return value: error code (0 means ok) + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory +*/ +unsigned lodepng_save_file(const unsigned char* buffer, size_t buffersize, const char* filename); +#endif /*LODEPNG_COMPILE_DISK*/ + +#ifdef LODEPNG_COMPILE_CPP +/* The LodePNG C++ wrapper uses std::vectors instead of manually allocated memory buffers. */ +namespace lodepng { +#ifdef LODEPNG_COMPILE_PNG +/* Wrapper around LodePNGState, which automatically calls lodepng_state_init in the constructor +and lodepng_state_cleanup in the desctructor.*/ +class State : public LodePNGState { + public: + State(); + State(const State& other); + ~State(); + State& operator=(const State& other); +}; + +#ifdef LODEPNG_COMPILE_DECODER +/* Same as other lodepng::decode, but using a State for more settings and information. */ +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + State& state, + const unsigned char* in, size_t insize); +unsigned decode(std::vector& out, unsigned& w, unsigned& h, + State& state, + const std::vector& in); +#endif /*LODEPNG_COMPILE_DECODER*/ + +#ifdef LODEPNG_COMPILE_ENCODER +/* Same as other lodepng::encode, but using a State for more settings and information. */ +unsigned encode(std::vector& out, + const unsigned char* in, unsigned w, unsigned h, + State& state); +unsigned encode(std::vector& out, + const std::vector& in, unsigned w, unsigned h, + State& state); +#endif /*LODEPNG_COMPILE_ENCODER*/ + +#ifdef LODEPNG_COMPILE_DISK +/* +Load a file from disk into an std::vector. +return value: error code (0 means ok) + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and decode in-memory +*/ +unsigned load_file(std::vector& buffer, const std::string& filename); + +/* +Save the binary data in an std::vector to a file on disk. The file is overwritten +without warning. + +NOTE: Wide-character filenames are not supported, you can use an external method +to handle such files and encode in-memory +*/ +unsigned save_file(const std::vector& buffer, const std::string& filename); +#endif /* LODEPNG_COMPILE_DISK */ +#endif /* LODEPNG_COMPILE_PNG */ + +#ifdef LODEPNG_COMPILE_ZLIB +#ifdef LODEPNG_COMPILE_DECODER +/* Zlib-decompress an unsigned char buffer */ +unsigned decompress(std::vector& out, const unsigned char* in, size_t insize, + const LodePNGDecompressSettings& settings = lodepng_default_decompress_settings); + +/* Zlib-decompress an std::vector */ +unsigned decompress(std::vector& out, const std::vector& in, + const LodePNGDecompressSettings& settings = lodepng_default_decompress_settings); +#endif /* LODEPNG_COMPILE_DECODER */ + +#ifdef LODEPNG_COMPILE_ENCODER +/* Zlib-compress an unsigned char buffer */ +unsigned compress(std::vector& out, const unsigned char* in, size_t insize, + const LodePNGCompressSettings& settings = lodepng_default_compress_settings); + +/* Zlib-compress an std::vector */ +unsigned compress(std::vector& out, const std::vector& in, + const LodePNGCompressSettings& settings = lodepng_default_compress_settings); +#endif /* LODEPNG_COMPILE_ENCODER */ +#endif /* LODEPNG_COMPILE_ZLIB */ +} /* namespace lodepng */ +#endif /*LODEPNG_COMPILE_CPP*/ + +/* +TODO: +[.] test if there are no memory leaks or security exploits - done a lot but needs to be checked often +[.] check compatibility with various compilers - done but needs to be redone for every newer version +[X] converting color to 16-bit per channel types +[X] support color profile chunk types (but never let them touch RGB values by default) +[ ] support all second edition public PNG chunk types (almost done except sPLT and hIST) +[X] support non-animation third edition public PNG chunk types: eXIf, cICP, mDCV, cLLI +[ ] make sure encoder generates no chunks with size > (2^31)-1 +[ ] partial decoding (stream processing) +[X] let the "isFullyOpaque" function check color keys and transparent palettes too +[X] better name for the variables "codes", "codesD", "codelengthcodes", "clcl" and "lldl" +[ ] allow treating some errors like warnings, when image is recoverable (e.g. 69, 57, 58) +[ ] make warnings like: oob palette, checksum fail, data after iend, wrong/unknown crit chunk, no null terminator in text, ... +[ ] error messages with line numbers (and version) +[ ] errors in state instead of as return code? +[ ] new errors/warnings like suspiciously big decompressed ztxt or iccp chunk +[ ] let the C++ wrapper catch exceptions coming from the standard library and return LodePNG error codes +[ ] allow user to provide custom color conversion functions, e.g. for premultiplied alpha, padding bits or not, ... +[ ] allow user to give data (void*) to custom allocator +[X] provide alternatives for C library functions not present on some platforms (memcpy, ...) +*/ + +#endif /*LODEPNG_H inclusion guard*/ + +/* +LodePNG Documentation +--------------------- + +0. table of contents +-------------------- + + 1. about + 1.1. supported features + 1.2. features not supported + 2. C and C++ version + 3. security + 4. decoding + 5. encoding + 6. color conversions + 6.1. PNG color types + 6.2. color conversions + 6.3. padding bits + 6.4. A note about 16-bits per channel and endianness + 7. error values + 8. chunks and PNG editing + 9. compiler support + 10. examples + 10.1. decoder C++ example + 10.2. decoder C example + 11. state settings reference + 12. changes + 13. contact information + + +1. about +-------- + +PNG is a file format to store raster images losslessly with good compression, +supporting different color types and alpha channel. + +LodePNG is a PNG codec according to the Portable Network Graphics (PNG) +Specification (Second Edition) - W3C Recommendation 10 November 2003. + +The specifications used are: + +*) Portable Network Graphics (PNG) Specification (Second Edition): + http://www.w3.org/TR/2003/REC-PNG-20031110 +*) RFC 1950 ZLIB Compressed Data Format version 3.3: + http://www.gzip.org/zlib/rfc-zlib.html +*) RFC 1951 DEFLATE Compressed Data Format Specification ver 1.3: + http://www.gzip.org/zlib/rfc-deflate.html + +The most recent version of LodePNG can currently be found at +http://lodev.org/lodepng/ + +LodePNG works both in C (ISO C90) and C++, with a C++ wrapper that adds +extra functionality. + +LodePNG exists out of two files: +-lodepng.h: the header file for both C and C++ +-lodepng.c(pp): give it the name lodepng.c or lodepng.cpp (or .cc) depending on your usage + +If you want to start using LodePNG right away without reading this doc, get the +examples from the LodePNG website to see how to use it in code, or check the +smaller examples in chapter 13 here. + +LodePNG is simple but only supports the basic requirements. To achieve +simplicity, the following design choices were made: There are no dependencies +on any external library. There are functions to decode and encode a PNG with +a single function call, and extended versions of these functions taking a +LodePNGState struct allowing to specify or get more information. By default +the colors of the raw image are always RGB or RGBA, no matter what color type +the PNG file uses. To read and write files, there are simple functions to +convert the files to/from buffers in memory. + +This all makes LodePNG suitable for loading textures in games, demos and small +programs, ... It's less suitable for full fledged image editors, loading PNGs +over network (it requires all the image data to be available before decoding can +begin), life-critical systems, ... + +1.1. supported features +----------------------- + +The following features are supported by the decoder: + +*) decoding of PNGs with any color type, bit depth and interlace mode, to a 24- or 32-bit color raw image, + or the same color type as the PNG +*) encoding of PNGs, from any raw image to 24- or 32-bit color, or the same color type as the raw image +*) Adam7 interlace and deinterlace for any color type +*) loading the image from harddisk or decoding it from a buffer from other sources than harddisk +*) support for alpha channels, including RGBA color model, translucent palettes and color keying +*) zlib decompression (inflate) +*) zlib compression (deflate) +*) CRC32 and ADLER32 checksums +*) colorimetric color profile conversions: currently experimentally available in lodepng_util.cpp only, + plus alternatively ability to pass on chroma/gamma/ICC profile information to other color management system. +*) handling of unknown chunks, allowing making a PNG editor that stores custom and unknown chunks. +*) the following chunks are supported by both encoder and decoder: + IHDR: header information + PLTE: color palette + IDAT: pixel data + IEND: the final chunk + tRNS: transparency for palettized images + tEXt: textual information + zTXt: compressed textual information + iTXt: international textual information + bKGD: suggested background color + pHYs: physical dimensions + tIME: modification time + cHRM: RGB chromaticities + gAMA: RGB gamma correction + iCCP: ICC color profile + sRGB: rendering intent + sBIT: significant bits + +1.2. features not supported +--------------------------- + +The following features are not (yet) supported: + +*) some features needed to make a conformant PNG-Editor might be still missing. +*) partial loading/stream processing. All data must be available and is processed in one call. +*) The hIST and sPLT public chunks are not (yet) supported but treated as unknown chunks + + +2. C and C++ version +-------------------- + +The C version uses buffers allocated with alloc that you need to free() +yourself. You need to use init and cleanup functions for each struct whenever +using a struct from the C version to avoid exploits and memory leaks. + +The C++ version has extra functions with std::vectors in the interface and the +lodepng::State class which is a LodePNGState with constructor and destructor. + +These files work without modification for both C and C++ compilers because all +the additional C++ code is in "#ifdef __cplusplus" blocks that make C-compilers +ignore it, and the C code is made to compile both with strict ISO C90 and C++. + +To use the C++ version, you need to rename the source file to lodepng.cpp +(instead of lodepng.c), and compile it with a C++ compiler. + +To use the C version, you need to rename the source file to lodepng.c (instead +of lodepng.cpp), and compile it with a C compiler. + + +3. Security +----------- + +Even if carefully designed, it's always possible that LodePNG contains possible +exploits. If you discover one, please let me know, and it will be fixed. + +When using LodePNG, care has to be taken with the C version of LodePNG, as well +as the C-style structs when working with C++. The following conventions are used +for all C-style structs: + +-if a struct has a corresponding init function, always call the init function when making a new one +-if a struct has a corresponding cleanup function, call it before the struct disappears to avoid memory leaks +-if a struct has a corresponding copy function, use the copy function instead of "=". + The destination must also be inited already. + + +4. Decoding +----------- + +Decoding converts a PNG compressed image to a raw pixel buffer. + +Most documentation on using the decoder is at its declarations in the header +above. For C, simple decoding can be done with functions such as +lodepng_decode32, and more advanced decoding can be done with the struct +LodePNGState and lodepng_decode. For C++, all decoding can be done with the +various lodepng::decode functions, and lodepng::State can be used for advanced +features. + +When using the LodePNGState, it uses the following fields for decoding: +*) LodePNGInfo info_png: it stores extra information about the PNG (the input) in here +*) LodePNGColorMode info_raw: here you can say what color mode of the raw image (the output) you want to get +*) LodePNGDecoderSettings decoder: you can specify a few extra settings for the decoder to use + +LodePNGInfo info_png +-------------------- + +After decoding, this contains extra information of the PNG image, except the actual +pixels, width and height because these are already gotten directly from the decoder +functions. + +It contains for example the original color type of the PNG image, text comments, +suggested background color, etc... More details about the LodePNGInfo struct are +at its declaration documentation. + +LodePNGColorMode info_raw +------------------------- + +When decoding, here you can specify which color type you want +the resulting raw image to be. If this is different from the colortype of the +PNG, then the decoder will automatically convert the result. This conversion +always works, except if you want it to convert a color PNG to grayscale or to +a palette with missing colors. + +By default, 32-bit color is used for the result. + +LodePNGDecoderSettings decoder +------------------------------ + +The settings can be used to ignore the errors created by invalid CRC and Adler32 +chunks, and to disable the decoding of tEXt chunks. + +There's also a setting color_convert, true by default. If false, no conversion +is done, the resulting data will be as it was in the PNG (after decompression) +and you'll have to puzzle the colors of the pixels together yourself using the +color type information in the LodePNGInfo. + + +5. Encoding +----------- + +Encoding converts a raw pixel buffer to a PNG compressed image. + +Most documentation on using the encoder is at its declarations in the header +above. For C, simple encoding can be done with functions such as +lodepng_encode32, and more advanced decoding can be done with the struct +LodePNGState and lodepng_encode. For C++, all encoding can be done with the +various lodepng::encode functions, and lodepng::State can be used for advanced +features. + +Like the decoder, the encoder can also give errors. However it gives less errors +since the encoder input is trusted, the decoder input (a PNG image that could +be forged by anyone) is not trusted. + +When using the LodePNGState, it uses the following fields for encoding: +*) LodePNGInfo info_png: here you specify how you want the PNG (the output) to be. +*) LodePNGColorMode info_raw: here you say what color type of the raw image (the input) has +*) LodePNGEncoderSettings encoder: you can specify a few settings for the encoder to use + +LodePNGInfo info_png +-------------------- + +When encoding, you use this the opposite way as when decoding: for encoding, +you fill in the values you want the PNG to have before encoding. By default it's +not needed to specify a color type for the PNG since it's automatically chosen, +but it's possible to choose it yourself given the right settings. + +The encoder will not always exactly match the LodePNGInfo struct you give, +it tries as close as possible. Some things are ignored by the encoder. The +encoder uses, for example, the following settings from it when applicable: +colortype and bitdepth, text chunks, time chunk, the color key, the palette, the +background color, the interlace method, unknown chunks, ... + +When encoding to a PNG with colortype 3, the encoder will generate a PLTE chunk. +If the palette contains any colors for which the alpha channel is not 255 (so +there are translucent colors in the palette), it'll add a tRNS chunk. + +LodePNGColorMode info_raw +------------------------- + +You specify the color type of the raw image that you give to the input here, +including a possible transparent color key and palette you happen to be using in +your raw image data. + +By default, 32-bit color is assumed, meaning your input has to be in RGBA +format with 4 bytes (unsigned chars) per pixel. + +LodePNGEncoderSettings encoder +------------------------------ + +The following settings are supported (some are in sub-structs): +*) auto_convert: when this option is enabled, the encoder will +automatically choose the smallest possible color mode (including color key) that +can encode the colors of all pixels without information loss. +*) btype: the block type for LZ77. 0 = uncompressed, 1 = fixed huffman tree, + 2 = dynamic huffman tree (best compression). Should be 2 for proper + compression. +*) use_lz77: whether or not to use LZ77 for compressed block types. Should be + true for proper compression. +*) windowsize: the window size used by the LZ77 encoder (1 - 32768). Has value + 2048 by default, but can be set to 32768 for better, but slow, compression. +*) force_palette: if colortype is 2 or 6, you can make the encoder write a PLTE + chunk if force_palette is true. This can used as suggested palette to convert + to by viewers that don't support more than 256 colors (if those still exist) +*) add_id: add text chunk "Encoder: LodePNG " to the image. +*) text_compression: default 1. If 1, it'll store texts as zTXt instead of tEXt chunks. + zTXt chunks use zlib compression on the text. This gives a smaller result on + large texts but a larger result on small texts (such as a single program name). + It's all tEXt or all zTXt though, there's no separate setting per text yet. + + +6. color conversions +-------------------- + +An important thing to note about LodePNG, is that the color type of the PNG, and +the color type of the raw image, are completely independent. By default, when +you decode a PNG, you get the result as a raw image in the color type you want, +no matter whether the PNG was encoded with a palette, grayscale or RGBA color. +And if you encode an image, by default LodePNG will automatically choose the PNG +color type that gives good compression based on the values of colors and amount +of colors in the image. It can be configured to let you control it instead as +well, though. + +To be able to do this, LodePNG does conversions from one color mode to another. +It can convert from almost any color type to any other color type, except the +following conversions: RGB to grayscale is not supported, and converting to a +palette when the palette doesn't have a required color is not supported. This is +not supported on purpose: this is information loss which requires a color +reduction algorithm that is beyond the scope of a PNG encoder (yes, RGB to gray +is easy, but there are multiple ways if you want to give some channels more +weight). + +By default, when decoding, you get the raw image in 32-bit RGBA or 24-bit RGB +color, no matter what color type the PNG has. And by default when encoding, +LodePNG automatically picks the best color model for the output PNG, and expects +the input image to be 32-bit RGBA or 24-bit RGB. So, unless you want to control +the color format of the images yourself, you can skip this chapter. + +6.1. PNG color types +-------------------- + +A PNG image can have many color types, ranging from 1-bit color to 64-bit color, +as well as palettized color modes. After the zlib decompression and unfiltering +in the PNG image is done, the raw pixel data will have that color type and thus +a certain amount of bits per pixel. If you want the output raw image after +decoding to have another color type, a conversion is done by LodePNG. + +The PNG specification gives the following color types: + +0: grayscale, bit depths 1, 2, 4, 8, 16 +2: RGB, bit depths 8 and 16 +3: palette, bit depths 1, 2, 4 and 8 +4: grayscale with alpha, bit depths 8 and 16 +6: RGBA, bit depths 8 and 16 + +Bit depth is the amount of bits per pixel per color channel. So the total amount +of bits per pixel is: amount of channels * bitdepth. + +6.2. color conversions +---------------------- + +As explained in the sections about the encoder and decoder, you can specify +color types and bit depths in info_png and info_raw to change the default +behaviour. + +If, when decoding, you want the raw image to be something else than the default, +you need to set the color type and bit depth you want in the LodePNGColorMode, +or the parameters colortype and bitdepth of the simple decoding function. + +If, when encoding, you use another color type than the default in the raw input +image, you need to specify its color type and bit depth in the LodePNGColorMode +of the raw image, or use the parameters colortype and bitdepth of the simple +encoding function. + +If, when encoding, you don't want LodePNG to choose the output PNG color type +but control it yourself, you need to set auto_convert in the encoder settings +to false, and specify the color type you want in the LodePNGInfo of the +encoder (including palette: it can generate a palette if auto_convert is true, +otherwise not). + +If the input and output color type differ (whether user chosen or auto chosen), +LodePNG will do a color conversion, which follows the rules below, and may +sometimes result in an error. + +To avoid some confusion: +-the decoder converts from PNG to raw image +-the encoder converts from raw image to PNG +-the colortype and bitdepth in LodePNGColorMode info_raw, are those of the raw image +-the colortype and bitdepth in the color field of LodePNGInfo info_png, are those of the PNG +-when encoding, the color type in LodePNGInfo is ignored if auto_convert + is enabled, it is automatically generated instead +-when decoding, the color type in LodePNGInfo is set by the decoder to that of the original + PNG image, but it can be ignored since the raw image has the color type you requested instead +-if the color type of the LodePNGColorMode and PNG image aren't the same, a conversion + between the color types is done if the color types are supported. If it is not + supported, an error is returned. If the types are the same, no conversion is done. +-even though some conversions aren't supported, LodePNG supports loading PNGs from any + colortype and saving PNGs to any colortype, sometimes it just requires preparing + the raw image correctly before encoding. +-both encoder and decoder use the same color converter. + +The function lodepng_convert does the color conversion. It is available in the +interface but normally isn't needed since the encoder and decoder already call +it. + +Non supported color conversions: +-color to grayscale when non-gray pixels are present: no error is thrown, but +the result will look ugly because only the red channel is taken (it assumes all +three channels are the same in this case so ignores green and blue). The reason +no error is given is to allow converting from three-channel grayscale images to +one-channel even if there are numerical imprecisions. +-anything to palette when the palette does not have an exact match for a from-color +in it: in this case an error is thrown + +Supported color conversions: +-anything to 8-bit RGB, 8-bit RGBA, 16-bit RGB, 16-bit RGBA +-any gray or gray+alpha, to gray or gray+alpha +-anything to a palette, as long as the palette has the requested colors in it +-removing alpha channel +-higher to smaller bitdepth, and vice versa + +If you want no color conversion to be done (e.g. for speed or control): +-In the encoder, you can make it save a PNG with any color type by giving the +raw color mode and LodePNGInfo the same color mode, and setting auto_convert to +false. +-In the decoder, you can make it store the pixel data in the same color type +as the PNG has, by setting the color_convert setting to false. Settings in +info_raw are then ignored. + +6.3. padding bits +----------------- + +In the PNG file format, if a less than 8-bit per pixel color type is used and the scanlines +have a bit amount that isn't a multiple of 8, then padding bits are used so that each +scanline starts at a fresh byte. But that is NOT true for the LodePNG raw input and output. +The raw input image you give to the encoder, and the raw output image you get from the decoder +will NOT have these padding bits, e.g. in the case of a 1-bit image with a width +of 7 pixels, the first pixel of the second scanline will the 8th bit of the first byte, +not the first bit of a new byte. + +6.4. A note about 16-bits per channel and endianness +---------------------------------------------------- + +LodePNG uses unsigned char arrays for 16-bit per channel colors too, just like +for any other color format. The 16-bit values are stored in big endian (most +significant byte first) in these arrays. This is the opposite order of the +little endian used by x86 CPU's. + +LodePNG always uses big endian because the PNG file format does so internally. +Conversions to other formats than PNG uses internally are not supported by +LodePNG on purpose, there are myriads of formats, including endianness of 16-bit +colors, the order in which you store R, G, B and A, and so on. Supporting and +converting to/from all that is outside the scope of LodePNG. + +This may mean that, depending on your use case, you may want to convert the big +endian output of LodePNG to little endian with a for loop. This is certainly not +always needed, many applications and libraries support big endian 16-bit colors +anyway, but it means you cannot simply cast the unsigned char* buffer to an +unsigned short* buffer on x86 CPUs. + + +7. error values +--------------- + +All functions in LodePNG that return an error code, return 0 if everything went +OK, or a non-zero code if there was an error. + +The meaning of the LodePNG error values can be retrieved with the function +lodepng_error_text: given the numerical error code, it returns a description +of the error in English as a string. + +Check the implementation of lodepng_error_text to see the meaning of each code. + +It is not recommended to use the numerical values to programmatically make +different decisions based on error types as the numbers are not guaranteed to +stay backwards compatible. They are for human consumption only. Programmatically +only 0 or non-0 matter. + + +8. chunks and PNG editing +------------------------- + +If you want to add extra chunks to a PNG you encode, or use LodePNG for a PNG +editor that should follow the rules about handling of unknown chunks, or if your +program is able to read other types of chunks than the ones handled by LodePNG, +then that's possible with the chunk functions of LodePNG. + +A PNG chunk has the following layout: + +4 bytes length +4 bytes type name +length bytes data +4 bytes CRC + +8.1. iterating through chunks +----------------------------- + +If you have a buffer containing the PNG image data, then the first chunk (the +IHDR chunk) starts at byte number 8 of that buffer. The first 8 bytes are the +signature of the PNG and are not part of a chunk. But if you start at byte 8 +then you have a chunk, and can check the following things of it. + +NOTE: none of these functions check for memory buffer boundaries. To avoid +exploits, always make sure the buffer contains all the data of the chunks. +When using lodepng_chunk_next, make sure the returned value is within the +allocated memory. + +unsigned lodepng_chunk_length(const unsigned char* chunk): + +Get the length of the chunk's data. The total chunk length is this length + 12. + +void lodepng_chunk_type(char type[5], const unsigned char* chunk): +unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, const char* type): + +Get the type of the chunk or compare if it's a certain type + +unsigned char lodepng_chunk_critical(const unsigned char* chunk): +unsigned char lodepng_chunk_private(const unsigned char* chunk): +unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk): + +Check if the chunk is critical in the PNG standard (only IHDR, PLTE, IDAT and IEND are). +Check if the chunk is private (public chunks are part of the standard, private ones not). +Check if the chunk is safe to copy. If it's not, then, when modifying data in a critical +chunk, unsafe to copy chunks of the old image may NOT be saved in the new one if your +program doesn't handle that type of unknown chunk. + +unsigned char* lodepng_chunk_data(unsigned char* chunk): +const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk): + +Get a pointer to the start of the data of the chunk. + +unsigned lodepng_chunk_check_crc(const unsigned char* chunk): +void lodepng_chunk_generate_crc(unsigned char* chunk): + +Check if the crc is correct or generate a correct one. + +unsigned char* lodepng_chunk_next(unsigned char* chunk): +const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk): + +Iterate to the next chunk. This works if you have a buffer with consecutive chunks. Note that these +functions do no boundary checking of the allocated data whatsoever, so make sure there is enough +data available in the buffer to be able to go to the next chunk. + +unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsigned char* chunk): +unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, unsigned length, + const char* type, const unsigned char* data): + +These functions are used to create new chunks that are appended to the data in *out that has +length *outsize. The append function appends an existing chunk to the new data. The create +function creates a new chunk with the given parameters and appends it. Type is the 4-letter +name of the chunk. + +8.2. chunks in info_png +----------------------- + +The LodePNGInfo struct contains fields with the unknown chunk in it. It has 3 +buffers (each with size) to contain 3 types of unknown chunks: +the ones that come before the PLTE chunk, the ones that come between the PLTE +and the IDAT chunks, and the ones that come after the IDAT chunks. +It's necessary to make the distinction between these 3 cases because the PNG +standard forces to keep the ordering of unknown chunks compared to the critical +chunks, but does not force any other ordering rules. + +info_png.unknown_chunks_data[0] is the chunks before PLTE +info_png.unknown_chunks_data[1] is the chunks after PLTE, before IDAT +info_png.unknown_chunks_data[2] is the chunks after IDAT + +The chunks in these 3 buffers can be iterated through and read by using the same +way described in the previous subchapter. + +When using the decoder to decode a PNG, you can make it store all unknown chunks +if you set the option settings.remember_unknown_chunks to 1. By default, this +option is off (0). + +The encoder will always encode unknown chunks that are stored in the info_png. +If you need it to add a particular chunk that isn't known by LodePNG, you can +use lodepng_chunk_append or lodepng_chunk_create to the chunk data in +info_png.unknown_chunks_data[x]. + +Chunks that are known by LodePNG should not be added in that way. E.g. to make +LodePNG add a bKGD chunk, set background_defined to true and add the correct +parameters there instead. + + +9. compiler support +------------------- + +No libraries other than the current standard C library are needed to compile +LodePNG. For the C++ version, only the standard C++ library is needed on top. +Add the files lodepng.c(pp) and lodepng.h to your project, include +lodepng.h where needed, and your program can read/write PNG files. + +It is compatible with C90 and up, and C++03 and up. + +If performance is important, use optimization when compiling! For both the +encoder and decoder, this makes a large difference. + +Make sure that LodePNG is compiled with the same compiler of the same version +and with the same settings as the rest of the program, or the interfaces with +std::vectors and std::strings in C++ can be incompatible. + +CHAR_BITS must be 8 or higher, because LodePNG uses unsigned chars for octets. + +*) gcc and g++ + +LodePNG is developed in gcc so this compiler is natively supported. It gives no +warnings with compiler options "-Wall -Wextra -pedantic -ansi", with gcc and g++ +version 4.7.1 on Linux, 32-bit and 64-bit. + +*) Clang + +Fully supported and warning-free. + +*) Mingw + +The Mingw compiler (a port of gcc for Windows) should be fully supported by +LodePNG. + +*) Visual Studio and Visual C++ Express Edition + +LodePNG should be warning-free with warning level W4. Two warnings were disabled +with pragmas though: warning 4244 about implicit conversions, and warning 4996 +where it wants to use a non-standard function fopen_s instead of the standard C +fopen. + +Visual Studio may want "stdafx.h" files to be included in each source file and +give an error "unexpected end of file while looking for precompiled header". +This is not standard C++ and will not be added to the stock LodePNG. You can +disable it for lodepng.cpp only by right clicking it, Properties, C/C++, +Precompiled Headers, and set it to Not Using Precompiled Headers there. + +NOTE: Modern versions of VS should be fully supported, but old versions, e.g. +VS6, are not guaranteed to work. + +*) Compilers on Macintosh + +LodePNG has been reported to work both with gcc and LLVM for Macintosh, both for +C and C++. + +*) Other Compilers + +If you encounter problems on any compilers, feel free to let me know and I may +try to fix it if the compiler is modern and standards compliant. + + +10. examples +------------ + +This decoder example shows the most basic usage of LodePNG. More complex +examples can be found on the LodePNG website. + +NOTE: these examples do not support wide-character filenames, you can use an +external method to handle such files and encode or decode in-memory + +10.1. decoder C++ example +------------------------- + +#include "lodepng.h" +#include + +int main(int argc, char *argv[]) { + const char* filename = argc > 1 ? argv[1] : "test.png"; + + //load and decode + std::vector image; + unsigned width, height; + unsigned error = lodepng::decode(image, width, height, filename); + + //if there's an error, display it + if(error) std::cout << "decoder error " << error << ": " << lodepng_error_text(error) << std::endl; + + //the pixels are now in the vector "image", 4 bytes per pixel, ordered RGBARGBA..., use it as texture, draw it, ... +} + +10.2. decoder C example +----------------------- + +#include "lodepng.h" + +int main(int argc, char *argv[]) { + unsigned error; + unsigned char* image; + size_t width, height; + const char* filename = argc > 1 ? argv[1] : "test.png"; + + error = lodepng_decode32_file(&image, &width, &height, filename); + + if(error) printf("decoder error %u: %s\n", error, lodepng_error_text(error)); + + / * use image here * / + + free(image); + return 0; +} + +11. state settings reference +---------------------------- + +A quick reference of some settings to set on the LodePNGState + +For decoding: + +state.decoder.zlibsettings.ignore_adler32: ignore ADLER32 checksums +state.decoder.zlibsettings.custom_...: use custom inflate function +state.decoder.ignore_crc: ignore CRC checksums +state.decoder.ignore_critical: ignore unknown critical chunks +state.decoder.ignore_end: ignore missing IEND chunk. May fail if this corruption causes other errors +state.decoder.color_convert: convert internal PNG color to chosen one +state.decoder.read_text_chunks: whether to read in text metadata chunks +state.decoder.remember_unknown_chunks: whether to read in unknown chunks +state.info_raw.colortype: desired color type for decoded image +state.info_raw.bitdepth: desired bit depth for decoded image +state.info_raw....: more color settings, see struct LodePNGColorMode +state.info_png....: no settings for decoder but output, see struct LodePNGInfo + +For encoding: + +state.encoder.zlibsettings.btype: disable compression by setting it to 0 +state.encoder.zlibsettings.use_lz77: use LZ77 in compression +state.encoder.zlibsettings.windowsize: tweak LZ77 windowsize +state.encoder.zlibsettings.minmatch: tweak min LZ77 length to match +state.encoder.zlibsettings.nicematch: tweak LZ77 match where to stop searching +state.encoder.zlibsettings.lazymatching: try one more LZ77 matching +state.encoder.zlibsettings.custom_...: use custom deflate function +state.encoder.auto_convert: choose optimal PNG color type, if 0 uses info_png +state.encoder.filter_palette_zero: PNG filter strategy for palette +state.encoder.filter_strategy: PNG filter strategy to encode with +state.encoder.force_palette: add palette even if not encoding to one +state.encoder.add_id: add LodePNG identifier and version as a text chunk +state.encoder.text_compression: use compressed text chunks for metadata +state.info_raw.colortype: color type of raw input image you provide +state.info_raw.bitdepth: bit depth of raw input image you provide +state.info_raw: more color settings, see struct LodePNGColorMode +state.info_png.color.colortype: desired color type if auto_convert is false +state.info_png.color.bitdepth: desired bit depth if auto_convert is false +state.info_png.color....: more color settings, see struct LodePNGColorMode +state.info_png....: more PNG related settings, see struct LodePNGInfo + + +12. changes +----------- + +The version number of LodePNG is the date of the change given in the format +yyyymmdd. + +Some changes aren't backwards compatible. Those are indicated with a (!) +symbol. + +Not all changes are listed here, the commit history in github lists more: +https://github.com/lvandeve/lodepng + +*) 6 may 2025 (!): renamed mDCv to mDCV and cLLi to cLLI as per the recent + rename in the draft png third edition spec. Please note that as long as the + third edition is not finalized, backwards-incompatible changes to its + features are possible. +*) 23 dec 2024: added support for the mDCv and cLLi chunks (for png third + edition spec) +*) 22 dec 2024: added support for the cICP chunk (for png third edition spec) +*) 15 dec 2024: added support for the eXIf chunk (for png third edition spec) +*) 10 apr 2023: faster CRC32 implementation, but with larger lookup table. +*) 13 jun 2022: added support for the sBIT chunk. +*) 09 jan 2022: minor decoder speed improvements. +*) 27 jun 2021: added warnings that file reading/writing functions don't support + wide-character filenames (support for this is not planned, opening files is + not the core part of PNG decoding/decoding and is platform dependent). +*) 17 oct 2020: prevent decoding too large text/icc chunks by default. +*) 06 mar 2020: simplified some of the dynamic memory allocations. +*) 12 jan 2020: (!) added 'end' argument to lodepng_chunk_next to allow correct + overflow checks. +*) 14 aug 2019: around 25% faster decoding thanks to huffman lookup tables. +*) 15 jun 2019: (!) auto_choose_color API changed (for bugfix: don't use palette + if gray ICC profile) and non-ICC LodePNGColorProfile renamed to + LodePNGColorStats. +*) 30 dec 2018: code style changes only: removed newlines before opening braces. +*) 10 sep 2018: added way to inspect metadata chunks without full decoding. +*) 19 aug 2018: (!) fixed color mode bKGD is encoded with and made it use + palette index in case of palette. +*) 10 aug 2018: (!) added support for gAMA, cHRM, sRGB and iCCP chunks. This + change is backwards compatible unless you relied on unknown_chunks for those. +*) 11 jun 2018: less restrictive check for pixel size integer overflow +*) 14 jan 2018: allow optionally ignoring a few more recoverable errors +*) 17 sep 2017: fix memory leak for some encoder input error cases +*) 27 nov 2016: grey+alpha auto color model detection bugfix +*) 18 apr 2016: Changed qsort to custom stable sort (for platforms w/o qsort). +*) 09 apr 2016: Fixed colorkey usage detection, and better file loading (within + the limits of pure C90). +*) 08 dec 2015: Made load_file function return error if file can't be opened. +*) 24 oct 2015: Bugfix with decoding to palette output. +*) 18 apr 2015: Boundary PM instead of just package-merge for faster encoding. +*) 24 aug 2014: Moved to github +*) 23 aug 2014: Reduced needless memory usage of decoder. +*) 28 jun 2014: Removed fix_png setting, always support palette OOB for + simplicity. Made ColorProfile public. +*) 09 jun 2014: Faster encoder by fixing hash bug and more zeros optimization. +*) 22 dec 2013: Power of two windowsize required for optimization. +*) 15 apr 2013: Fixed bug with LAC_ALPHA and color key. +*) 25 mar 2013: Added an optional feature to ignore some PNG errors (fix_png). +*) 11 mar 2013: (!) Bugfix with custom free. Changed from "my" to "lodepng_" + prefix for the custom allocators and made it possible with a new #define to + use custom ones in your project without needing to change lodepng's code. +*) 28 jan 2013: Bugfix with color key. +*) 27 oct 2012: Tweaks in text chunk keyword length error handling. +*) 8 oct 2012: (!) Added new filter strategy (entropy) and new auto color mode. + (no palette). Better deflate tree encoding. New compression tweak settings. + Faster color conversions while decoding. Some internal cleanups. +*) 23 sep 2012: Reduced warnings in Visual Studio a little bit. +*) 1 sep 2012: (!) Removed #define's for giving custom (de)compression functions + and made it work with function pointers instead. +*) 23 jun 2012: Added more filter strategies. Made it easier to use custom alloc + and free functions and toggle #defines from compiler flags. Small fixes. +*) 6 may 2012: (!) Made plugging in custom zlib/deflate functions more flexible. +*) 22 apr 2012: (!) Made interface more consistent, renaming a lot. Removed + redundant C++ codec classes. Reduced amount of structs. Everything changed, + but it is cleaner now imho and functionality remains the same. Also fixed + several bugs and shrunk the implementation code. Made new samples. +*) 6 nov 2011: (!) By default, the encoder now automatically chooses the best + PNG color model and bit depth, based on the amount and type of colors of the + raw image. For this, autoLeaveOutAlphaChannel replaced by auto_choose_color. +*) 9 oct 2011: simpler hash chain implementation for the encoder. +*) 8 sep 2011: lz77 encoder lazy matching instead of greedy matching. +*) 23 aug 2011: tweaked the zlib compression parameters after benchmarking. + A bug with the PNG filtertype heuristic was fixed, so that it chooses much + better ones (it's quite significant). A setting to do an experimental, slow, + brute force search for PNG filter types is added. +*) 17 aug 2011: (!) changed some C zlib related function names. +*) 16 aug 2011: made the code less wide (max 120 characters per line). +*) 17 apr 2011: code cleanup. Bugfixes. Convert low to 16-bit per sample colors. +*) 21 feb 2011: fixed compiling for C90. Fixed compiling with sections disabled. +*) 11 dec 2010: encoding is made faster, based on suggestion by Peter Eastman + to optimize long sequences of zeros. +*) 13 nov 2010: added LodePNG_InfoColor_hasPaletteAlpha and + LodePNG_InfoColor_canHaveAlpha functions for convenience. +*) 7 nov 2010: added LodePNG_error_text function to get error code description. +*) 30 oct 2010: made decoding slightly faster +*) 26 oct 2010: (!) changed some C function and struct names (more consistent). + Reorganized the documentation and the declaration order in the header. +*) 08 aug 2010: only changed some comments and external samples. +*) 05 jul 2010: fixed bug thanks to warnings in the new gcc version. +*) 14 mar 2010: fixed bug where too much memory was allocated for char buffers. +*) 02 sep 2008: fixed bug where it could create empty tree that linux apps could + read by ignoring the problem but windows apps couldn't. +*) 06 jun 2008: added more error checks for out of memory cases. +*) 26 apr 2008: added a few more checks here and there to ensure more safety. +*) 06 mar 2008: crash with encoding of strings fixed +*) 02 feb 2008: support for international text chunks added (iTXt) +*) 23 jan 2008: small cleanups, and #defines to divide code in sections +*) 20 jan 2008: support for unknown chunks allowing using LodePNG for an editor. +*) 18 jan 2008: support for tIME and pHYs chunks added to encoder and decoder. +*) 17 jan 2008: ability to encode and decode compressed zTXt chunks added + Also various fixes, such as in the deflate and the padding bits code. +*) 13 jan 2008: Added ability to encode Adam7-interlaced images. Improved + filtering code of encoder. +*) 07 jan 2008: (!) changed LodePNG to use ISO C90 instead of C++. A + C++ wrapper around this provides an interface almost identical to before. + Having LodePNG be pure ISO C90 makes it more portable. The C and C++ code + are together in these files but it works both for C and C++ compilers. +*) 29 dec 2007: (!) changed most integer types to unsigned int + other tweaks +*) 30 aug 2007: bug fixed which makes this Borland C++ compatible +*) 09 aug 2007: some VS2005 warnings removed again +*) 21 jul 2007: deflate code placed in new namespace separate from zlib code +*) 08 jun 2007: fixed bug with 2- and 4-bit color, and small interlaced images +*) 04 jun 2007: improved support for Visual Studio 2005: crash with accessing + invalid std::vector element [0] fixed, and level 3 and 4 warnings removed +*) 02 jun 2007: made the encoder add a tag with version by default +*) 27 may 2007: zlib and png code separated (but still in the same file), + simple encoder/decoder functions added for more simple usage cases +*) 19 may 2007: minor fixes, some code cleaning, new error added (error 69), + moved some examples from here to lodepng_examples.cpp +*) 12 may 2007: palette decoding bug fixed +*) 24 apr 2007: changed the license from BSD to the zlib license +*) 11 mar 2007: very simple addition: ability to encode bKGD chunks. +*) 04 mar 2007: (!) tEXt chunk related fixes, and support for encoding + palettized PNG images. Plus little interface change with palette and texts. +*) 03 mar 2007: Made it encode dynamic Huffman shorter with repeat codes. + Fixed a bug where the end code of a block had length 0 in the Huffman tree. +*) 26 feb 2007: Huffman compression with dynamic trees (BTYPE 2) now implemented + and supported by the encoder, resulting in smaller PNGs at the output. +*) 27 jan 2007: Made the Adler-32 test faster so that a timewaste is gone. +*) 24 jan 2007: gave encoder an error interface. Added color conversion from any + greyscale type to 8-bit greyscale with or without alpha. +*) 21 jan 2007: (!) Totally changed the interface. It allows more color types + to convert to and is more uniform. See the manual for how it works now. +*) 07 jan 2007: Some cleanup & fixes, and a few changes over the last days: + encode/decode custom tEXt chunks, separate classes for zlib & deflate, and + at last made the decoder give errors for incorrect Adler32 or Crc. +*) 01 jan 2007: Fixed bug with encoding PNGs with less than 8 bits per channel. +*) 29 dec 2006: Added support for encoding images without alpha channel, and + cleaned out code as well as making certain parts faster. +*) 28 dec 2006: Added "Settings" to the encoder. +*) 26 dec 2006: The encoder now does LZ77 encoding and produces much smaller files now. + Removed some code duplication in the decoder. Fixed little bug in an example. +*) 09 dec 2006: (!) Placed output parameters of public functions as first parameter. + Fixed a bug of the decoder with 16-bit per color. +*) 15 oct 2006: Changed documentation structure +*) 09 oct 2006: Encoder class added. It encodes a valid PNG image from the + given image buffer, however for now it's not compressed. +*) 08 sep 2006: (!) Changed to interface with a Decoder class +*) 30 jul 2006: (!) LodePNG_InfoPng , width and height are now retrieved in different + way. Renamed decodePNG to decodePNGGeneric. +*) 29 jul 2006: (!) Changed the interface: image info is now returned as a + struct of type LodePNG::LodePNG_Info, instead of a vector, which was a bit clumsy. +*) 28 jul 2006: Cleaned the code and added new error checks. + Corrected terminology "deflate" into "inflate". +*) 23 jun 2006: Added SDL example in the documentation in the header, this + example allows easy debugging by displaying the PNG and its transparency. +*) 22 jun 2006: (!) Changed way to obtain error value. Added + loadFile function for convenience. Made decodePNG32 faster. +*) 21 jun 2006: (!) Changed type of info vector to unsigned. + Changed position of palette in info vector. Fixed an important bug that + happened on PNGs with an uncompressed block. +*) 16 jun 2006: Internally changed unsigned into unsigned where + needed, and performed some optimizations. +*) 07 jun 2006: (!) Renamed functions to decodePNG and placed them + in LodePNG namespace. Changed the order of the parameters. Rewrote the + documentation in the header. Renamed files to lodepng.cpp and lodepng.h +*) 22 apr 2006: Optimized and improved some code +*) 07 sep 2005: (!) Changed to std::vector interface +*) 12 aug 2005: Initial release (C++, decoder only) +*/ From d1d5be799b0f940feb0276a61670ddc8e319b52e Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 17:10:28 +0200 Subject: [PATCH 106/123] refactor(vision): share the Pillow-exact resize, add a projector file reader The bicubic resize that reproduces Pillow byte for byte moves out of the DS4V preprocessing into common/vision, with its own test against Pillow output, so every vision model resizes the way it was trained. DS4V keeps a thin wrapper and its output is unchanged. MmprojFile reads a projector published in llama.cpp's "clip" GGUF layout: metadata by key, tensors loaded onto one backend. Co-Authored-By: Claude Fable 5.1 --- server/CMakeLists.txt | 9 +- server/src/common/vision/image_resize.cpp | 257 ++++++++++++++++ server/src/common/vision/image_resize.h | 19 ++ server/src/common/vision/mmproj_file.cpp | 106 +++++++ server/src/common/vision/mmproj_file.h | 49 ++++ .../deepseek4/deepseek4_vision_preprocess.cpp | 200 +------------ server/test/test_image_resize.cpp | 276 ++++++++++++++++++ 7 files changed, 718 insertions(+), 198 deletions(-) create mode 100644 server/src/common/vision/image_resize.cpp create mode 100644 server/src/common/vision/image_resize.h create mode 100644 server/src/common/vision/mmproj_file.cpp create mode 100644 server/src/common/vision/mmproj_file.h create mode 100644 server/test/test_image_resize.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 76e96f997..c89b0d613 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -520,6 +520,8 @@ add_library(dflash_common STATIC src/deepseek4/deepseek4_vision.cpp src/common/gpu_page_pool.cpp src/common/vision/image_decode.cpp + src/common/vision/image_resize.cpp + src/common/vision/mmproj_file.cpp src/deepseek4/deepseek4_vision_preprocess.cpp src/deepseek4/deepseek4_image_prompt.cpp src/deepseek4/deepseek4_image_assembly.cpp @@ -1907,9 +1909,11 @@ if(DFLASH27B_TESTS) target_link_libraries(test_image_decode PRIVATE image_codec_jpeg image_codec_png) list(APPEND _raw_unit_test_targets test_image_decode) endif() + add_executable(test_image_resize test/test_image_resize.cpp src/common/vision/image_resize.cpp) + target_include_directories(test_image_resize PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) add_executable(test_gpu_page_pool test/test_gpu_page_pool.cpp src/common/gpu_page_pool.cpp) target_include_directories(test_gpu_page_pool PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) - list(APPEND _raw_unit_test_targets test_image_input test_gpu_page_pool) + list(APPEND _raw_unit_test_targets test_image_input test_image_resize test_gpu_page_pool) # DS4V image units: each test builds only the unit it covers. foreach(_ds4v_unit assembly integration policy prompt) @@ -1922,7 +1926,8 @@ if(DFLASH27B_TESTS) target_sources(test_ds4v_image_policy PRIVATE src/deepseek4/deepseek4_image_policy.cpp) target_sources(test_ds4v_image_prompt PRIVATE src/deepseek4/deepseek4_image_prompt.cpp - src/deepseek4/deepseek4_vision_preprocess.cpp) + src/deepseek4/deepseek4_vision_preprocess.cpp + src/common/vision/image_resize.cpp) if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/test_moe_source_page_range.cpp") add_executable(test_moe_source_page_range test/test_moe_source_page_range.cpp) diff --git a/server/src/common/vision/image_resize.cpp b/server/src/common/vision/image_resize.cpp new file mode 100644 index 000000000..521f15de3 --- /dev/null +++ b/server/src/common/vision/image_resize.cpp @@ -0,0 +1,257 @@ +#include "image_resize.h" + +#include +#include +#include +#include +#include +#include + +namespace dflash::vision { +namespace { + +struct Status { + bool good = true; + std::string message; + explicit operator bool() const { return good; } +}; +// Pillow's fixed-point precision for 8-bit resampling. +constexpr int kPrecisionBits = 22; + +Status ok() { return {}; } +Status fail(std::string message) { return {false, std::move(message)}; } + +bool checked_mul(std::uint64_t a, std::uint64_t b, std::uint64_t & out) { + if (a != 0 && b > std::numeric_limits::max() / a) return false; + out = a * b; + return true; +} + +struct Coefficients { + int kernel_size = 0; + std::vector bounds; + std::vector weights; +}; + +// Pillow 12.3.0 Resample.c reference used for byte parity: +// https://github.com/python-pillow/Pillow/blob/12.3.0/src/libImaging/Resample.c +double bicubic(double x) { + constexpr double a = -0.5; + if (x < 0.0) { + x = -x; + } + if (x < 1.0) { + return ((a + 2.0) * x - (a + 3.0)) * x * x + 1.0; + } + if (x < 2.0) { + return (((x - 5.0) * x + 8.0) * x - 4.0) * a; + } + return 0.0; +} + +Status precompute_coefficients(int input_size, int output_size, Coefficients & out) { + if (input_size <= 0 || output_size <= 0) { + return fail("resample dimensions must be positive"); + } + const float input_begin = 0.0F; + const float input_end = static_cast(input_size); + double filter_scale = + (static_cast(input_end) - static_cast(input_begin)) / output_size; + const double scale = filter_scale; + if (filter_scale < 1.0) { + filter_scale = 1.0; + } + const double support = 2.0 * filter_scale; + const int kernel_size = static_cast(std::ceil(support)) * 2 + 1; + std::uint64_t coefficient_count = 0; + if (!checked_mul(static_cast(output_size), + static_cast(kernel_size), coefficient_count) || + coefficient_count > std::numeric_limits::max()) { + return fail("resample coefficient count overflow"); + } + + std::vector floating(static_cast(coefficient_count), 0.0); + out.bounds.resize(static_cast(output_size) * 2); + out.weights.resize(static_cast(coefficient_count)); + const double inverse_filter_scale = 1.0 / filter_scale; + for (int output = 0; output < output_size; ++output) { + const double center = input_begin + (output + 0.5) * scale; + int first = static_cast(center - support + 0.5); + if (first < 0) { + first = 0; + } + int count = static_cast(center + support + 0.5); + if (count > input_size) { + count = input_size; + } + count -= first; + double sum = 0.0; + const std::size_t base = static_cast(output) * kernel_size; + for (int index = 0; index < count; ++index) { + const double weight = bicubic( + (index + first - center + 0.5) * inverse_filter_scale); + floating[base + index] = weight; + sum += weight; + } + if (sum != 0.0) { + for (int index = 0; index < count; ++index) { + floating[base + index] /= sum; + } + } + out.bounds[static_cast(output) * 2] = first; + out.bounds[static_cast(output) * 2 + 1] = count; + } + + constexpr double scale_to_fixed = static_cast(1U << kPrecisionBits); + for (std::size_t index = 0; index < floating.size(); ++index) { + const double value = floating[index] * scale_to_fixed; + out.weights[index] = static_cast( + value < 0.0 ? value - 0.5 : value + 0.5); + } + out.kernel_size = kernel_size; + return ok(); +} + +std::uint8_t clip_fixed(std::int32_t value) { + const std::int32_t rounded = value >> kPrecisionBits; + return static_cast(std::clamp(rounded, 0, 255)); +} + +Status resize_horizontal( + const std::vector & input, + int input_width, + int input_height, + int output_width, + std::vector & output) { + Coefficients coeffs; + if (const auto status = precompute_coefficients(input_width, output_width, coeffs); !status) { + return status; + } + output.resize(static_cast(output_width) * input_height * 3); + for (int y = 0; y < input_height; ++y) { + for (int x = 0; x < output_width; ++x) { + const int first = coeffs.bounds[static_cast(x) * 2]; + const int count = coeffs.bounds[static_cast(x) * 2 + 1]; + const std::int32_t * weights = + coeffs.weights.data() + static_cast(x) * coeffs.kernel_size; + for (int channel = 0; channel < 3; ++channel) { + std::int32_t sum = 1 << (kPrecisionBits - 1); + for (int index = 0; index < count; ++index) { + const std::size_t source = + (static_cast(y) * input_width + first + index) * 3 + channel; + sum += static_cast(input[source]) * weights[index]; + } + const std::size_t destination = + (static_cast(y) * output_width + x) * 3 + channel; + output[destination] = clip_fixed(sum); + } + } + } + return ok(); +} + +Status resize_vertical( + const std::vector & input, + int input_width, + int input_height, + int output_height, + std::vector & output) { + Coefficients coeffs; + if (const auto status = precompute_coefficients(input_height, output_height, coeffs); !status) { + return status; + } + output.resize(static_cast(input_width) * output_height * 3); + for (int y = 0; y < output_height; ++y) { + const int first = coeffs.bounds[static_cast(y) * 2]; + const int count = coeffs.bounds[static_cast(y) * 2 + 1]; + const std::int32_t * weights = + coeffs.weights.data() + static_cast(y) * coeffs.kernel_size; + for (int x = 0; x < input_width; ++x) { + for (int channel = 0; channel < 3; ++channel) { + std::int32_t sum = 1 << (kPrecisionBits - 1); + for (int index = 0; index < count; ++index) { + const std::size_t source = + (static_cast(first + index) * input_width + x) * 3 + channel; + sum += static_cast(input[source]) * weights[index]; + } + const std::size_t destination = + (static_cast(y) * input_width + x) * 3 + channel; + output[destination] = clip_fixed(sum); + } + } + } + return ok(); +} + +Status resize_impl( + const std::vector & input, + int input_width, + int input_height, + int output_width, + int output_height, + std::vector & output) { + if (input_width == output_width && input_height == output_height) { + output = input; + return ok(); + } + + std::vector intermediate; + const bool vertical_first = + static_cast(input_height) > + static_cast(input_width) * 100 && + output_height < input_height; + if (vertical_first) { + if (const auto status = resize_vertical( + input, input_width, input_height, output_height, intermediate); + !status) { + return status; + } + if (output_width == input_width) { + output = std::move(intermediate); + return ok(); + } + return resize_horizontal( + intermediate, input_width, output_height, output_width, output); + } + + const std::vector * vertical_input = &input; + int vertical_width = input_width; + if (output_width != input_width) { + if (const auto status = resize_horizontal( + input, input_width, input_height, output_width, intermediate); + !status) { + return status; + } + vertical_input = &intermediate; + vertical_width = output_width; + } + if (output_height != input_height) { + return resize_vertical( + *vertical_input, vertical_width, input_height, output_height, output); + } + output = *vertical_input; + return ok(); +} + +} // namespace + +bool resize_rgb_bicubic(const std::vector & input, int input_width, int input_height, + int output_width, int output_height, + std::vector & output, std::string & error) { + error.clear(); + if (input_width <= 0 || input_height <= 0 || output_width <= 0 || output_height <= 0 || + input.size() != static_cast(input_width) * input_height * 3) { + error = "resize needs positive sizes and width*height*3 input bytes"; + return false; + } + try { + const Status status = resize_impl(input, input_width, input_height, output_width, output_height, output); + if (!status) error = status.message; + return bool(status); + } catch (const std::bad_alloc &) { + error = "resize allocation failed"; + return false; + } +} + +} // namespace dflash::vision diff --git a/server/src/common/vision/image_resize.h b/server/src/common/vision/image_resize.h new file mode 100644 index 000000000..351107ee2 --- /dev/null +++ b/server/src/common/vision/image_resize.h @@ -0,0 +1,19 @@ +// Bicubic resize of 8-bit RGB that reproduces Pillow's Image.resize(BICUBIC) +// byte for byte, including its antialiasing when shrinking. Vision models are +// trained on images resized this way, so every model's preprocessing uses it. +// Reference: Pillow 12.3.0 src/libImaging/Resample.c. +#pragma once + +#include +#include +#include + +namespace dflash::vision { + +// `input` is row-major RGB, three bytes per pixel. Returns false and sets +// `error` on invalid sizes or allocation failure; `output` is then unspecified. +bool resize_rgb_bicubic(const std::vector & input, int input_width, int input_height, + int output_width, int output_height, + std::vector & output, std::string & error); + +} // namespace dflash::vision diff --git a/server/src/common/vision/mmproj_file.cpp b/server/src/common/vision/mmproj_file.cpp new file mode 100644 index 000000000..0ef96068d --- /dev/null +++ b/server/src/common/vision/mmproj_file.cpp @@ -0,0 +1,106 @@ +#include "mmproj_file.h" + +#include "gguf.h" + +#include +#include + +namespace dflash::vision { + +MmprojFile::~MmprojFile() { + if (buffer_) ggml_backend_buffer_free(buffer_); + if (ctx_) ggml_free(ctx_); + if (gguf_) gguf_free(gguf_); +} + +bool MmprojFile::load(const std::string & path, ggml_backend_t backend, std::string & error) { + error.clear(); + if (gguf_ || !backend) { error = "projector is already loaded or has no backend"; return false; } + + gguf_init_params params{}; + params.no_alloc = true; + params.ctx = &ctx_; + gguf_ = gguf_init_from_file(path.c_str(), params); + if (!gguf_ || !ctx_) { error = "cannot read projector GGUF: " + path; return false; } + + const int64_t arch = gguf_find_key(gguf_, "general.architecture"); + if (arch < 0 || gguf_get_kv_type(gguf_, arch) != GGUF_TYPE_STRING || + std::string(gguf_get_val_str(gguf_, arch)) != "clip") { + error = "not a clip projector file (general.architecture != clip)"; + return false; + } + + buffer_ = ggml_backend_alloc_ctx_tensors(ctx_, backend); + if (!buffer_) { error = "cannot allocate projector weights on the backend"; return false; } + ggml_backend_buffer_set_usage(buffer_, GGML_BACKEND_BUFFER_USAGE_WEIGHTS); + weight_bytes_ = ggml_backend_buffer_get_size(buffer_); + + std::unique_ptr file(std::fopen(path.c_str(), "rb"), std::fclose); + if (!file) { error = "cannot open projector file: " + path; return false; } + const size_t data_offset = gguf_get_data_offset(gguf_); + std::vector staging; + const int64_t n_tensors = gguf_get_n_tensors(gguf_); + for (int64_t i = 0; i < n_tensors; ++i) { + const char * name = gguf_get_tensor_name(gguf_, i); + ggml_tensor * t = ggml_get_tensor(ctx_, name); + if (!t) { error = std::string("projector tensor has no metadata: ") + name; return false; } + const size_t size = ggml_nbytes(t); + staging.resize(size); + const size_t offset = data_offset + gguf_get_tensor_offset(gguf_, i); +#if defined(_WIN32) + const int seek = _fseeki64(file.get(), (long long) offset, SEEK_SET); +#else + const int seek = fseeko(file.get(), (off_t) offset, SEEK_SET); +#endif + if (seek != 0 || std::fread(staging.data(), 1, size, file.get()) != size) { + error = std::string("projector file is truncated at tensor ") + name; + return false; + } + ggml_backend_tensor_set(t, staging.data(), 0, size); + } + return true; +} + +std::string MmprojFile::projector_type() const { + if (!gguf_) return {}; + const int64_t id = gguf_find_key(gguf_, "clip.projector_type"); + if (id < 0 || gguf_get_kv_type(gguf_, id) != GGUF_TYPE_STRING) return {}; + return gguf_get_val_str(gguf_, id); +} + +bool MmprojFile::u32(const char * key, uint32_t & out) const { + const int64_t id = gguf_ ? gguf_find_key(gguf_, key) : -1; + if (id < 0 || gguf_get_kv_type(gguf_, id) != GGUF_TYPE_UINT32) return false; + out = gguf_get_val_u32(gguf_, id); + return true; +} + +bool MmprojFile::f32(const char * key, float & out) const { + const int64_t id = gguf_ ? gguf_find_key(gguf_, key) : -1; + if (id < 0 || gguf_get_kv_type(gguf_, id) != GGUF_TYPE_FLOAT32) return false; + out = gguf_get_val_f32(gguf_, id); + return true; +} + +bool MmprojFile::f32_array(const char * key, std::vector & out) const { + const int64_t id = gguf_ ? gguf_find_key(gguf_, key) : -1; + if (id < 0 || gguf_get_kv_type(gguf_, id) != GGUF_TYPE_ARRAY || + gguf_get_arr_type(gguf_, id) != GGUF_TYPE_FLOAT32) return false; + const auto * values = static_cast(gguf_get_arr_data(gguf_, id)); + out.assign(values, values + gguf_get_arr_n(gguf_, id)); + return true; +} + +ggml_tensor * MmprojFile::tensor(const std::string & name) const { + return ctx_ ? ggml_get_tensor(ctx_, name.c_str()) : nullptr; +} + +bool MmprojFile::has_tensor_prefix(const std::string & prefix) const { + const int64_t n_tensors = gguf_ ? gguf_get_n_tensors(gguf_) : 0; + for (int64_t i = 0; i < n_tensors; ++i) { + if (std::string(gguf_get_tensor_name(gguf_, i)).rfind(prefix, 0) == 0) return true; + } + return false; +} + +} // namespace dflash::vision diff --git a/server/src/common/vision/mmproj_file.h b/server/src/common/vision/mmproj_file.h new file mode 100644 index 000000000..16d8c0cee --- /dev/null +++ b/server/src/common/vision/mmproj_file.h @@ -0,0 +1,49 @@ +// A multimodal projector file in llama.cpp's "clip" GGUF layout: the standard +// file published next to a vision-capable model. Holds the metadata and the +// tensors, loaded onto one backend. Each model's tower reads what it needs. +#pragma once + +#include "ggml-backend.h" +#include "ggml.h" + +#include +#include +#include + +struct gguf_context; + +namespace dflash::vision { + +class MmprojFile { +public: + MmprojFile() = default; + ~MmprojFile(); + MmprojFile(const MmprojFile &) = delete; + MmprojFile & operator=(const MmprojFile &) = delete; + + // Loads every tensor onto `backend`, which must outlive this object. + bool load(const std::string & path, ggml_backend_t backend, std::string & error); + + // "clip.projector_type", for example "qwen3vl_merger". Empty when absent. + std::string projector_type() const; + + // Metadata by full key. False when the key is absent or has another type. + bool u32(const char * key, uint32_t & out) const; + bool f32(const char * key, float & out) const; + bool f32_array(const char * key, std::vector & out) const; + + // nullptr when the file has no tensor of that name. + ggml_tensor * tensor(const std::string & name) const; + // True when any tensor name starts with `prefix`. + bool has_tensor_prefix(const std::string & prefix) const; + + size_t weight_bytes() const { return weight_bytes_; } + +private: + gguf_context * gguf_ = nullptr; + ggml_context * ctx_ = nullptr; + ggml_backend_buffer_t buffer_ = nullptr; + size_t weight_bytes_ = 0; +}; + +} // namespace dflash::vision diff --git a/server/src/deepseek4/deepseek4_vision_preprocess.cpp b/server/src/deepseek4/deepseek4_vision_preprocess.cpp index 6cd09f1bc..182794997 100644 --- a/server/src/deepseek4/deepseek4_vision_preprocess.cpp +++ b/server/src/deepseek4/deepseek4_vision_preprocess.cpp @@ -1,4 +1,5 @@ #include "deepseek4_vision_preprocess.h" +#include "../common/vision/image_resize.h" #include #include @@ -10,7 +11,6 @@ namespace dflash::vision { namespace { -constexpr int kPrecisionBits = 22; PreprocessStatus ok() { return {}; @@ -163,162 +163,6 @@ PreprocessStatus solve_resize_ratio( return grid_tokens(best_height, best_width, config, grid); } -struct Coefficients { - int kernel_size = 0; - std::vector bounds; - std::vector weights; -}; - -// Pillow 12.3.0 Resample.c reference used for byte parity: -// https://github.com/python-pillow/Pillow/blob/12.3.0/src/libImaging/Resample.c -double bicubic(double x) { - constexpr double a = -0.5; - if (x < 0.0) { - x = -x; - } - if (x < 1.0) { - return ((a + 2.0) * x - (a + 3.0)) * x * x + 1.0; - } - if (x < 2.0) { - return (((x - 5.0) * x + 8.0) * x - 4.0) * a; - } - return 0.0; -} - -PreprocessStatus precompute_coefficients(int input_size, int output_size, Coefficients & out) { - if (input_size <= 0 || output_size <= 0) { - return fail(PreprocessError::ResizePlanFailed, "resample dimensions must be positive"); - } - const float input_begin = 0.0F; - const float input_end = static_cast(input_size); - double filter_scale = - (static_cast(input_end) - static_cast(input_begin)) / output_size; - const double scale = filter_scale; - if (filter_scale < 1.0) { - filter_scale = 1.0; - } - const double support = 2.0 * filter_scale; - const int kernel_size = static_cast(std::ceil(support)) * 2 + 1; - std::uint64_t coefficient_count = 0; - if (!checked_mul(static_cast(output_size), - static_cast(kernel_size), coefficient_count) || - coefficient_count > std::numeric_limits::max()) { - return fail(PreprocessError::OutputTooLarge, "resample coefficient count overflow"); - } - - std::vector floating(static_cast(coefficient_count), 0.0); - out.bounds.resize(static_cast(output_size) * 2); - out.weights.resize(static_cast(coefficient_count)); - const double inverse_filter_scale = 1.0 / filter_scale; - for (int output = 0; output < output_size; ++output) { - const double center = input_begin + (output + 0.5) * scale; - int first = static_cast(center - support + 0.5); - if (first < 0) { - first = 0; - } - int count = static_cast(center + support + 0.5); - if (count > input_size) { - count = input_size; - } - count -= first; - double sum = 0.0; - const std::size_t base = static_cast(output) * kernel_size; - for (int index = 0; index < count; ++index) { - const double weight = bicubic( - (index + first - center + 0.5) * inverse_filter_scale); - floating[base + index] = weight; - sum += weight; - } - if (sum != 0.0) { - for (int index = 0; index < count; ++index) { - floating[base + index] /= sum; - } - } - out.bounds[static_cast(output) * 2] = first; - out.bounds[static_cast(output) * 2 + 1] = count; - } - - constexpr double scale_to_fixed = static_cast(1U << kPrecisionBits); - for (std::size_t index = 0; index < floating.size(); ++index) { - const double value = floating[index] * scale_to_fixed; - out.weights[index] = static_cast( - value < 0.0 ? value - 0.5 : value + 0.5); - } - out.kernel_size = kernel_size; - return ok(); -} - -std::uint8_t clip_fixed(std::int32_t value) { - const std::int32_t rounded = value >> kPrecisionBits; - return static_cast(std::clamp(rounded, 0, 255)); -} - -PreprocessStatus resize_horizontal( - const std::vector & input, - int input_width, - int input_height, - int output_width, - std::vector & output) { - Coefficients coeffs; - if (const auto status = precompute_coefficients(input_width, output_width, coeffs); !status) { - return status; - } - output.resize(static_cast(output_width) * input_height * 3); - for (int y = 0; y < input_height; ++y) { - for (int x = 0; x < output_width; ++x) { - const int first = coeffs.bounds[static_cast(x) * 2]; - const int count = coeffs.bounds[static_cast(x) * 2 + 1]; - const std::int32_t * weights = - coeffs.weights.data() + static_cast(x) * coeffs.kernel_size; - for (int channel = 0; channel < 3; ++channel) { - std::int32_t sum = 1 << (kPrecisionBits - 1); - for (int index = 0; index < count; ++index) { - const std::size_t source = - (static_cast(y) * input_width + first + index) * 3 + channel; - sum += static_cast(input[source]) * weights[index]; - } - const std::size_t destination = - (static_cast(y) * output_width + x) * 3 + channel; - output[destination] = clip_fixed(sum); - } - } - } - return ok(); -} - -PreprocessStatus resize_vertical( - const std::vector & input, - int input_width, - int input_height, - int output_height, - std::vector & output) { - Coefficients coeffs; - if (const auto status = precompute_coefficients(input_height, output_height, coeffs); !status) { - return status; - } - output.resize(static_cast(input_width) * output_height * 3); - for (int y = 0; y < output_height; ++y) { - const int first = coeffs.bounds[static_cast(y) * 2]; - const int count = coeffs.bounds[static_cast(y) * 2 + 1]; - const std::int32_t * weights = - coeffs.weights.data() + static_cast(y) * coeffs.kernel_size; - for (int x = 0; x < input_width; ++x) { - for (int channel = 0; channel < 3; ++channel) { - std::int32_t sum = 1 << (kPrecisionBits - 1); - for (int index = 0; index < count; ++index) { - const std::size_t source = - (static_cast(first + index) * input_width + x) * 3 + channel; - sum += static_cast(input[source]) * weights[index]; - } - const std::size_t destination = - (static_cast(y) * input_width + x) * 3 + channel; - output[destination] = clip_fixed(sum); - } - } - } - return ok(); -} - PreprocessStatus pillow_resize( const std::vector & input, int input_width, @@ -326,46 +170,10 @@ PreprocessStatus pillow_resize( int output_width, int output_height, std::vector & output) { - if (input_width == output_width && input_height == output_height) { - output = input; - return ok(); - } - - std::vector intermediate; - const bool vertical_first = - static_cast(input_height) > - static_cast(input_width) * 100 && - output_height < input_height; - if (vertical_first) { - if (const auto status = resize_vertical( - input, input_width, input_height, output_height, intermediate); - !status) { - return status; - } - if (output_width == input_width) { - output = std::move(intermediate); - return ok(); - } - return resize_horizontal( - intermediate, input_width, output_height, output_width, output); - } - - const std::vector * vertical_input = &input; - int vertical_width = input_width; - if (output_width != input_width) { - if (const auto status = resize_horizontal( - input, input_width, input_height, output_width, intermediate); - !status) { - return status; - } - vertical_input = &intermediate; - vertical_width = output_width; - } - if (output_height != input_height) { - return resize_vertical( - *vertical_input, vertical_width, input_height, output_height, output); + std::string message; + if (!resize_rgb_bicubic(input, input_width, input_height, output_width, output_height, output, message)) { + return fail(PreprocessError::ResizePlanFailed, message); } - output = *vertical_input; return ok(); } diff --git a/server/test/test_image_resize.cpp b/server/test/test_image_resize.cpp new file mode 100644 index 000000000..991161fcf --- /dev/null +++ b/server/test/test_image_resize.cpp @@ -0,0 +1,276 @@ +// resize_rgb_bicubic must reproduce Pillow's Image.resize(BICUBIC) byte for byte. +// Expected data generated with Pillow 12.2.0. +#include "common/vision/image_resize.h" + +#include +#include +#include + +using dflash::vision::resize_rgb_bicubic; + +static int failures = 0; +static void check(bool condition, const char * message) { + if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); ++failures; } +} + +// 23x17 RGB source with hard edges and noise. +const std::vector SOURCE = { + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 214, 35, 123, 46, 217, 30, 63, 114, 31, 203, 25, 113, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 32, 30, 105, 254, 218, 160, 238, 232, 185, 153, 127, 92, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 77, 250, 215, 20, 39, 160, 174, 179, 254, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 11, 236, 181, 86, 59, 252, 30, 111, 147, 66, 126, 203, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 194, 118, 77, 42, 90, 77, 118, 119, 6, 248, 93, 134, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 201, 53, 246, 205, 31, 97, 34, 106, 225, 0, 0, 0, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 192, 76, 129, 177, 186, 242, 62, 59, 249, 238, 245, 247, 0, 0, 0, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 13, 152, 46, 133, 187, 85, 182, 114, 168, 114, 99, 122, 0, 0, 0, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 228, 178, 186, 41, 112, 52, 116, 240, 100, 172, 104, 247, 0, 245, 176, 43, 61, 198, + 102, 244, 91, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 238, 74, 242, 179, 79, 67, 10, 7, 52, + 71, 222, 99, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 234, 215, 66, 77, 9, 225, 93, 2, 76, + 88, 72, 242, 255, 255, 255, 0, 0, 0, 255, 255, 255, 21, 50, 231, 14, 32, 226, 166, 102, 141, 231, 244, 126, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 37, 108, 155, 62, 79, 187, 73, 129, 70, 239, 112, 48, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 47, 187, 9, 173, 234, 225, 9, 196, 169, 151, 32, 57, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 216, 132, 207, 76, 253, 167, 45, 142, 29, 93, 217, 37, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 213, 137, 66, 22, 122, 56, 82, 134, 25, 92, 103, 159, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 97, 243, 125, 228, 54, 221, 253, 201, 157, 110, 117, 175, 255, 255, 255, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 137, 228, 1, 134, 186, 168, + 165, 125, 17, 158, 111, 182, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 45, 73, 204, 21, 201, 11, + 153, 155, 119, 43, 79, 199, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 21, 68, 184, 53, 192, 231, + 25, 9, 125, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 105, 118, 235, 252, 195, 39, 245, 147, 23, + 101, 39, 75, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 148, 146, 237, 238, 238, 60, 102, 159, 43, + 242, 8, 148, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 67, 143, 57, 186, 118, 254, 248, 201, 12, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 169, 0, 166, 173, 203, 61, 100, 6, 148, 129, 190, 33, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 127, 136, 223, 161, 97, 191, 219, 14, 204, 104, 41, 25, + 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 152, 130, 133, 207, 122, 154, 247, 201, 61, 85, 82, 38, + 106, 254, 112, 231, 170, 230, 218, 71, 98, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 196, 211, 107, + 192, 138, 173, 31, 255, 142, 184, 64, 110, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 242, 250, 0, + 37, 200, 239, 229, 127, 55, 114, 79, 77, 255, 255, 255, 0, 0, 0, 255, 255, 255, 65, 128, 223, 57, 50, 36, + 153, 98, 198, 133, 114, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 28, 11, 99, 255, 215, 41, + 131, 116, 217, 189, 116, 252, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 159, 99, 118, 238, 113, 135, + 151, 55, 253, 95, 114, 248, 0, 0, 0, 255, 255, 255, 0, 0, 0, 26, 30, 94, 201, 230, 160, 57, 40, 84, + 168, 97, 94, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 41, 179, 215, 63, 106, 194, 182, 158, 221, + 44, 25, 242, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 192, 17, 237, 32, 31, 131, 99, 32, 173, + 185, 139, 171, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 77, 120, 167, 163, 235, 185, 40, 101, 200, 81, 126, 208, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 215, 255, 228, 88, 119, 68, 213, 235, 120, 62, 150, 150, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 144, 96, 167, 33, 202, 128, 125, 118, 51, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 25, 97, 99, + 38, 190, 91, 229, 133, 3, 54, 179, 111, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 190, 94, 159, + 39, 104, 16, 253, 247, 32, 208, 51, 202, 255, 255, 255, 0, 0, 0, 255, 255, 255, 0, 0, 0, 212, 213, 9, + 186, 100, 200, 207, 104, 3, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 72, 203, 45, 189, 87, 74, + 178, 145, 82, 87, 34, 55, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 113, 207, 100, 242, 93, 111, + 21, 204, 80, 196, 183, 63, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, 127, 217, 199, 188, 228, 224, + 91, 11, 1, 250, 238, 120, 228, 234, 91, 242, 204, 54, 34, 65, 183, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 0, 0, 0, 69, 13, 33, 56, 99, 67, 251, 147, 84, 113, 33, 179, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 0, 0, 0, 190, 18, 101, 93, 206, 82, 142, 167, 192, 86, 135, 58, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 188, 74, 184, 169, 41, 226, 117, 90, 24, 151, 129, 158, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 116, 23, 11, 27, 1, 181, 155, 54, 182, 114, 211, 154, 0, 0, 0, 255, 255, 255, 0, 0, 0, 255, 255, 255, + 74, 138, 205, 135, 5, 28, 179, 227, 252, 127, 84, 0, 0, 0, 0, 255, 255, 255, 0, 0, 0, +}; + +// Pillow: 9x11 (shrinks both ways, antialiased). +const std::vector PILLOW_9X11 = { + 126, 124, 123, 125, 133, 137, 80, 110, 106, 129, 112, 128, 125, 117, 124, 174, 151, 113, 143, 122, 115, 129, 139, 150, + 110, 107, 200, 118, 130, 127, 134, 144, 158, 115, 129, 220, 135, 124, 136, 114, 145, 112, 125, 127, 94, 130, 102, 116, + 152, 126, 156, 132, 139, 132, 61, 112, 206, 139, 165, 134, 132, 126, 135, 118, 81, 139, 112, 131, 90, 132, 133, 128, + 122, 143, 115, 102, 88, 154, 133, 135, 126, 98, 141, 152, 136, 183, 100, 129, 125, 123, 89, 112, 83, 123, 132, 120, + 113, 129, 125, 119, 150, 131, 163, 148, 166, 114, 126, 145, 128, 127, 125, 132, 156, 121, 178, 141, 77, 127, 113, 122, + 118, 132, 150, 123, 153, 101, 132, 101, 137, 112, 128, 133, 103, 126, 157, 123, 135, 117, 172, 123, 161, 143, 110, 86, + 116, 128, 102, 148, 154, 169, 169, 90, 149, 120, 103, 88, 144, 143, 133, 202, 158, 125, 106, 128, 125, 159, 115, 111, + 122, 124, 125, 148, 149, 121, 144, 130, 164, 126, 123, 124, 168, 150, 123, 140, 105, 162, 124, 125, 127, 100, 103, 116, + 149, 134, 134, 107, 128, 146, 79, 114, 154, 147, 135, 212, 131, 144, 126, 106, 53, 141, 145, 111, 184, 128, 148, 136, + 129, 125, 130, 94, 130, 124, 104, 154, 118, 120, 133, 134, 137, 133, 132, 160, 166, 92, 139, 131, 142, 135, 135, 117, + 152, 138, 95, 155, 153, 109, 134, 160, 129, 133, 108, 72, 95, 86, 82, 162, 148, 147, 138, 148, 96, 121, 109, 90, + 142, 172, 154, 158, 153, 142, 195, 128, 137, 121, 112, 118, 128, 127, 127, 100, 53, 98, 128, 111, 144, 125, 128, 131, + 131, 114, 137, 127, 133, 105, 119, 119, 116, +}; + +// Pillow: 40x31 (enlarges both ways). +const std::vector PILLOW_40X31 = { + 0, 0, 0, 81, 81, 81, 255, 255, 255, 122, 122, 122, 0, 0, 0, 179, 195, 187, 255, 243, 255, 253, 45, 151, + 159, 81, 68, 46, 228, 14, 34, 185, 6, 79, 102, 29, 192, 43, 92, 167, 0, 77, 0, 0, 0, 93, 108, 101, + 255, 255, 255, 100, 100, 100, 0, 0, 0, 214, 214, 207, 228, 230, 254, 33, 39, 122, 109, 91, 118, 255, 230, 168, + 255, 254, 198, 237, 231, 190, 179, 165, 121, 84, 70, 41, 0, 0, 0, 120, 121, 123, 255, 255, 255, 78, 78, 78, + 5, 2, 3, 227, 213, 218, 237, 255, 255, 82, 255, 219, 13, 147, 180, 10, 39, 167, 127, 132, 227, 200, 196, 255, + 76, 76, 76, 111, 111, 111, 178, 178, 178, 126, 126, 126, 78, 78, 78, 146, 151, 147, 179, 168, 180, 166, 102, 150, + 116, 117, 120, 55, 171, 91, 40, 149, 64, 61, 107, 63, 138, 62, 118, 148, 46, 125, 81, 75, 75, 115, 124, 117, + 179, 179, 179, 118, 118, 118, 81, 81, 81, 159, 160, 157, 160, 152, 166, 85, 59, 105, 122, 97, 109, 196, 182, 139, + 203, 206, 139, 203, 194, 129, 195, 140, 111, 145, 97, 91, 76, 75, 76, 121, 126, 127, 179, 179, 179, 110, 110, 110, + 84, 84, 83, 162, 156, 154, 167, 179, 197, 115, 196, 226, 84, 110, 183, 75, 35, 147, 114, 112, 215, 139, 165, 254, + 243, 243, 243, 163, 163, 163, 14, 14, 14, 132, 132, 132, 239, 239, 239, 90, 74, 78, 12, 39, 26, 13, 201, 149, + 44, 182, 215, 77, 70, 232, 53, 84, 176, 31, 115, 131, 45, 103, 169, 117, 145, 214, 237, 243, 243, 153, 151, 145, + 12, 12, 12, 149, 149, 149, 232, 233, 232, 63, 68, 70, 41, 19, 11, 175, 97, 73, 141, 109, 91, 58, 97, 84, + 83, 119, 34, 144, 126, 24, 221, 94, 96, 249, 142, 178, 243, 242, 243, 125, 135, 133, 12, 12, 12, 166, 166, 166, + 223, 224, 222, 48, 57, 44, 48, 12, 70, 177, 63, 237, 212, 46, 185, 190, 30, 106, 91, 81, 189, 32, 113, 243, + 162, 166, 158, 140, 138, 134, 98, 85, 89, 135, 128, 126, 167, 166, 160, 120, 106, 111, 90, 98, 97, 64, 168, 141, + 93, 161, 209, 134, 110, 255, 74, 81, 236, 34, 87, 196, 103, 163, 226, 152, 197, 227, 162, 175, 166, 129, 133, 124, + 88, 93, 93, 137, 140, 138, 167, 166, 165, 113, 105, 116, 96, 91, 75, 120, 128, 54, 98, 136, 62, 71, 129, 79, + 100, 112, 59, 144, 99, 62, 195, 94, 118, 193, 117, 155, 160, 160, 168, 122, 132, 128, 93, 97, 86, 142, 146, 140, + 158, 165, 159, 99, 110, 93, 119, 89, 125, 214, 86, 219, 202, 71, 155, 149, 71, 77, 80, 127, 133, 43, 161, 174, + 2, 3, 1, 89, 88, 87, 251, 248, 249, 125, 123, 122, 9, 9, 7, 173, 180, 177, 252, 232, 244, 201, 98, 142, + 185, 110, 161, 180, 187, 243, 100, 109, 253, 68, 66, 249, 203, 208, 255, 194, 201, 190, 16, 20, 15, 95, 96, 94, + 252, 253, 253, 105, 106, 106, 17, 16, 17, 203, 191, 200, 209, 243, 218, 31, 160, 63, 44, 164, 43, 134, 188, 87, + 172, 144, 139, 176, 107, 165, 141, 108, 143, 73, 64, 80, 1, 1, 4, 122, 124, 122, 252, 253, 250, 87, 88, 86, + 23, 25, 24, 202, 206, 203, 253, 250, 251, 236, 181, 196, 133, 131, 115, 42, 115, 52, 85, 196, 80, 118, 247, 104, + 92, 36, 118, 105, 125, 153, 138, 255, 225, 57, 141, 158, 4, 18, 91, 117, 186, 148, 208, 255, 196, 255, 188, 212, + 188, 131, 162, 76, 81, 98, 127, 131, 200, 180, 174, 255, 130, 136, 150, 103, 82, 94, 112, 21, 111, 181, 86, 144, + 239, 179, 165, 87, 54, 66, 0, 0, 20, 115, 189, 146, 175, 255, 204, 152, 230, 180, 94, 154, 95, 53, 79, 25, + 178, 160, 163, 225, 189, 229, 74, 67, 91, 37, 36, 18, 111, 93, 14, 157, 125, 140, 170, 125, 254, 78, 21, 95, + 49, 0, 40, 144, 131, 224, 215, 220, 255, 255, 246, 241, 123, 135, 110, 0, 46, 7, 133, 194, 124, 214, 255, 191, + 185, 94, 255, 114, 152, 228, 0, 236, 178, 8, 144, 192, 53, 67, 195, 79, 183, 111, 140, 253, 116, 242, 250, 239, + 158, 151, 160, 6, 6, 4, 142, 142, 138, 241, 242, 236, 61, 74, 59, 38, 4, 43, 211, 66, 225, 236, 85, 194, + 173, 74, 68, 73, 24, 47, 9, 27, 56, 40, 163, 71, 128, 246, 141, 248, 247, 249, 147, 134, 146, 6, 5, 8, + 158, 159, 159, 230, 232, 232, 49, 50, 61, 56, 55, 6, 222, 212, 53, 176, 139, 149, 74, 7, 224, 79, 6, 129, + 90, 15, 94, 77, 42, 201, 143, 133, 246, 245, 247, 246, 127, 128, 118, 12, 10, 12, 166, 163, 166, 249, 245, 249, + 75, 65, 252, 40, 76, 238, 0, 92, 211, 57, 81, 195, 141, 96, 174, 184, 210, 134, 168, 220, 100, 86, 95, 77, + 107, 107, 114, 173, 173, 170, 123, 123, 116, 85, 85, 76, 148, 149, 138, 163, 162, 186, 116, 108, 205, 104, 80, 187, + 104, 68, 145, 63, 72, 99, 60, 93, 63, 154, 141, 51, 164, 135, 55, 90, 78, 82, 122, 118, 130, 176, 173, 178, + 113, 114, 114, 83, 85, 85, 156, 152, 163, 169, 193, 138, 125, 203, 27, 126, 184, 110, 125, 153, 226, 56, 145, 181, + 34, 110, 132, 106, 46, 120, 113, 35, 92, 74, 77, 76, 128, 134, 130, 179, 175, 178, 114, 109, 115, 79, 74, 80, + 39, 58, 230, 27, 52, 231, 17, 44, 224, 87, 70, 172, 169, 121, 122, 222, 220, 122, 181, 203, 92, 32, 34, 18, + 92, 89, 101, 238, 237, 237, 115, 115, 115, 24, 23, 25, 184, 180, 177, 213, 228, 232, 73, 134, 153, 33, 91, 153, + 48, 83, 176, 55, 116, 116, 96, 138, 60, 209, 120, 58, 182, 73, 40, 27, 17, 15, 109, 117, 119, 240, 238, 240, + 99, 98, 99, 31, 30, 32, 199, 188, 200, 198, 243, 188, 52, 197, 29, 103, 206, 102, 184, 224, 227, 87, 228, 210, + 44, 185, 156, 133, 72, 83, 109, 11, 30, 17, 17, 16, 130, 136, 133, 237, 235, 235, 94, 94, 93, 18, 18, 16, + 188, 112, 230, 137, 144, 217, 52, 190, 182, 55, 166, 113, 85, 146, 52, 103, 198, 32, 139, 224, 76, 191, 191, 189, + 145, 131, 156, 61, 57, 59, 133, 133, 138, 186, 190, 197, 90, 97, 99, 80, 71, 58, 173, 132, 86, 118, 129, 89, + 12, 102, 77, 44, 129, 48, 94, 142, 44, 109, 98, 96, 147, 117, 150, 200, 195, 187, 144, 138, 136, 69, 56, 70, + 146, 141, 150, 184, 183, 184, 96, 83, 86, 59, 110, 55, 78, 238, 94, 146, 180, 162, 218, 91, 220, 220, 176, 199, + 193, 207, 165, 124, 106, 138, 137, 114, 154, 201, 198, 189, 134, 128, 119, 68, 58, 56, 155, 152, 146, 201, 203, 194, + 187, 95, 158, 165, 163, 172, 129, 255, 191, 61, 192, 93, 22, 109, 6, 79, 183, 44, 160, 248, 117, 248, 255, 208, + 160, 173, 144, 8, 22, 21, 143, 145, 133, 244, 237, 217, 77, 72, 83, 37, 10, 14, 159, 87, 32, 147, 135, 66, + 76, 160, 95, 56, 125, 36, 64, 99, 22, 99, 123, 141, 160, 172, 229, 230, 235, 255, 115, 138, 134, 0, 24, 0, + 153, 170, 145, 236, 233, 234, 45, 47, 75, 0, 37, 25, 61, 180, 88, 158, 153, 173, 246, 90, 234, 230, 125, 158, + 198, 156, 121, 151, 154, 184, 172, 189, 238, 225, 234, 255, 101, 124, 140, 0, 26, 32, 140, 150, 173, 218, 214, 247, + 20, 3, 14, 98, 98, 99, 244, 255, 255, 111, 135, 117, 0, 10, 0, 164, 179, 167, 248, 255, 213, 179, 253, 53, + 130, 210, 60, 103, 157, 147, 144, 148, 97, 178, 143, 60, 158, 116, 139, 101, 65, 124, 18, 2, 0, 107, 108, 90, + 237, 252, 240, 92, 105, 89, 9, 16, 0, 190, 193, 188, 223, 228, 255, 95, 112, 230, 21, 116, 113, 1, 166, 3, + 114, 181, 78, 167, 162, 160, 60, 94, 180, 0, 40, 117, 0, 19, 5, 133, 129, 127, 255, 238, 255, 107, 87, 94, + 43, 35, 27, 215, 213, 211, 208, 220, 255, 68, 101, 210, 31, 126, 198, 33, 161, 190, 43, 79, 165, 46, 28, 148, + 76, 83, 77, 110, 110, 108, 174, 162, 168, 132, 121, 128, 91, 84, 93, 150, 135, 158, 168, 167, 161, 120, 193, 74, + 136, 204, 86, 183, 203, 139, 190, 164, 54, 188, 122, 3, 167, 99, 118, 129, 83, 156, 79, 79, 93, 116, 122, 122, + 180, 172, 177, 125, 119, 127, 91, 88, 93, 159, 156, 145, 153, 154, 183, 73, 85, 218, 68, 145, 131, 96, 234, 22, + 115, 200, 37, 125, 139, 97, 120, 78, 185, 101, 54, 176, 86, 72, 87, 124, 128, 117, 164, 177, 165, 103, 111, 109, + 84, 86, 88, 163, 159, 152, 131, 148, 164, 21, 81, 133, 46, 131, 204, 101, 180, 255, 89, 96, 138, 80, 48, 67, + 248, 249, 248, 165, 165, 165, 9, 8, 9, 132, 131, 132, 244, 244, 245, 82, 79, 73, 6, 10, 34, 78, 104, 200, + 163, 164, 176, 245, 194, 52, 254, 182, 16, 234, 140, 19, 132, 52, 52, 132, 82, 131, 244, 237, 244, 152, 157, 154, + 7, 6, 6, 150, 150, 150, 238, 237, 237, 63, 63, 55, 19, 18, 49, 122, 121, 221, 195, 197, 175, 228, 242, 51, + 145, 210, 27, 108, 139, 53, 202, 26, 125, 248, 73, 198, 249, 241, 249, 126, 140, 131, 7, 7, 7, 167, 168, 168, + 229, 228, 229, 53, 48, 53, 3, 28, 6, 52, 130, 51, 117, 135, 175, 183, 126, 254, 221, 167, 98, 238, 194, 4, + 156, 156, 158, 138, 135, 139, 104, 96, 104, 126, 123, 125, 146, 151, 146, 112, 125, 110, 97, 88, 124, 115, 42, 210, + 170, 107, 160, 234, 209, 47, 207, 142, 51, 171, 69, 72, 129, 93, 50, 121, 124, 73, 148, 147, 151, 128, 130, 137, + 98, 101, 99, 134, 127, 130, 156, 144, 149, 112, 114, 107, 101, 110, 128, 130, 134, 224, 183, 166, 200, 228, 183, 131, + 180, 129, 112, 150, 68, 102, 191, 22, 89, 185, 51, 112, 147, 142, 160, 123, 129, 132, 107, 98, 96, 134, 134, 136, + 143, 149, 154, 112, 113, 118, 97, 111, 92, 98, 134, 71, 154, 125, 156, 221, 125, 216, 253, 182, 84, 255, 219, 5, + 0, 0, 0, 88, 88, 88, 252, 252, 252, 123, 123, 123, 5, 6, 5, 175, 187, 175, 255, 220, 255, 184, 27, 189, + 168, 64, 118, 174, 202, 62, 131, 95, 110, 100, 12, 144, 128, 147, 70, 97, 156, 11, 1, 10, 0, 103, 99, 110, + 255, 255, 255, 104, 104, 104, 12, 12, 11, 195, 194, 188, 241, 246, 255, 139, 151, 230, 129, 116, 207, 162, 96, 191, + 206, 43, 210, 210, 13, 184, 138, 36, 66, 59, 23, 0, 0, 0, 0, 123, 127, 129, 254, 254, 254, 85, 85, 85, + 21, 22, 22, 208, 210, 209, 241, 237, 236, 157, 138, 140, 170, 113, 141, 210, 125, 151, 237, 176, 90, 250, 207, 54, + 33, 19, 0, 85, 114, 59, 185, 255, 189, 155, 192, 162, 126, 83, 118, 213, 133, 161, 255, 169, 203, 247, 161, 237, + 164, 120, 144, 56, 73, 12, 132, 119, 135, 190, 152, 225, 83, 96, 63, 41, 76, 0, 91, 107, 33, 171, 156, 131, + 230, 199, 232, 85, 167, 135, 7, 136, 62, 167, 147, 140, 247, 181, 206, 222, 224, 255, 133, 131, 176, 50, 17, 74, + 188, 105, 197, 246, 147, 238, 71, 30, 51, 32, 24, 0, 120, 124, 0, 138, 204, 124, 134, 243, 255, 120, 125, 101, + 128, 65, 8, 175, 141, 119, 215, 198, 195, 235, 221, 227, 147, 119, 132, 78, 40, 46, 203, 176, 138, 255, 251, 187, + 85, 75, 47, 85, 138, 61, 93, 237, 96, 171, 222, 184, 240, 164, 234, 222, 82, 125, 221, 99, 111, 241, 236, 233, + 149, 164, 161, 11, 10, 15, 141, 142, 141, 236, 240, 235, 70, 67, 78, 34, 36, 18, 170, 185, 93, 214, 188, 137, + 195, 139, 161, 94, 217, 161, 44, 242, 147, 138, 98, 112, 214, 97, 149, 243, 236, 239, 137, 146, 139, 10, 13, 9, + 155, 161, 155, 224, 231, 228, 51, 55, 71, 66, 64, 15, 237, 234, 8, 158, 244, 118, 43, 191, 232, 164, 153, 138, + 226, 113, 58, 125, 71, 64, 148, 130, 139, 242, 243, 243, 124, 128, 126, 11, 15, 14, 159, 161, 165, 238, 240, 246, + 79, 124, 184, 66, 116, 128, 53, 103, 46, 129, 119, 144, 198, 131, 221, 179, 108, 84, 134, 85, 25, 68, 69, 63, + 108, 117, 120, 184, 183, 189, 121, 122, 121, 74, 77, 71, 158, 158, 160, 169, 164, 180, 93, 79, 111, 151, 112, 73, + 246, 193, 68, 162, 190, 142, 99, 160, 206, 176, 117, 205, 168, 82, 165, 72, 66, 72, 115, 122, 112, 185, 188, 184, + 109, 115, 109, 72, 80, 75, 157, 164, 169, 199, 191, 174, 194, 167, 81, 187, 157, 106, 180, 143, 170, 193, 105, 192, + 176, 83, 194, 109, 92, 192, 71, 82, 149, 67, 68, 71, 130, 132, 124, 182, 185, 184, 105, 107, 111, 63, 65, 71, + 57, 121, 231, 61, 99, 160, 72, 65, 51, 106, 70, 117, 138, 88, 176, 142, 111, 62, 106, 92, 9, 26, 23, 22, + 100, 101, 109, 231, 230, 230, 116, 115, 115, 32, 31, 28, 185, 184, 176, 198, 197, 223, 35, 42, 137, 90, 84, 76, + 227, 201, 53, 192, 163, 147, 146, 111, 239, 169, 110, 255, 126, 76, 198, 25, 20, 31, 112, 116, 106, 233, 232, 233, + 99, 99, 101, 35, 36, 37, 187, 191, 187, 227, 207, 223, 165, 89, 154, 186, 76, 124, 217, 91, 129, 173, 59, 210, + 131, 56, 255, 116, 110, 254, 71, 87, 170, 23, 23, 26, 132, 131, 122, 231, 228, 230, 96, 95, 96, 23, 23, 24, + 24, 42, 132, 79, 100, 130, 163, 183, 126, 124, 123, 122, 81, 50, 109, 123, 66, 65, 172, 118, 91, 206, 198, 194, + 147, 147, 149, 49, 44, 48, 138, 133, 131, 206, 200, 193, 104, 92, 80, 47, 62, 71, 34, 139, 188, 53, 145, 193, + 90, 120, 154, 159, 148, 199, 178, 145, 243, 84, 49, 248, 89, 66, 240, 191, 186, 198, 138, 138, 126, 54, 52, 56, + 145, 143, 150, 188, 188, 193, 80, 86, 74, 88, 47, 91, 190, 33, 222, 143, 29, 188, 64, 30, 120, 87, 29, 172, + 124, 49, 207, 150, 104, 187, 182, 162, 194, 202, 206, 201, 126, 124, 119, 60, 48, 53, 154, 150, 155, 203, 205, 210, + 0, 0, 48, 86, 94, 104, 229, 253, 199, 130, 142, 124, 30, 16, 47, 138, 89, 104, 222, 173, 179, 238, 241, 253, + 151, 167, 170, 26, 42, 30, 132, 150, 162, 207, 226, 255, 65, 69, 97, 0, 22, 45, 16, 121, 141, 56, 152, 201, + 103, 143, 228, 128, 128, 185, 129, 108, 164, 91, 75, 227, 136, 130, 255, 255, 255, 255, 153, 166, 144, 5, 11, 0, + 168, 172, 151, 244, 255, 228, 47, 83, 61, 14, 6, 45, 132, 0, 167, 123, 42, 188, 79, 88, 168, 55, 34, 129, + 83, 32, 124, 185, 141, 174, 246, 212, 227, 248, 233, 255, 105, 126, 128, 0, 37, 12, 151, 170, 150, 241, 238, 223, + 0, 0, 0, 84, 85, 86, 255, 255, 255, 124, 125, 123, 0, 0, 0, 180, 170, 169, 247, 246, 255, 125, 154, 198, + 113, 160, 170, 149, 208, 165, 100, 165, 197, 54, 116, 221, 70, 114, 211, 49, 77, 140, 0, 5, 17, 97, 109, 111, + 245, 251, 255, 105, 105, 114, 17, 14, 22, 180, 175, 200, 248, 249, 255, 235, 255, 250, 153, 192, 157, 78, 103, 59, + 168, 186, 104, 207, 236, 147, 87, 172, 141, 13, 80, 87, 7, 0, 12, 125, 107, 130, 240, 242, 254, 76, 73, 87, + 19, 13, 26, 214, 210, 210, 255, 229, 255, 167, 117, 196, 75, 138, 146, 19, 182, 103, 95, 154, 85, 145, 131, 75, + 103, 103, 97, 120, 119, 118, 155, 153, 158, 126, 125, 127, 101, 102, 99, 147, 147, 143, 144, 152, 162, 55, 98, 141, + 65, 150, 148, 121, 240, 165, 113, 169, 138, 92, 100, 125, 78, 145, 178, 78, 148, 172, 97, 92, 97, 132, 117, 111, + 165, 162, 156, 120, 120, 116, 93, 95, 90, 143, 146, 131, 177, 166, 162, 210, 188, 208, 142, 158, 141, 67, 123, 49, + 170, 199, 51, 229, 232, 92, 151, 150, 170, 100, 102, 165, 90, 100, 87, 127, 134, 117, 167, 167, 160, 121, 124, 115, + 100, 106, 97, 143, 149, 148, 166, 153, 151, 167, 127, 97, 126, 151, 132, 91, 173, 159, 129, 130, 65, 157, 103, 14, + 252, 252, 252, 166, 166, 166, 6, 6, 6, 132, 132, 132, 247, 247, 247, 86, 80, 80, 3, 2, 4, 18, 72, 86, + 24, 140, 111, 35, 194, 97, 152, 162, 40, 217, 131, 11, 91, 160, 74, 92, 206, 160, 241, 252, 251, 157, 147, 152, + 3, 3, 3, 150, 150, 150, 239, 240, 240, 57, 64, 59, 35, 7, 27, 181, 86, 153, 128, 99, 111, 35, 106, 14, + 167, 206, 7, 255, 231, 54, 217, 84, 165, 221, 100, 239, 252, 247, 252, 128, 138, 128, 4, 4, 4, 169, 169, 169, + 230, 230, 233, 41, 41, 55, 48, 51, 2, 200, 200, 2, 208, 169, 112, 180, 98, 195, 198, 100, 71, 209, 107, 1, + 134, 135, 142, 125, 125, 133, 108, 107, 116, 121, 122, 132, 135, 137, 145, 131, 122, 126, 108, 116, 97, 40, 139, 62, + 51, 145, 64, 110, 143, 82, 182, 139, 44, 205, 132, 19, 109, 118, 57, 80, 120, 100, 139, 149, 146, 139, 137, 139, + 118, 115, 117, 127, 129, 133, 132, 139, 142, 118, 124, 119, 120, 118, 112, 140, 136, 119, 148, 116, 97, 147, 104, 64, + 130, 182, 39, 138, 218, 49, 209, 143, 119, 208, 119, 151, 140, 147, 143, 120, 130, 130, 116, 109, 116, 132, 128, 130, + 138, 135, 137, 118, 111, 124, 128, 142, 115, 163, 217, 83, 194, 210, 169, 207, 162, 224, 167, 80, 65, 143, 38, 0, + 1, 0, 0, 90, 90, 87, 255, 255, 253, 125, 125, 122, 5, 5, 2, 182, 172, 185, 236, 255, 235, 98, 224, 79, + 103, 155, 41, 186, 85, 71, 196, 119, 84, 172, 142, 85, 118, 68, 67, 53, 9, 32, 0, 0, 0, 104, 108, 107, + 255, 255, 255, 105, 104, 103, 13, 10, 10, 197, 188, 198, 237, 255, 238, 124, 222, 118, 170, 151, 97, 240, 91, 110, + 101, 155, 97, 31, 210, 80, 163, 202, 68, 144, 121, 32, 3, 0, 0, 118, 118, 125, 255, 255, 255, 84, 86, 85, + 21, 19, 20, 210, 205, 205, 234, 255, 255, 134, 221, 206, 153, 232, 221, 183, 218, 212, 123, 79, 73, 84, 3, 5, + 143, 141, 56, 181, 186, 92, 254, 255, 160, 205, 197, 82, 136, 114, 27, 121, 126, 162, 142, 173, 237, 200, 255, 201, + 150, 167, 111, 65, 13, 11, 168, 133, 122, 231, 221, 201, 84, 61, 64, 3, 0, 0, 17, 0, 1, 75, 72, 69, + 150, 181, 147, 152, 131, 74, 148, 79, 43, 171, 99, 175, 196, 161, 239, 216, 255, 216, 158, 155, 123, 89, 19, 31, + 138, 161, 143, 159, 243, 191, 81, 113, 46, 63, 33, 0, 103, 0, 34, 145, 114, 105, 166, 254, 169, 101, 148, 125, + 76, 87, 100, 140, 170, 134, 193, 229, 193, 221, 255, 255, 135, 170, 171, 59, 78, 71, 151, 136, 134, 198, 166, 171, + 255, 234, 133, 245, 229, 123, 221, 216, 98, 243, 217, 64, 230, 195, 58, 83, 92, 133, 64, 90, 203, 222, 221, 241, + 163, 159, 157, 16, 21, 21, 137, 140, 142, 228, 230, 233, 82, 88, 88, 20, 20, 19, 69, 9, 26, 57, 38, 46, + 43, 81, 68, 174, 130, 82, 250, 137, 105, 139, 53, 151, 134, 85, 201, 236, 225, 238, 139, 145, 138, 12, 19, 19, + 158, 155, 158, 227, 220, 226, 57, 66, 67, 48, 17, 31, 179, 21, 109, 151, 102, 93, 88, 187, 71, 130, 193, 167, + 144, 171, 190, 86, 128, 64, 131, 161, 99, 237, 233, 232, 126, 120, 125, 17, 16, 16, 162, 164, 164, 239, 243, 243, + 214, 127, 169, 205, 113, 182, 185, 92, 189, 172, 109, 100, 154, 127, 36, 128, 117, 124, 100, 95, 152, 72, 67, 79, + 115, 113, 113, 181, 184, 185, 119, 121, 123, 74, 75, 77, 155, 163, 164, 178, 164, 158, 121, 41, 31, 62, 10, 55, + 28, 21, 144, 122, 53, 160, 190, 94, 160, 136, 147, 165, 91, 137, 134, 71, 72, 73, 120, 118, 120, 180, 185, 184, + 114, 112, 114, 82, 76, 80, 160, 160, 159, 170, 177, 189, 116, 115, 188, 107, 70, 104, 120, 59, 39, 159, 165, 178, + 169, 210, 222, 127, 122, 54, 90, 72, 26, 71, 69, 69, 129, 128, 133, 180, 179, 179, 110, 112, 112, 73, 76, 76, + 184, 64, 186, 179, 48, 213, 166, 25, 241, 130, 48, 120, 108, 86, 22, 156, 132, 121, 123, 102, 125, 0, 0, 0, + 87, 88, 87, 255, 255, 255, 111, 111, 111, 0, 0, 0, 196, 204, 205, 255, 242, 234, 147, 58, 34, 65, 0, 61, + 25, 0, 189, 92, 10, 203, 152, 68, 188, 136, 202, 174, 70, 169, 98, 0, 0, 0, 111, 104, 109, 255, 255, 255, + 88, 88, 88, 0, 0, 0, 219, 214, 209, 238, 255, 255, 78, 166, 228, 81, 53, 110, 141, 0, 26, 174, 148, 183, + 179, 227, 235, 152, 121, 51, 69, 26, 0, 0, 0, 0, 131, 135, 140, 255, 255, 255, 81, 81, 81, 0, 0, 0, +}; + +// Pillow: 23x5 (height only). +const std::vector PILLOW_23X5 = { + 104, 96, 126, 126, 165, 164, 92, 91, 117, 153, 175, 147, 143, 136, 159, 94, 128, 162, 74, 113, 162, 143, 123, 167, + 116, 93, 124, 159, 142, 146, 82, 83, 89, 148, 167, 143, 109, 123, 94, 120, 138, 97, 180, 159, 129, 155, 97, 104, + 118, 119, 87, 144, 138, 171, 90, 84, 96, 147, 139, 167, 183, 162, 215, 89, 57, 87, 121, 180, 196, 113, 84, 179, + 82, 177, 196, 77, 92, 92, 160, 227, 119, 153, 167, 134, 121, 124, 114, 165, 147, 129, 114, 104, 118, 126, 95, 118, + 107, 124, 134, 61, 85, 47, 157, 155, 129, 130, 144, 170, 91, 130, 73, 154, 155, 144, 95, 84, 121, 92, 169, 66, + 180, 134, 215, 102, 125, 117, 143, 111, 162, 131, 141, 156, 91, 119, 131, 139, 139, 135, 88, 94, 93, 140, 175, 133, + 131, 112, 134, 171, 131, 123, 159, 122, 169, 152, 150, 71, 173, 117, 115, 107, 101, 72, 109, 114, 95, 177, 164, 149, + 86, 138, 123, 167, 128, 167, 140, 146, 211, 132, 135, 98, 168, 121, 151, 116, 47, 90, 138, 129, 80, 142, 161, 176, + 138, 97, 107, 143, 139, 154, 124, 143, 141, 136, 110, 157, 198, 173, 105, 60, 76, 108, 152, 173, 136, 100, 80, 120, + 168, 133, 110, 115, 146, 150, 100, 140, 112, 144, 145, 149, 77, 110, 120, 69, 108, 127, 163, 163, 153, 113, 125, 151, + 142, 111, 203, 185, 184, 178, 85, 93, 71, 189, 204, 130, 104, 104, 119, 141, 80, 124, 136, 126, 162, 110, 63, 135, + 157, 146, 183, 172, 163, 145, 93, 126, 119, 160, 137, 102, 164, 131, 118, 190, 160, 162, 140, 124, 57, 122, 130, 176, + 102, 153, 111, 122, 122, 100, 164, 142, 107, 92, 100, 99, 89, 47, 49, 99, 107, 142, 153, 94, 103, 139, 140, 166, + 147, 159, 139, 134, 104, 92, 132, 183, 107, 148, 122, 113, 111, 76, 117, 139, 144, 108, 123, 147, 155, 135, 140, 92, + 152, 175, 139, 140, 145, 157, 140, 100, 77, +}; + +int main() { + std::vector out; + std::string error; + check(resize_rgb_bicubic(SOURCE, 23, 17, 9, 11, out, error) && out == PILLOW_9X11, "shrink matches Pillow"); + check(resize_rgb_bicubic(SOURCE, 23, 17, 40, 31, out, error) && out == PILLOW_40X31, "enlarge matches Pillow"); + check(resize_rgb_bicubic(SOURCE, 23, 17, 23, 5, out, error) && out == PILLOW_23X5, "one-axis resize matches Pillow"); + check(resize_rgb_bicubic(SOURCE, 23, 17, 23, 17, out, error) && out == SOURCE, "same size is a copy"); + check(!resize_rgb_bicubic(SOURCE, 23, 17, 0, 5, out, error) && !error.empty(), "zero output size is rejected"); + check(!resize_rgb_bicubic(SOURCE, 24, 17, 9, 11, out, error), "input size mismatch is rejected"); + if (failures) { std::fprintf(stderr, "%d failure(s)\n", failures); return 1; } + std::printf("OK\n"); + return 0; +} From b52c981bb76ea2a5f9f61a6a361fcccfad78d03d Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 17:13:03 +0200 Subject: [PATCH 107/123] feat(qwen35): image input for Qwen3.5 / Qwen3.8 through --mmproj Qwen35Backend implements the image contract, so the engine now takes images on a single GPU with a dense Qwen model as well as with DS4V. - qwen35_vision: the qwen3vl_merger tower in plain ggml, read straight from the projector file published next to the model. Preprocessing follows the model's own rule (bicubic, sides to a multiple of 32, 64 to 1,024 tokens). - qwen35_image_prompt: one pad per image token, and the two-dimensional rotary positions image tokens take. Pure functions, covered by test_qwen35_image. - Prefill writes the encoded rows over the pad embeddings in its normal chunk loop; decode carries the position offset an image leaves behind. Image requests decode one token at a time, text keeps speculative decoding. - The target graph now uses interleaved M-RoPE, as the model is defined. For text all three axes hold the same position, so output is unchanged: byte identical to main on five prompts up to 19.6K tokens, same speed. - --mmproj is accepted for qwen35 on any backend; layer or tensor splits, remote shards and --max-concurrency still refuse it. On an R9700 with Qwen3.8-27B UD-IQ4_XS: AI2D 86/100, ChartQA 55/60 augmented and 42/60 human, image prefill 0.74 s on average, decode 31 to 35 tok/s. docs/ds4v-image-serving.md becomes docs/image-input.md and covers both models. Co-Authored-By: Claude Fable 5.1 --- docs/ds4v-image-serving.md | 127 ------- docs/image-input.md | 194 ++++++++++ server/CMakeLists.txt | 11 +- server/src/common/backend_factory.cpp | 1 + server/src/common/feature_gate.cpp | 15 +- server/src/internal.h | 3 + server/src/qwen35/gguf_target_loader.cpp | 13 + server/src/qwen35/qwen35_backend.cpp | 47 ++- server/src/qwen35/qwen35_backend.h | 33 +- server/src/qwen35/qwen35_backend_images.cpp | 103 ++++++ server/src/qwen35/qwen35_image_prompt.cpp | 84 +++++ server/src/qwen35/qwen35_image_prompt.h | 51 +++ server/src/qwen35/qwen35_image_request.h | 39 ++ server/src/qwen35/qwen35_target_graph.cpp | 7 +- server/src/qwen35/qwen35_vision.cpp | 379 ++++++++++++++++++++ server/src/qwen35/qwen35_vision.h | 92 +++++ server/src/server/server_main.cpp | 2 +- server/test/test_qwen35_image.cpp | 122 +++++++ 18 files changed, 1181 insertions(+), 142 deletions(-) delete mode 100644 docs/ds4v-image-serving.md create mode 100644 docs/image-input.md create mode 100644 server/src/qwen35/qwen35_backend_images.cpp create mode 100644 server/src/qwen35/qwen35_image_prompt.cpp create mode 100644 server/src/qwen35/qwen35_image_prompt.h create mode 100644 server/src/qwen35/qwen35_image_request.h create mode 100644 server/src/qwen35/qwen35_vision.cpp create mode 100644 server/src/qwen35/qwen35_vision.h create mode 100644 server/test/test_qwen35_image.cpp diff --git a/docs/ds4v-image-serving.md b/docs/ds4v-image-serving.md deleted file mode 100644 index 03714921a..000000000 --- a/docs/ds4v-image-serving.md +++ /dev/null @@ -1,127 +0,0 @@ -# DS4V image serving - -**Status: experimental.** The request path works end to end, but the vision -tower has not met its numerical gate and no image-chat acceptance run exists. -See [Verification status](#verification-status) before relying on image output. - -The DS4V integration accepts JPEG and PNG images through OpenAI chat -completions when the matching projector is supplied with `--mmproj`. Without -`--mmproj` nothing in the text serving path changes. - -The server must be built with hipBLASLt available (the `hipblaslt-dev` package -on ROCm). CMake reports `hipBLASLt found: building the DS4V vision ops`; a build -without it refuses `--mmproj` at startup. - -## Supported configuration - -Image input needs Linux HIP, a DeepSeek4 decoder whose GGUF carries the image -router biases, `--ds4-prefill sparse`, and `--mmproj` pointing at the -[exported projector](ds4v-mmproj.md). Two layouts work: - -- **One GPU holding the whole model** (for example a Strix Halo): nothing else - to set. The projector is loaded after the weights and must fit beside them. -- **Two GPUs splitting the experts in process** (for example R9700 + Strix - Halo): `DFLASH_DS4_MOE_TP=1`, `DFLASH_DS4_MOE_TP_INPROC=1`, and - `DFLASH_DS4_MOE_TP_GPU` selecting the second device, with `--target-device` - on the first. Device ordinals must match the host's actual topology. - -Layer splitting, remote expert IPC, all-on-secondary placement, experts kept -on the CPU, dense prefill, concurrent sequence scheduling, and upstream -forwarding do not support images. `/props` reports the effective capability in -`capabilities.image_input_supported` after backend initialization. Without -`--mmproj`, text serving follows its existing path and image requests are -passed through as they were before. - -Published llama.cpp conversions of the decoder load directly (image router -bias named `blk.N.exp_probs_b_vl.bias`, no `deepseek4.vocab_size` key). Split -GGUF files and llama.cpp's `clip` projector files are not read yet. - -## Request contract - -Use `POST /v1/chat/completions` with user-message content parts in display order: - -```json -{ - "messages": [{ - "role": "user", - "content": [ - {"type": "text", "text": "Describe this image."}, - {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}} - ] - }], - "max_tokens": 128 -} -``` - -Only base64 JPEG/PNG data URLs are supported. Remote URLs, images outside user -content arrays, and image parts through other API formats are rejected. -Requests permit at most four images, 16 MiB encoded bytes per image, and -32 MiB combined encoded bytes. Decoder pixel and aspect limits also apply. -The reserved DS4 image marker cannot be supplied as ordinary text. - -One image request may be outstanding per backend. Its admission lease remains -with the immutable payload through queueing and generation; another image -request is rejected until that payload is released. This bounds simultaneous -preprocessing and prepared-image memory. Text requests retain the normal queue. - -The server expands image markers after final rendering and tokenization. -Expanded image tokens count toward context and usage. Image blocks remain -whole during prefill, image rows use their learned routing bias, and raw -attention is bidirectional within each image's visible span. The projector's -tile permutation is applied once when assembling rows with named sentinel -embeddings. All chunks are capped at 1,024 tokens while a projector is loaded. - -Image requests use autoregressive decoding and bypass token-only prefix, -disk, and agent-turn caches, prompt compression, and speculative capture. -Their image payload survives request copies and retry paths. Failed or cancelled -multi-image encoding publishes no partial embedding matrices. - -## Memory and verification - -The projector is validated and loaded before expert placement. Admission counts -actual selected owner tensor sizes, allocation alignment, MIX tables, copy -staging, future KV, and explicit execution reserves. Host and integrated-device -charges share one physical-memory budget. Before image decoding, the server -checks host availability; before encoding, it synchronizes and releases -disposable decoder, owner, and draft graphs and checks live device/host -availability again. KV, saved snapshots, and draft weights remain reflected in -that live measurement. Reservations are conservative policy, not a guarantee -against unrelated concurrent allocations. - -## Verification status - -Covered by unit tests in the main build: image transport and request policy, -prompt expansion and ownership, embedding assembly and cancellation, image -spans and the expert budget, plus the decoder loader and image-batch admission -tests in `test_deepseek4_unit`. - -Not yet established: - -- The vision tower misses the fixed 0.9995 feature-cosine gate against the - reference implementation: 0.99906 on the Radeon RX 7900 XT it was developed - on, 0.99823 on CPU. Embeddings pass; features do not. -- No image-chat quality run, and no measurement of paired-GPU memory peaks or - throughput with a projector loaded. -- No other HIP device has been tried. - -## Code layout - -Shared by every model: - -| Piece | Where | -| --- | --- | -| Reading images out of a request, limits, redaction | `server/src/server/image_input.*` | -| JPEG and PNG decoding to RGB | `server/src/common/vision/image_decode.*`, codecs in `server/cmake/ImageCodecs.cmake` | -| Image positions in a prompt, batches that keep an image whole | `server/src/common/vision/image_spans.h` | -| The backend contract | `supports_images`, `image_placeholder`, `prepare_images` in `server/src/common/model_backend.h`; `GenerateRequest::images` in `server/src/common/generation_types.h` | - -DS4V only, all under `server/src/deepseek4/`: resizing and patching -(`deepseek4_vision_preprocess`), the vision tower (`deepseek4_vision`), marker -expansion and embedding assembly (`deepseek4_image_prompt`, -`deepseek4_image_assembly`), attention visibility and expert routing for image -rows (`deepseek4_image_policy`), and memory admission -(`deepseek4_image_admission`). - -Another model needs its own preprocessing, tower and prompt expansion, and its -backend implements the three contract methods. Nothing in the HTTP server or in -`common/vision` names a model. diff --git a/docs/image-input.md b/docs/image-input.md new file mode 100644 index 000000000..d532c41df --- /dev/null +++ b/docs/image-input.md @@ -0,0 +1,194 @@ +# Image input + +The server accepts JPEG and PNG images through OpenAI chat completions when a +model is started with its vision projector, `--mmproj `. Without +`--mmproj` nothing in the text serving path changes. + +| Model | Projector file | Runs on | +| --- | --- | --- | +| Qwen3.5 / Qwen3.8 dense | the `mmproj-*.gguf` published next to the model (llama.cpp `clip` format, type `qwen3vl_merger`) | one GPU, any backend | +| DeepSeek V4 Flash Vision (DS4V) | [exported with our tool](ds4v-mmproj.md) | HIP: one GPU, or two GPUs splitting the experts | + +**Status: experimental.** Both models answer image questions correctly end to +end; see each model's verification notes for what has and has not been +measured. + +## Request contract + +Use `POST /v1/chat/completions` with user-message content parts in display order: + +```json +{ + "messages": [{ + "role": "user", + "content": [ + {"type": "text", "text": "Describe this image."}, + {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}} + ] + }], + "max_tokens": 128 +} +``` + +Only base64 JPEG/PNG data URLs are supported. Remote URLs, images outside user +content arrays, and image parts through other API formats are rejected. An +image is at most 16 MiB encoded. Decoder pixel and aspect limits also apply. A model's image marker cannot be supplied +as ordinary text. + +The server expands image markers after final rendering and tokenization, and +the expanded image tokens count toward context and usage. Image requests use +plain autoregressive decoding and bypass the token-keyed prefix, disk and +agent-turn caches and prompt compression: tokens alone do not identify an +image. Text requests on the same server keep speculative decoding and caching. + +Layer or tensor splitting across GPUs, remote target shards, concurrent +sequence scheduling (`--max-concurrency`) and upstream forwarding do not +support images. `/props` reports the effective capability in +`capabilities.image_input_supported` after backend initialization. + +## Qwen3.5 / Qwen3.8 + +``` +dflash_server Qwen3.8-27B-UD-IQ4_XS.gguf --target-device hip:0 \ + --draft qwen38-dflash2-q8_0.gguf --draft-device hip:0 \ + --mmproj mmproj-Qwen3.8-27B-BF16.gguf +``` + +The projector is read directly from the published file. Projectors with +deepstack branches (Qwen3-VL) are refused. An image is resized the way the +model was trained (bicubic, both sides to a multiple of 32 pixels) and costs +one token per 32x32 pixels, between 64 and 1,024 tokens; larger images are +scaled down to the cap. Up to eight images per request. + +Image tokens take two-dimensional rotary positions, so positions run behind +token counts after an image. Prefill handles that in its normal chunk loop; +decoding carries the offset for the rest of the request. + +### Verification status + +Covered by `test_qwen35_image`: target sizes against the model's reference +resize rule, the tower's patch order and position table sampling, marker +expansion, rotary positions, and image rows that straddle prefill chunks. + +Measured on an R9700 alone with Qwen3.8-27B UD-IQ4_XS, the DFlash2 drafter and +the published BF16 projector, thinking off: + +- 220 seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with + lmms-eval prompts: AI2D 86/100, ChartQA relaxed accuracy 55/60 (augmented) + and 42/60 (human), no errors. Image prompts average 448 tokens and prefill in + 0.74 s (largest 1,068 tokens, 1.9 s); decode runs at 31 to 35 tok/s. +- An image inside a 6,271-token prompt, two images in one request, and a + follow-up turn after an image all answer correctly. +- Text requests are byte-identical to a build without image support, at the + same speed, with or without a projector loaded (five prompts up to 19.6K + tokens). The projector adds 0.9 GiB of VRAM; the peak during image requests + was 21.6 GiB against 20.8 GiB for text. + +Not yet established: a comparison against the reference implementation on the +same questions, other GPUs, and CUDA. The tower uses only standard ggml +operators, so nothing in it is HIP specific. + +## DS4V + +The server must be built with hipBLASLt available (the `hipblaslt-dev` package +on ROCm). CMake reports `hipBLASLt found: building the DS4V vision ops`; a build +without it refuses `--mmproj` for this model at startup. + +Image input needs Linux HIP, a DeepSeek4 decoder whose GGUF carries the image +router biases, `--ds4-prefill sparse`, and `--mmproj` pointing at the +[exported projector](ds4v-mmproj.md). Two layouts work: + +- **One GPU holding the whole model** (for example a Strix Halo): nothing else + to set. The projector is loaded after the weights and must fit beside them. +- **Two GPUs splitting the experts in process** (for example R9700 + Strix + Halo): `DFLASH_DS4_MOE_TP=1`, `DFLASH_DS4_MOE_TP_INPROC=1`, and + `DFLASH_DS4_MOE_TP_GPU` selecting the second device, with `--target-device` + on the first. Device ordinals must match the host's actual topology. + +Remote expert IPC, all-on-secondary placement, experts kept on the CPU and +dense prefill do not support images. + +Published llama.cpp conversions of the decoder load directly (image router +bias named `blk.N.exp_probs_b_vl.bias`, no `deepseek4.vocab_size` key). Split +GGUF files and llama.cpp's `clip` projector files are not read yet. + +Requests permit at most four images and 32 MiB of encoded images combined. + +One image request may be outstanding per backend. Its admission lease remains +with the immutable payload through queueing and generation; another image +request is rejected until that payload is released. This bounds simultaneous +preprocessing and prepared-image memory. Text requests retain the normal queue. + +The server expands image markers after final rendering and tokenization. +Expanded image tokens count toward context and usage. Image blocks remain +whole during prefill, image rows use their learned routing bias, and raw +attention is bidirectional within each image's visible span. The projector's +tile permutation is applied once when assembling rows with named sentinel +embeddings. All chunks are capped at 1,024 tokens while a projector is loaded. + +The image payload survives request copies and retry paths. Failed or cancelled +multi-image encoding publishes no partial embedding matrices. + +### Memory + +The projector is validated and loaded before expert placement. Admission counts +actual selected owner tensor sizes, allocation alignment, MIX tables, copy +staging, future KV, and explicit execution reserves. Host and integrated-device +charges share one physical-memory budget. Before image decoding, the server +checks host availability; before encoding, it synchronizes and releases +disposable decoder, owner, and draft graphs and checks live device/host +availability again. KV, saved snapshots, and draft weights remain reflected in +that live measurement. Reservations are conservative policy, not a guarantee +against unrelated concurrent allocations. + +### Verification status + +Covered by unit tests in the main build: image transport and request policy, +prompt expansion and ownership, embedding assembly and cancellation, image +spans and the expert budget, plus the decoder loader and image-batch admission +tests in `test_deepseek4_unit`. + +Measured with the public `DeepSeek-V4-Flash-Vision-Exp` Q2_K_S decoder and +the exported projector, on a Strix Halo alone and on R9700 + Strix Halo, 220 +seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with lmms-eval +prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented) and 43/60 +(human). Both layouts score the same and give word-identical answers on 213 of +220 questions. An image request prefills in about 4 s and decodes at about +23 tok/s. + +Not yet established: + +- The vision tower misses the fixed 0.9995 feature-cosine gate against the + reference implementation: 0.99906 on the Radeon RX 7900 XT it was developed + on, 0.99823 on CPU. Embeddings pass; features do not. +- No comparison against the reference implementation on the same questions. + +## Code layout + +Shared by every model: + +| Piece | Where | +| --- | --- | +| Reading images out of a request, limits, redaction | `server/src/server/image_input.*` | +| JPEG and PNG decoding to RGB | `server/src/common/vision/image_decode.*`, codecs in `server/cmake/ImageCodecs.cmake` | +| Bicubic resizing that matches Pillow byte for byte | `server/src/common/vision/image_resize.*` | +| Reading a published `clip`-format projector file | `server/src/common/vision/mmproj_file.*` | +| Image positions in a prompt, batches that keep an image whole | `server/src/common/vision/image_spans.h` | +| The backend contract | `supports_images`, `image_placeholder`, `prepare_images` in `server/src/common/model_backend.h`; `GenerateRequest::images` in `server/src/common/generation_types.h` | + +DS4V only, all under `server/src/deepseek4/`: resizing and patching +(`deepseek4_vision_preprocess`), the vision tower (`deepseek4_vision`), marker +expansion and embedding assembly (`deepseek4_image_prompt`, +`deepseek4_image_assembly`), attention visibility and expert routing for image +rows (`deepseek4_image_policy`), and memory admission +(`deepseek4_image_admission`). + +Qwen3.5 / Qwen3.8 only, all under `server/src/qwen35/`: the vision tower and +its preprocessing (`qwen35_vision`), marker expansion and rotary positions +(`qwen35_image_prompt`), what a request carries (`qwen35_image_request.h`), and +the backend's three contract methods (`qwen35_backend_images.cpp`). Prefill and +decode changes are a few lines in `qwen35_backend.cpp`. + +Another model needs its own preprocessing, tower and prompt expansion, and its +backend implements the three contract methods. Nothing in the HTTP server or in +`common/vision` names a model. diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index c89b0d613..115efeb7c 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -584,6 +584,9 @@ add_library(dflash_common STATIC src/qwen35/qwen35_target_shard_ipc_daemon.cpp src/qwen35/layer_split_daemon.cpp src/qwen35/qwen35_backend.cpp + src/qwen35/qwen35_backend_images.cpp + src/qwen35/qwen35_image_prompt.cpp + src/qwen35/qwen35_vision.cpp src/qwen35/qwen35_tensor_parallel.cpp src/qwen35/concurrency/qwen35_seq_engine.cpp src/qwen35/qwen35_layer_split_adapter.cpp @@ -1913,7 +1916,13 @@ if(DFLASH27B_TESTS) target_include_directories(test_image_resize PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) add_executable(test_gpu_page_pool test/test_gpu_page_pool.cpp src/common/gpu_page_pool.cpp) target_include_directories(test_gpu_page_pool PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) - list(APPEND _raw_unit_test_targets test_image_input test_image_resize test_gpu_page_pool) + add_executable(test_qwen35_image test/test_qwen35_image.cpp + src/qwen35/qwen35_image_prompt.cpp src/qwen35/qwen35_vision.cpp + src/common/vision/mmproj_file.cpp src/common/vision/image_resize.cpp) + target_include_directories(test_qwen35_image PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + target_link_libraries(test_qwen35_image PRIVATE ggml ggml-base) + list(APPEND _raw_unit_test_targets test_image_input test_image_resize test_gpu_page_pool + test_qwen35_image) # DS4V image units: each test builds only the unit it covers. foreach(_ds4v_unit assembly integration policy prompt) diff --git a/server/src/common/backend_factory.cpp b/server/src/common/backend_factory.cpp index a34eeb279..4745a5283 100644 --- a/server/src/common/backend_factory.cpp +++ b/server/src/common/backend_factory.cpp @@ -190,6 +190,7 @@ std::unique_ptr construct_backend( Qwen35Config cfg; cfg.target_path = model.path; cfg.draft_path = speculation.draft_path; + cfg.mmproj_path = model.mmproj_path.value_or(""); cfg.device = placement.target; cfg.draft_gpu = placement.draft.gpu; cfg.remote_draft = placement.remote_draft; diff --git a/server/src/common/feature_gate.cpp b/server/src/common/feature_gate.cpp index 6868a3112..0611d266f 100644 --- a/server/src/common/feature_gate.cpp +++ b/server/src/common/feature_gate.cpp @@ -48,11 +48,16 @@ std::string check_feature_compatibility( } // ── vision projector × architecture / placement - if (args.mmproj_path.has_value() && - (arch != "deepseek4" || args.device.is_layer_split() || - args.remote_target_shard.enabled() || args.max_concurrency != 1 || - target_backend != PlacementBackend::Hip)) { - return "--mmproj requires a local single-request DeepSeek4 HIP backend"; + if (args.mmproj_path.has_value()) { + if ((arch != "deepseek4" && arch != "qwen35") || args.device.is_layer_split() || + args.device.is_tensor_parallel() || args.remote_target_shard.enabled() || + args.max_concurrency != 1) { + return "--mmproj requires a local single-request DeepSeek4 or Qwen3.5 backend " + "that is not split across GPUs by layer or tensor"; + } + if (arch == "deepseek4" && target_backend != PlacementBackend::Hip) { + return "--mmproj with DeepSeek4 requires a HIP backend"; + } } // ── PFlash enablement × drafter model diff --git a/server/src/internal.h b/server/src/internal.h index f5404906e..234c27ef5 100644 --- a/server/src/internal.h +++ b/server/src/internal.h @@ -243,6 +243,9 @@ struct TargetWeights { // comparands with `>= 0` so the sentinel never matches a real token. int32_t eos_id = -1; int32_t eos_chat_id = -1; + // Token the chat template repeats once per image token; -1 when the + // vocabulary has none (a model without image input). + int32_t image_pad_id = -1; // DFlash noise mask token ID (from target tokenizer, used by draft model). // Default: Qwen tokenizer's mask token. Overridden by GGUF metadata if available. diff --git a/server/src/qwen35/gguf_target_loader.cpp b/server/src/qwen35/gguf_target_loader.cpp index 3a869b2f6..226bf36b9 100644 --- a/server/src/qwen35/gguf_target_loader.cpp +++ b/server/src/qwen35/gguf_target_loader.cpp @@ -115,6 +115,18 @@ int32_t get_i32_or(const gguf_context * g, const char * key, int32_t fallback) { return gguf_get_val_i32(g, id); } +// Id of the vocabulary entry spelled exactly `text`, or -1. Searched from the +// end, where the added special tokens live. +static int32_t find_token_id(const gguf_context * g, const char * text) { + const int64_t key = gguf_find_key(g, "tokenizer.ggml.tokens"); + if (key < 0 || gguf_get_kv_type(g, key) != GGUF_TYPE_ARRAY || + gguf_get_arr_type(g, key) != GGUF_TYPE_STRING) return -1; + for (size_t i = gguf_get_arr_n(g, key); i-- > 0;) { + if (std::strcmp(gguf_get_arr_str(g, key, i), text) == 0) return (int32_t) i; + } + return -1; +} + uint32_t get_u32_or(const gguf_context * g, const char * key, uint32_t fallback) { int64_t id = gguf_find_key(g, key); if (id < 0) return fallback; @@ -544,6 +556,7 @@ bool load_target_gguf_partial(const std::string & path, out.eos_chat_id = (raw_eos_chat == kEosKeyMissing) ? -1 : (int32_t)raw_eos_chat; std::printf("[loader] eos_id=%d eos_chat_id=%d\n", out.eos_id, out.eos_chat_id); } + out.image_pad_id = find_token_id(gctx, "<|image_pad|>"); // Compute capture layer IDs: evenly spaced through the target layers. // step = (n_layer - 2) / (N - 1), ids[k] = 1 + k * step. diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index d86a5bfc0..bf2749cb1 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1,4 +1,5 @@ #include "qwen35_backend.h" +#include "qwen35_image_request.h" #include "concurrency/qwen35_seq_engine.h" #include "common/chain_rollback_policy.h" #include "common/adaptive_spec_width.h" @@ -328,6 +329,7 @@ bool Qwen35Backend::init() { return false; } std::printf("[target] %s\n", dflash27b_last_error()); + if (!load_vision()) return false; if (cfg_.paged_attention && (w_.n_embd_head_k != 256 || w_.n_embd_head_v != 256)) { std::fprintf(stderr, @@ -894,6 +896,7 @@ bool Qwen35Backend::park(ParkTarget target) { step_graph_destroy(proj_sg_); dflash2_selector_graph_invalidate(); free_target_weights(w_); + vision_.reset(); target_parked_ = true; std::printf("[park] target released\n"); std::fflush(stdout); } @@ -910,6 +913,7 @@ bool Qwen35Backend::unpark(ParkTarget target) { std::fprintf(stderr, "[unpark] target: %s\n", dflash27b_last_error()); return false; } + if (!load_vision()) return false; kvflash_drafter_failed_ = false; // fresh VRAM: allow a retry target_parked_ = false; std::printf("[unpark] target restored\n"); std::fflush(stdout); @@ -1437,7 +1441,19 @@ GenerateResult Qwen35Backend::generate_impl(const GenerateRequest & req, // Prefill auto t_prefill_start = std::chrono::steady_clock::now(); - const int committed = do_prefill(req.prompt, out_io, req.snap_pos, req.snap_slot); + Qwen35ImageRows image_rows; + if (req.images) { + const auto * prompt = dynamic_cast(req.images.get()); + std::string image_error = "image binding does not match the prompt"; + if (!prompt || !prompt->matches(req.prompt) || + !encode_images(*prompt, image_rows, image_error)) { + result.fail(GenerateErrorCode::BackendSpecific, image_error); + return result; + } + } + const bool has_images = image_rows.prompt != nullptr; + const int committed = do_prefill(req.prompt, out_io, req.snap_pos, req.snap_slot, + /*kv_offset=*/0, has_images ? &image_rows : nullptr); if (committed < 0) { result.fail(GenerateErrorCode::PrefillFailed); return result; @@ -1487,7 +1503,9 @@ GenerateResult Qwen35Backend::generate_impl(const GenerateRequest & req, req.n_gen, ar_n_gen, committed, cfg_.device.max_ctx); } } - if (cfg_.paged_attention || req.force_ar_decode) { + // Speculative decoding takes rotary positions from KV positions, which + // an image prompt pulls apart, so image requests decode one by one. + if (cfg_.paged_attention || req.force_ar_decode || has_images) { decode_ok = do_ar_decode(committed, ar_n_gen, result.tokens, out_io, req.budget_hook, &result.budget_forced_close, @@ -1539,6 +1557,13 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, out_io.emit(-1); return result; } + if (req.images) { + // A snapshot is keyed by tokens, and pad tokens do not identify an image. + result.fail(GenerateErrorCode::BackendSpecific, + "image requests cannot resume from a snapshot"); + out_io.emit(-1); + return result; + } if (slot < 0 || slot >= PREFIX_SLOTS || !prefix_snapshots_[slot].ctx) { result.fail(GenerateErrorCode::InvalidSnapshotSlot); out_io.emit(-1); @@ -1709,7 +1734,13 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, int Qwen35Backend::do_prefill(const std::vector & tokens, const DaemonIO & io, int snap_pos, int snap_slot, - int kv_offset) { + int kv_offset, + const Qwen35ImageRows * images) { + if (images && kv_offset != 0) { + std::fprintf(stderr, "prefill: an image prompt must start at position 0\n"); + return -1; + } + rope_delta_ = images ? images->prompt->positions.next - (int)tokens.size() : 0; // A finite --fa-window caps the full-attention layers to a sliding // window, so anything earlier than the window is invisible to them. That // is silent: the model still answers, it just cannot see the head of a @@ -1901,12 +1932,17 @@ int Qwen35Backend::do_prefill(const std::vector & tokens, if (!w_.embedder.embed(tokens.data() + start, n_tokens, embed_buf.data())) { return -1; } + if (images) images->overwrite(embed_buf.data(), start, n_tokens, hidden); ggml_backend_tensor_set(sg_.inp_embed, embed_buf.data(), 0, sizeof(float) * (size_t)hidden * n_tokens); // Positions (M-RoPE) std::vector pos_buf((size_t)4 * n_tokens, 0); - fill_qwen35_mrope_positions(pos_buf.data(), kv_pos, n_tokens); + if (images) { + images->prompt->positions.fill(pos_buf.data(), start, n_tokens); + } else { + fill_qwen35_mrope_positions(pos_buf.data(), kv_pos, n_tokens); + } ggml_backend_tensor_set(sg_.positions, pos_buf.data(), 0, sizeof(int32_t) * pos_buf.size()); @@ -2310,7 +2346,8 @@ bool Qwen35Backend::do_ar_decode(int committed, int n_gen, if (!w_.embedder.embed(&tok, 1, embed_buf)) return false; ggml_backend_tensor_set(sg_.inp_embed, embed_buf, 0, sizeof(float) * hidden); - int32_t pos4[4] = {committed, committed, committed, 0}; + const int32_t rope_pos = committed + rope_delta_; + int32_t pos4[4] = {rope_pos, rope_pos, rope_pos, 0}; ggml_backend_tensor_set(sg_.positions, pos4, 0, sizeof(int32_t) * 4); // kvflash: graph carries a slot-validity mask alongside the diff --git a/server/src/qwen35/qwen35_backend.h b/server/src/qwen35/qwen35_backend.h index e55671b9e..3d29e5d98 100644 --- a/server/src/qwen35/qwen35_backend.h +++ b/server/src/qwen35/qwen35_backend.h @@ -26,6 +26,7 @@ #include "common/concurrency/paged_kv_pool.h" #include "concurrency/qwen35_seq_engine.h" #include "internal.h" // TargetWeights, TargetCache, DraftWeights, PrefixSnapshot +#include "qwen35_vision.h" #include "qwen3/qwen3_drafter.h" // DrafterContext, load_drafter, free_drafter, drafter_score_and_compress #include "kvflash_pager.h" // bounded KV residency pool #include "kvflash_scorer.h" // chunk-relevance policy interface @@ -43,12 +44,15 @@ namespace dflash::common { class Qwen35TensorParallelContext; +class Qwen35ImagePrompt; +struct Qwen35ImageRows; // ── Configuration passed at construction ──────────────────────────────── struct Qwen35Config { std::string target_path; std::optional draft_path; + std::string mmproj_path; // vision projector; empty = text only DevicePlacement device; // target GPU placement int draft_gpu = 0; RemoteDraftConfig remote_draft; @@ -153,6 +157,18 @@ class Qwen35Backend : public ModelBackend { bool supports_dflash_spec_decode() const override { return !cfg_.paged_attention; } DFlashTarget * dflash_target() override; + + // Image input (--mmproj). Requests with images prefill through the normal + // chunk loop with the image rows written over the pad embeddings, and + // decode one token at a time. + bool supports_images() const override { return image_input_; } + std::string image_placeholder() const override; + bool prepare_images(std::vector & tokens, + std::vector images, + uint64_t context_capacity, + uint64_t output_reserve, + ImagePromptHandle & payload, + std::string & error) const override; bool supports_remote_draft() const override { return true; } // ── Concurrent slot serving (paged AR decode over N sequences) ──── @@ -291,6 +307,19 @@ class Qwen35Backend : public ModelBackend { bool target_parked_ = false; bool draft_parked_ = false; + // Vision projector, loaded next to the target weights when --mmproj is set. + bool load_vision(); + bool encode_images(const Qwen35ImagePrompt & prompt, Qwen35ImageRows & rows, + std::string & error); + std::unique_ptr vision_; // released while parked + // Fixed after init(); read by prepare_images() on request threads. + bool image_input_ = false; + vision::Qwen35VisionConfig vision_config_; + // Rotary position minus KV position for the sequence being decoded. + // Negative after an image prompt (an image spans fewer positions than + // tokens); zero for text. + int rope_delta_ = 0; + // ── Pflash drafter (lazy-loaded) ───────────────────────────────── DrafterContext drafter_ctx_; bool drafter_loaded_ = false; @@ -341,10 +370,12 @@ class Qwen35Backend : public ModelBackend { // Prefill a prompt and return the number of tokens committed to KV. // kv_offset > 0 resumes from a restored snapshot: tokens are placed at // KV positions [kv_offset, kv_offset + tokens.size()) instead of [0, N). + // `images` carries the encoded rows of an image prompt (kv_offset 0 only). int do_prefill(const std::vector & tokens, const DaemonIO & io, int snap_pos = -1, int snap_slot = -1, - int kv_offset = 0); + int kv_offset = 0, + const Qwen35ImageRows * images = nullptr); // Speculative decode loop: draft → verify → accept until EOS/max. // When budget_hook is non-null and (n_gen - generated) drops to the diff --git a/server/src/qwen35/qwen35_backend_images.cpp b/server/src/qwen35/qwen35_backend_images.cpp new file mode 100644 index 000000000..0a0d964d2 --- /dev/null +++ b/server/src/qwen35/qwen35_backend_images.cpp @@ -0,0 +1,103 @@ +// Image input for Qwen35Backend: projector loading, request preparation, and +// encoding. The prefill and decode changes live next to the code they touch +// in qwen35_backend.cpp. +#include "qwen35_backend.h" + +#include "common/vision/image_decode.h" +#include "qwen35_image_request.h" + +#include +#include +#include + +namespace dflash::common { + +namespace { +constexpr size_t MAX_IMAGES_PER_REQUEST = 8; +} + +bool Qwen35Backend::load_vision() { + if (cfg_.mmproj_path.empty()) return true; + if (w_.image_pad_id < 0) { + std::fprintf(stderr, "[vision] this model's vocabulary has no <|image_pad|> token\n"); + return false; + } + auto tower = std::make_unique(); + std::string error; + if (!tower->load(cfg_.mmproj_path, target_backend_, w_.n_embd, error)) { + std::fprintf(stderr, "[vision] %s\n", error.c_str()); + return false; + } + vision_config_ = tower->config(); + vision_ = std::move(tower); + image_input_ = true; + std::printf("[vision] projector loaded: %d layers, %.0f MiB, up to %d tokens per image\n", + vision_config_.layers, vision_->weight_bytes() / (1024.0 * 1024.0), + vision_config_.max_image_tokens); + return true; +} + +std::string Qwen35Backend::image_placeholder() const { + return image_input_ ? QWEN35_IMAGE_PLACEHOLDER : ""; +} + +bool Qwen35Backend::prepare_images(std::vector & tokens, std::vector images, + uint64_t context_capacity, uint64_t output_reserve, + ImagePromptHandle & payload, std::string & error) const { + payload.reset(); + if (images.empty()) { + // A pad typed into a text prompt would be embedded as an ordinary + // token, with no image behind it. + if (image_input_ && std::find(tokens.begin(), tokens.end(), w_.image_pad_id) != tokens.end()) { + error = "image marker in a prompt without images"; + return false; + } + return true; + } + if (!image_input_) { error = "this model was started without --mmproj"; return false; } + if (images.size() > MAX_IMAGES_PER_REQUEST) { error = "too many images in request"; return false; } + try { + auto prompt = std::make_shared(); + prompt->owner = this; + for (const EncodedImage & image : images) { + auto decoded = vision::decode_image({image.bytes.data(), image.bytes.size()}); + if (!decoded) { error = decoded.status.message; return false; } + vision::Qwen35Pixels pixels; + if (!vision::qwen35_vision_preprocess(vision_config_, decoded.image, pixels, error)) return false; + Qwen35ImageSlot slot; + slot.columns = pixels.grid_columns; + slot.rows = pixels.grid_rows; + prompt->slots.push_back(slot); + prompt->pixels.push_back(std::move(pixels)); + } + const uint64_t limit = context_capacity > output_reserve ? context_capacity - output_reserve : 0; + if (!qwen35_expand_image_tokens(tokens, w_.image_pad_id, prompt->slots, limit, error)) return false; + prompt->expanded_tokens = tokens; + prompt->positions = qwen35_image_rope_positions((int) tokens.size(), prompt->slots); + payload = std::move(prompt); + return true; + } catch (const std::bad_alloc &) { + error = "image preparation allocation failed"; + return false; + } +} + +bool Qwen35Backend::encode_images(const Qwen35ImagePrompt & prompt, Qwen35ImageRows & rows, + std::string & error) { + if (prompt.owner != this || !vision_) { + error = "image binding does not belong to the loaded backend"; + return false; + } + rows.prompt = &prompt; + rows.rows.resize(prompt.pixels.size()); + bool ok = true; + for (size_t i = 0; ok && i < prompt.pixels.size(); ++i) { + ok = vision_->encode(prompt.pixels[i], rows.rows[i], error); + } + // The attention scratch is large and only needed here; give it back + // before prefill sizes its own graphs. + vision_->release_scratch(); + return ok; +} + +} // namespace dflash::common diff --git a/server/src/qwen35/qwen35_image_prompt.cpp b/server/src/qwen35/qwen35_image_prompt.cpp new file mode 100644 index 000000000..6d46eaa5b --- /dev/null +++ b/server/src/qwen35/qwen35_image_prompt.cpp @@ -0,0 +1,84 @@ +#include "qwen35_image_prompt.h" + +#include +#include + +namespace dflash::common { + +bool qwen35_expand_image_tokens(std::vector & tokens, int32_t image_pad, + std::vector & slots, uint64_t max_tokens, + std::string & error) { + const size_t pads = (size_t) std::count(tokens.begin(), tokens.end(), image_pad); + if (pads != slots.size()) { + error = "prompt has " + std::to_string(pads) + " image markers for " + + std::to_string(slots.size()) + " images"; + return false; + } + uint64_t total = tokens.size() - pads; + for (const Qwen35ImageSlot & slot : slots) { + if (slot.tokens() <= 0) { error = "image has no tokens"; return false; } + total += (uint64_t) slot.tokens(); + } + if (total > max_tokens) { + error = "prompt with images needs " + std::to_string(total) + " tokens, the limit is " + + std::to_string(max_tokens); + return false; + } + std::vector expanded; + expanded.reserve((size_t) total); + size_t next_slot = 0; + for (int32_t token : tokens) { + if (token != image_pad) { expanded.push_back(token); continue; } + Qwen35ImageSlot & slot = slots[next_slot++]; + slot.begin = (int) expanded.size(); + expanded.insert(expanded.end(), (size_t) slot.tokens(), image_pad); + } + tokens.swap(expanded); + return true; +} + +Qwen35RopePositions qwen35_image_rope_positions(int prompt_tokens, const std::vector & slots) { + Qwen35RopePositions out; + out.temporal.resize((size_t) prompt_tokens); + out.height.resize((size_t) prompt_tokens); + out.width.resize((size_t) prompt_tokens); + int position = 0, token = 0; + const auto text_until = [&](int end) { + for (; token < end; ++token, ++position) { + out.temporal[token] = out.height[token] = out.width[token] = position; + } + }; + for (const Qwen35ImageSlot & slot : slots) { + text_until(slot.begin); + for (int row = 0; row < slot.rows; ++row) { + for (int column = 0; column < slot.columns; ++column, ++token) { + out.temporal[token] = position; + out.height[token] = position + row; + out.width[token] = position + column; + } + } + position += std::max(slot.rows, slot.columns); + } + text_until(prompt_tokens); + out.next = position; + return out; +} + +void qwen35_overwrite_image_rows(const Qwen35ImageSlot & slot, const float * rows, int hidden, + float * embeddings, int first, int count) { + const int begin = std::max(first, slot.begin); + const int end = std::min(first + count, slot.begin + slot.tokens()); + if (begin >= end) return; + std::memcpy(embeddings + (size_t) (begin - first) * hidden, + rows + (size_t) (begin - slot.begin) * hidden, + sizeof(float) * (size_t) (end - begin) * hidden); +} + +void Qwen35RopePositions::fill(int32_t * out, int first, int count) const { + std::copy_n(temporal.begin() + first, count, out); + std::copy_n(height.begin() + first, count, out + count); + std::copy_n(width.begin() + first, count, out + 2 * count); + std::fill_n(out + 3 * count, count, 0); +} + +} // namespace dflash::common diff --git a/server/src/qwen35/qwen35_image_prompt.h b/server/src/qwen35/qwen35_image_prompt.h new file mode 100644 index 000000000..5f788f757 --- /dev/null +++ b/server/src/qwen35/qwen35_image_prompt.h @@ -0,0 +1,51 @@ +// Prompt-side half of Qwen3.5 / Qwen3.8 image input: where each image sits in +// the token stream and which rotary positions its tokens take. Pure functions +// of the token ids and the image grids; no GPU, no projector. +#pragma once + +#include +#include +#include + +namespace dflash::common { + +// What the chat template carries for one image. The middle token is repeated +// once per image token by qwen35_expand_image_tokens(). +inline constexpr const char * QWEN35_IMAGE_PLACEHOLDER = "<|vision_start|><|image_pad|><|vision_end|>"; + +struct Qwen35ImageSlot { + int begin = 0; // first image token in the expanded prompt + int columns = 0, rows = 0; // image tokens across and down + + int tokens() const { return columns * rows; } +}; + +// `tokens` holds one `image_pad` per image, in order. Each becomes +// slot.tokens() pads and slot.begin is filled in. Fails when the number of +// pads differs from the number of slots or the result exceeds `max_tokens`. +bool qwen35_expand_image_tokens(std::vector & tokens, int32_t image_pad, + std::vector & slots, uint64_t max_tokens, + std::string & error); + +// Rotary positions of every prompt token. Text advances all three axes +// together. An image starting at position p holds the temporal axis at p and +// lays its rows and columns out from p on the other two; the text after it +// resumes at p + max(rows, columns). So positions run behind token indices +// after an image, by `next - prompt tokens`. +struct Qwen35RopePositions { + std::vector temporal, height, width; + int next = 0; // position of the first generated token + + // [4 * count] in the axis-major layout the target graph reads. + void fill(int32_t * out, int first, int count) const; +}; + +Qwen35RopePositions qwen35_image_rope_positions(int prompt_tokens, const std::vector & slots); + +// Copies the part of one image's `rows` that falls inside prompt tokens +// [first, first + count) over `embeddings`, which holds those `count` rows. +// Prefill runs in chunks, so an image can straddle several calls. +void qwen35_overwrite_image_rows(const Qwen35ImageSlot & slot, const float * rows, int hidden, + float * embeddings, int first, int count); + +} // namespace dflash::common diff --git a/server/src/qwen35/qwen35_image_request.h b/server/src/qwen35/qwen35_image_request.h new file mode 100644 index 000000000..1e7e4b7e6 --- /dev/null +++ b/server/src/qwen35/qwen35_image_request.h @@ -0,0 +1,39 @@ +// What an image request carries through the Qwen3.5 backend: the prompt +// binding built on the request thread, and the encoded rows built on the +// worker thread just before prefill. +#pragma once + +#include "common/image_prompt.h" +#include "qwen35_image_prompt.h" +#include "qwen35_vision.h" + +#include +#include + +namespace dflash::common { + +class Qwen35ImagePrompt final : public ImagePromptPayload { +public: + bool matches(const std::vector & tokens) const override { return tokens == expanded_tokens; } + + const void * owner = nullptr; // the backend that prepared it + std::vector expanded_tokens; // prompt with one pad per image token + std::vector slots; // one per image, in prompt order + std::vector pixels; // same order as slots + Qwen35RopePositions positions; +}; + +struct Qwen35ImageRows { + const Qwen35ImagePrompt * prompt = nullptr; + std::vector> rows; // per image: tokens() x hidden floats + + // Writes the image rows that fall inside prompt tokens [first, first + + // count) over `embeddings`, which holds `count` rows of `hidden` floats. + void overwrite(float * embeddings, int first, int count, int hidden) const { + for (size_t i = 0; i < rows.size(); ++i) { + qwen35_overwrite_image_rows(prompt->slots[i], rows[i].data(), hidden, embeddings, first, count); + } + } +}; + +} // namespace dflash::common diff --git a/server/src/qwen35/qwen35_target_graph.cpp b/server/src/qwen35/qwen35_target_graph.cpp index f0cd8ef87..1f93bc075 100644 --- a/server/src/qwen35/qwen35_target_graph.cpp +++ b/server/src/qwen35/qwen35_target_graph.cpp @@ -1204,12 +1204,15 @@ static ggml_tensor * build_full_attn_block( int sections[4]; for (int i = 0; i < 4; i++) sections[i] = rope_sections[i]; + // Interleaved M-RoPE, as the model is defined. For text the three axes + // carry the same position and the layout makes no difference; image + // tokens give each axis its own value, and then it does. Q = ggml_rope_multi(ctx, Q, positions, /*freq_factors=*/nullptr, - n_rot, sections, GGML_ROPE_TYPE_MROPE, + n_rot, sections, GGML_ROPE_TYPE_IMROPE, /*n_ctx_orig=*/0, w.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); Kcur = ggml_rope_multi(ctx, Kcur, positions, nullptr, - n_rot, sections, GGML_ROPE_TYPE_MROPE, + n_rot, sections, GGML_ROPE_TYPE_IMROPE, 0, w.rope_theta, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f); diff --git a/server/src/qwen35/qwen35_vision.cpp b/server/src/qwen35/qwen35_vision.cpp new file mode 100644 index 000000000..1f3e3a7a4 --- /dev/null +++ b/server/src/qwen35/qwen35_vision.cpp @@ -0,0 +1,379 @@ +#include "qwen35_vision.h" + +#include "../common/vision/image_resize.h" +#include "../common/vision/mmproj_file.h" + +#include "ggml-alloc.h" +#include "ggml.h" + +#include +#include +#include + +namespace dflash::vision { + +namespace { + +constexpr const char * PROJECTOR_TYPE = "qwen3vl_merger"; +// The model's own preprocessing refuses anything more elongated. +constexpr int MAX_ASPECT_RATIO = 200; +constexpr size_t GRAPH_NODES = 2048; + +struct Block { + ggml_tensor * ln1_w, * ln1_b, * qkv_w, * qkv_b, * out_w, * out_b; + ggml_tensor * ln2_w, * ln2_b, * up_w, * up_b, * down_w, * down_b; +}; + +ggml_tensor * layer_norm(ggml_context * ctx, ggml_tensor * x, ggml_tensor * w, ggml_tensor * b, float eps) { + return ggml_add(ctx, ggml_mul(ctx, ggml_norm(ctx, x, eps), w), b); +} + +ggml_tensor * linear(ggml_context * ctx, ggml_tensor * w, ggml_tensor * b, ggml_tensor * x) { + return ggml_add(ctx, ggml_mul_mat(ctx, w, x), b); +} + +struct ContextDeleter { + void operator()(ggml_context * ctx) const { ggml_free(ctx); } +}; + +} // namespace + +bool qwen35_vision_target_size(const Qwen35VisionConfig & config, int width, int height, + int & out_width, int & out_height, std::string & error) { + const int factor = config.patch_size * config.merge; + if (factor <= 0 || width <= 0 || height <= 0) { + error = "image has no pixels"; + return false; + } + if (std::max(width, height) > (int64_t) MAX_ASPECT_RATIO * std::min(width, height)) { + error = "image aspect ratio exceeds 200:1"; + return false; + } + const double area = double(width) * height; + const double min_pixels = double(config.min_image_tokens) * factor * factor; + const double max_pixels = double(config.max_image_tokens) * factor * factor; + // nearbyint rounds halves to even, like the reference implementation. + const auto nearest = [factor](double v) { return std::max(factor, int(std::nearbyint(v / factor)) * factor); }; + int w = nearest(width), h = nearest(height); + if (double(w) * h > max_pixels) { + const double shrink = std::sqrt(area / max_pixels); + w = std::max(factor, int(std::floor(width / shrink / factor)) * factor); + h = std::max(factor, int(std::floor(height / shrink / factor)) * factor); + } else if (double(w) * h < min_pixels) { + const double grow = std::sqrt(min_pixels / area); + w = int(std::ceil(width * grow / factor)) * factor; + h = int(std::ceil(height * grow / factor)) * factor; + } + out_width = w; + out_height = h; + return true; +} + +void qwen35_vision_position_rows(const std::vector & table, int side, int dimension, + int columns, int rows, int merge, std::vector & out) { + out.resize(size_t(columns) * rows * dimension); + // Aligned corners: point i of n lands on i * (side - 1) / (n - 1). + const auto source = [side](int i, int n) { return n > 1 ? double(i) * (side - 1) / (n - 1) : 0.0; }; + const auto row_of = [&](int y, int x) { return table.data() + (size_t(y) * side + x) * dimension; }; + float * dst = out.data(); + for (int by = 0; by < rows; by += merge) { + for (int bx = 0; bx < columns; bx += merge) { + for (int dy = 0; dy < merge; ++dy) { + for (int dx = 0; dx < merge; ++dx, dst += dimension) { + const double fy = source(by + dy, rows), fx = source(bx + dx, columns); + const int y0 = int(fy), x0 = int(fx); + const int y1 = std::min(y0 + 1, side - 1), x1 = std::min(x0 + 1, side - 1); + const float wy = float(fy - y0), wx = float(fx - x0); + const float * a = row_of(y0, x0), * b = row_of(y0, x1); + const float * c = row_of(y1, x0), * d = row_of(y1, x1); + for (int i = 0; i < dimension; ++i) { + dst[i] = (1.0f - wy) * ((1.0f - wx) * a[i] + wx * b[i]) + + wy * ((1.0f - wx) * c[i] + wx * d[i]); + } + } + } + } + } +} + +void qwen35_vision_rope_positions(int columns, int rows, int merge, std::vector & out) { + const size_t patches = size_t(columns) * rows; + out.resize(4 * patches); + size_t at = 0; + for (int by = 0; by < rows; by += merge) { + for (int bx = 0; bx < columns; bx += merge) { + for (int dy = 0; dy < merge; ++dy) { + for (int dx = 0; dx < merge; ++dx, ++at) { + out[at] = out[2 * patches + at] = by + dy; + out[patches + at] = out[3 * patches + at] = bx + dx; + } + } + } + } +} + +struct Qwen35VisionTower::Impl { + Qwen35VisionConfig config; + MmprojFile file; + ggml_backend_t backend = nullptr; + ggml_gallocr_t allocator = nullptr; + std::vector position_table; + std::vector blocks; + ggml_tensor * patch_first = nullptr, * patch_second = nullptr, * patch_bias = nullptr; + ggml_tensor * post_w = nullptr, * post_b = nullptr; + ggml_tensor * merge_in_w = nullptr, * merge_in_b = nullptr; + ggml_tensor * merge_out_w = nullptr, * merge_out_b = nullptr; + + ~Impl() { + if (allocator) ggml_gallocr_free(allocator); + } + + // Looks a tensor up and checks its shape; records the first failure. + ggml_tensor * need(const std::string & name, std::initializer_list shape, std::string & error) { + ggml_tensor * t = file.tensor(name); + bool ok = t != nullptr && shape.size() <= GGML_MAX_DIMS; + int axis = 0; + for (int64_t extent : shape) ok = ok && t->ne[axis++] == extent; + for (; ok && axis < GGML_MAX_DIMS; ++axis) ok = t->ne[axis] == 1; + if (!ok && error.empty()) error = "projector tensor is missing or has the wrong shape: " + name; + return ok ? t : nullptr; + } + + bool read_config(int language_dimension, std::string & error) { + uint32_t layers = 0, dimension = 0, heads = 0, intermediate = 0, patch = 0, merge = 0, projection = 0; + std::vector mean, deviation; + if (!file.u32("clip.vision.block_count", layers) || + !file.u32("clip.vision.embedding_length", dimension) || + !file.u32("clip.vision.attention.head_count", heads) || + !file.u32("clip.vision.feed_forward_length", intermediate) || + !file.u32("clip.vision.patch_size", patch) || + !file.u32("clip.vision.spatial_merge_size", merge) || + !file.u32("clip.vision.projection_dim", projection) || + !file.f32("clip.vision.attention.layer_norm_epsilon", config.epsilon) || + !file.f32_array("clip.vision.image_mean", mean) || mean.size() != 3 || + !file.f32_array("clip.vision.image_std", deviation) || deviation.size() != 3) { + error = "projector metadata is incomplete"; + return false; + } + // The patch reordering in encode() is written for 2x2 merging, and + // the vision rope splits each head into four equal sections. + if (layers == 0 || layers > 256 || heads == 0 || dimension == 0 || dimension > 16384 || + dimension % heads != 0 || (dimension / heads) % 4 != 0 || intermediate == 0 || + patch == 0 || patch > 64 || merge != 2) { + error = "projector geometry is not supported"; + return false; + } + if ((int) projection != language_dimension) { + error = "projector was built for a model of width " + std::to_string(projection) + + ", this model is " + std::to_string(language_dimension); + return false; + } + for (int i = 0; i < 3; ++i) { + if (!(deviation[i] > 0.0f)) { error = "projector image_std must be positive"; return false; } + config.mean[i] = mean[i]; + config.deviation[i] = deviation[i]; + } + config.layers = (int) layers; + config.dimension = (int) dimension; + config.heads = (int) heads; + config.intermediate = (int) intermediate; + config.patch_size = (int) patch; + config.merge = (int) merge; + config.language_dimension = language_dimension; + return true; + } + + bool bind_tensors(std::string & error) { + const int64_t d = config.dimension, ff = config.intermediate, p = config.patch_size; + const int64_t merged = d * config.merge * config.merge; + patch_first = need("v.patch_embd.weight", {p, p, 3, d}, error); + patch_second = need("v.patch_embd.weight.1", {p, p, 3, d}, error); + patch_bias = need("v.patch_embd.bias", {d}, error); + post_w = need("v.post_ln.weight", {d}, error); + post_b = need("v.post_ln.bias", {d}, error); + merge_in_w = need("mm.0.weight", {merged, merged}, error); + merge_in_b = need("mm.0.bias", {merged}, error); + merge_out_w = need("mm.2.weight", {merged, config.language_dimension}, error); + merge_out_b = need("mm.2.bias", {config.language_dimension}, error); + blocks.resize(config.layers); + for (int i = 0; i < config.layers; ++i) { + const std::string prefix = "v.blk." + std::to_string(i) + "."; + Block & b = blocks[i]; + b.ln1_w = need(prefix + "ln1.weight", {d}, error); + b.ln1_b = need(prefix + "ln1.bias", {d}, error); + b.qkv_w = need(prefix + "attn_qkv.weight", {d, 3 * d}, error); + b.qkv_b = need(prefix + "attn_qkv.bias", {3 * d}, error); + b.out_w = need(prefix + "attn_out.weight", {d, d}, error); + b.out_b = need(prefix + "attn_out.bias", {d}, error); + b.ln2_w = need(prefix + "ln2.weight", {d}, error); + b.ln2_b = need(prefix + "ln2.bias", {d}, error); + b.up_w = need(prefix + "ffn_up.weight", {d, ff}, error); + b.up_b = need(prefix + "ffn_up.bias", {ff}, error); + b.down_w = need(prefix + "ffn_down.weight", {ff, d}, error); + b.down_b = need(prefix + "ffn_down.bias", {d}, error); + } + if (!error.empty()) return false; + + ggml_tensor * table = file.tensor("v.position_embd.weight"); + const int side = table ? (int) std::lround(std::sqrt((double) table->ne[1])) : 0; + if (!table || table->type != GGML_TYPE_F32 || table->ne[0] != d || side < 2 || + (int64_t) side * side != table->ne[1]) { + error = "projector position table is missing or not a square F32 grid"; + return false; + } + config.position_side = side; + position_table.resize(size_t(side) * side * d); + ggml_backend_tensor_get(table, position_table.data(), 0, ggml_nbytes(table)); + return true; + } +}; + +Qwen35VisionTower::Qwen35VisionTower() : impl_(std::make_unique()) {} +Qwen35VisionTower::~Qwen35VisionTower() = default; + +const Qwen35VisionConfig & Qwen35VisionTower::config() const { return impl_->config; } +size_t Qwen35VisionTower::weight_bytes() const { return impl_->file.weight_bytes(); } + +bool Qwen35VisionTower::load(const std::string & path, ggml_backend_t backend, int language_dimension, + std::string & error) { + Impl & m = *impl_; + if (!m.file.load(path, backend, error)) return false; + if (m.file.projector_type() != PROJECTOR_TYPE) { + error = "projector type '" + m.file.projector_type() + "' is not " + PROJECTOR_TYPE; + return false; + } + if (m.file.has_tensor_prefix("v.deepstack.")) { + error = "projectors with deepstack branches are not supported"; + return false; + } + m.backend = backend; + return m.read_config(language_dimension, error) && m.bind_tensors(error); +} + +bool qwen35_vision_preprocess(const Qwen35VisionConfig & c, const DecodedRgb & image, + Qwen35Pixels & out, std::string & error) { + int width = 0, height = 0; + if (!qwen35_vision_target_size(c, (int) image.width, (int) image.height, width, height, error)) return false; + std::vector resized; + if (!resize_rgb_bicubic(image.pixels, (int) image.width, (int) image.height, width, height, resized, error)) { + return false; + } + out.width = width; + out.height = height; + out.grid_columns = width / (c.patch_size * c.merge); + out.grid_rows = height / (c.patch_size * c.merge); + const size_t plane = size_t(width) * height; + out.planar.resize(3 * plane); + for (int channel = 0; channel < 3; ++channel) { + float * dst = out.planar.data() + channel * plane; + for (size_t i = 0; i < plane; ++i) { + dst[i] = (resized[3 * i + channel] / 255.0f - c.mean[channel]) / c.deviation[channel]; + } + } + return true; +} + +bool Qwen35VisionTower::encode(const Qwen35Pixels & pixels, std::vector & rows, std::string & error) { + Impl & m = *impl_; + const Qwen35VisionConfig & c = m.config; + const int columns = pixels.width / c.patch_size, lines = pixels.height / c.patch_size; + const int64_t patches = int64_t(columns) * lines; + const int64_t tokens = pixels.tokens(); + if (!m.backend || tokens <= 0 || tokens > c.max_image_tokens || + columns != pixels.grid_columns * c.merge || lines != pixels.grid_rows * c.merge || + pixels.planar.size() != size_t(3) * pixels.width * pixels.height) { + error = "image was not prepared for this projector"; + return false; + } + + const int64_t d = c.dimension, head = d / c.heads; + ggml_init_params params{ggml_tensor_overhead() * GRAPH_NODES + ggml_graph_overhead_custom(GRAPH_NODES, false), + nullptr, /*no_alloc=*/true}; + std::unique_ptr owner(ggml_init(params)); + ggml_context * ctx = owner.get(); + if (!ctx) { error = "vision graph context allocation failed"; return false; } + ggml_cgraph * graph = ggml_new_graph_custom(ctx, GRAPH_NODES, false); + + ggml_tensor * image = ggml_new_tensor_4d(ctx, GGML_TYPE_F32, pixels.width, pixels.height, 3, 1); + ggml_tensor * position_rows = ggml_new_tensor_2d(ctx, GGML_TYPE_F32, d, patches); + ggml_tensor * rope_positions = ggml_new_tensor_1d(ctx, GGML_TYPE_I32, 4 * patches); + ggml_set_input(image); + ggml_set_input(position_rows); + ggml_set_input(rope_positions); + + // Patch embedding. The model was trained on two-frame clips; a still + // image is the same frame twice, so both temporal kernels see it. + const int p = c.patch_size; + ggml_tensor * x = ggml_add(ctx, ggml_conv_2d(ctx, m.patch_first, image, p, p, 0, 0, 1, 1), + ggml_conv_2d(ctx, m.patch_second, image, p, p, 0, 0, 1, 1)); + // [columns, lines, d] in raster order -> [d, patches] with the four + // patches of each 2x2 block adjacent, which is what the merger expects. + x = ggml_permute(ctx, x, 1, 2, 0, 3); + x = ggml_cont_4d(ctx, x, d * 2, columns / 2, lines, 1); + x = ggml_reshape_4d(ctx, x, d * 2, columns / 2, 2, lines / 2); + x = ggml_permute(ctx, x, 0, 2, 1, 3); + x = ggml_cont_2d(ctx, x, d, patches); + x = ggml_add(ctx, x, m.patch_bias); + x = ggml_add(ctx, x, position_rows); + + int sections[4] = {int(head / 4), int(head / 4), int(head / 4), int(head / 4)}; + const float attention_scale = 1.0f / std::sqrt(float(head)); + for (const Block & b : m.blocks) { + ggml_tensor * qkv = linear(ctx, b.qkv_w, b.qkv_b, layer_norm(ctx, x, b.ln1_w, b.ln1_b, c.epsilon)); + const auto part = [&](int index) { + return ggml_view_3d(ctx, qkv, head, c.heads, patches, ggml_row_size(qkv->type, head), qkv->nb[1], + ggml_row_size(qkv->type, d) * index); + }; + const auto rotate = [&](ggml_tensor * t) { + return ggml_rope_multi(ctx, t, rope_positions, nullptr, int(head / 2), sections, + GGML_ROPE_TYPE_VISION, 32768, 10000.0f, 1.0f, 0.0f, 1.0f, 32.0f, 1.0f); + }; + ggml_tensor * q = ggml_permute(ctx, rotate(part(0)), 0, 2, 1, 3); + ggml_tensor * k = ggml_permute(ctx, rotate(part(1)), 0, 2, 1, 3); + ggml_tensor * v = ggml_cont(ctx, ggml_permute(ctx, part(2), 1, 2, 0, 3)); + ggml_tensor * weights = ggml_soft_max_ext(ctx, ggml_mul_mat(ctx, k, q), nullptr, attention_scale, 0.0f); + ggml_tensor * mixed = ggml_permute(ctx, ggml_mul_mat(ctx, v, weights), 0, 2, 1, 3); + x = ggml_add(ctx, x, linear(ctx, b.out_w, b.out_b, ggml_cont_2d(ctx, mixed, d, patches))); + + ggml_tensor * hidden = linear(ctx, b.up_w, b.up_b, layer_norm(ctx, x, b.ln2_w, b.ln2_b, c.epsilon)); + x = ggml_add(ctx, x, linear(ctx, b.down_w, b.down_b, ggml_gelu(ctx, hidden))); + } + + // Merger: each block of four patches becomes one language-model row. + x = layer_norm(ctx, x, m.post_w, m.post_b, c.epsilon); + x = ggml_reshape_2d(ctx, x, d * c.merge * c.merge, tokens); + x = linear(ctx, m.merge_out_w, m.merge_out_b, ggml_gelu(ctx, linear(ctx, m.merge_in_w, m.merge_in_b, x))); + ggml_set_output(x); + ggml_build_forward_expand(graph, x); + + if (!m.allocator) m.allocator = ggml_gallocr_new(ggml_backend_get_default_buffer_type(m.backend)); + if (!m.allocator || !ggml_gallocr_alloc_graph(m.allocator, graph)) { + error = "not enough device memory for the vision tower scratch"; + return false; + } + + std::vector positions; + std::vector rope; + qwen35_vision_position_rows(m.position_table, c.position_side, c.dimension, columns, lines, c.merge, positions); + qwen35_vision_rope_positions(columns, lines, c.merge, rope); + ggml_backend_tensor_set(image, pixels.planar.data(), 0, ggml_nbytes(image)); + ggml_backend_tensor_set(position_rows, positions.data(), 0, ggml_nbytes(position_rows)); + ggml_backend_tensor_set(rope_positions, rope.data(), 0, ggml_nbytes(rope_positions)); + + if (ggml_backend_graph_compute(m.backend, graph) != GGML_STATUS_SUCCESS) { + error = "vision tower compute failed"; + return false; + } + rows.resize(size_t(tokens) * c.language_dimension); + ggml_backend_tensor_get(x, rows.data(), 0, ggml_nbytes(x)); + return true; +} + +void Qwen35VisionTower::release_scratch() { + if (impl_->allocator) { + ggml_gallocr_free(impl_->allocator); + impl_->allocator = nullptr; + } +} + +} // namespace dflash::vision diff --git a/server/src/qwen35/qwen35_vision.h b/server/src/qwen35/qwen35_vision.h new file mode 100644 index 000000000..a3c169550 --- /dev/null +++ b/server/src/qwen35/qwen35_vision.h @@ -0,0 +1,92 @@ +// Vision tower for Qwen3.5 / Qwen3.8 image input: the "qwen3vl_merger" +// projector published next to the model as an mmproj GGUF. Turns one image +// into rows of language-model embeddings, one per 2x2 block of patches. +// +// Split in two because the halves run on different threads: preprocessing is +// a CPU-only function of the config, safe while the GPU is busy or the tower +// is unloaded; encoding runs the tower graph and must be serialised with the +// rest of the backend's GPU work. +#pragma once + +#include "../common/vision/image_decode.h" + +#include "ggml-backend.h" + +#include +#include +#include +#include + +namespace dflash::vision { + +struct Qwen35VisionConfig { + int layers = 0, dimension = 0, heads = 0, intermediate = 0; + int patch_size = 0, merge = 0; + int position_side = 0; // the learned position table is side x side + int language_dimension = 0; // width of one output row + float epsilon = 1e-6f; + float mean[3] = {0.5f, 0.5f, 0.5f}; + float deviation[3] = {0.5f, 0.5f, 0.5f}; + // Image tokens per image. The lower bound is the model's own; the upper + // bound keeps the tower's attention scratch near 1 GiB. + int min_image_tokens = 64; + int max_image_tokens = 1024; +}; + +// A resized, normalised image. `planar` is channel major ([3, height, width]). +struct Qwen35Pixels { + int width = 0, height = 0; + int grid_columns = 0, grid_rows = 0; // image tokens across and down + std::vector planar; + + int tokens() const { return grid_columns * grid_rows; } +}; + +// Size the model resizes a width x height image to: both sides a multiple of +// patch_size * merge, area within the configured token bounds. +bool qwen35_vision_target_size(const Qwen35VisionConfig & config, int width, int height, + int & out_width, int & out_height, std::string & error); + +// Resizes the way the model was trained (bicubic, straight to the target +// size) and normalises. CPU only. +bool qwen35_vision_preprocess(const Qwen35VisionConfig & config, const DecodedRgb & image, + Qwen35Pixels & out, std::string & error); + +// Learned position rows for a columns x rows patch grid, bilinearly resampled +// from the side x side table with aligned corners and written in the tower's +// merged patch order. `table` and `out` hold `dimension` floats per position. +void qwen35_vision_position_rows(const std::vector & table, int side, int dimension, + int columns, int rows, int merge, std::vector & out); + +// Rotary positions of the patches, in merged order: [4 * patches] as the four +// consecutive axes (y, x, y, x) that ggml's vision rope expects. +void qwen35_vision_rope_positions(int columns, int rows, int merge, std::vector & out); + +class Qwen35VisionTower { +public: + Qwen35VisionTower(); + ~Qwen35VisionTower(); + Qwen35VisionTower(const Qwen35VisionTower &) = delete; + Qwen35VisionTower & operator=(const Qwen35VisionTower &) = delete; + + // `backend` must outlive the tower. `language_dimension` is the text + // model's hidden size; a projector built for another model is refused. + bool load(const std::string & path, ggml_backend_t backend, int language_dimension, + std::string & error); + + const Qwen35VisionConfig & config() const; + size_t weight_bytes() const; + + // Runs the tower. `rows` receives tokens() * language_dimension floats in + // reading order. One caller at a time. + bool encode(const Qwen35Pixels & pixels, std::vector & rows, std::string & error); + + // Frees the graph scratch between requests; the weights stay loaded. + void release_scratch(); + +private: + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace dflash::vision diff --git a/server/src/server/server_main.cpp b/server/src/server/server_main.cpp index 133b90d75..201feb079 100644 --- a/server/src/server/server_main.cpp +++ b/server/src/server/server_main.cpp @@ -85,7 +85,7 @@ static void print_usage(const char * prog) { " Defaults to the first block; request model names\n" " do not change generation routing.\n" " --draft Draft model for speculative decode\n" - " --mmproj DS4V image projector GGUF (heterogeneous HIP sparse mode)\n" + " --mmproj Vision projector GGUF: enables image input (Qwen3.5/3.8, DS4V)\n" " --port Listen port (default: 8080)\n" " --host Bind address (default: 0.0.0.0)\n" " --max-ctx Max context length (default: 131072)\n" diff --git a/server/test/test_qwen35_image.cpp b/server/test/test_qwen35_image.cpp new file mode 100644 index 000000000..027e7637e --- /dev/null +++ b/server/test/test_qwen35_image.cpp @@ -0,0 +1,122 @@ +// Qwen3.5 image input, the parts that need no GPU: target size, tower patch +// order, position tables, prompt expansion and rotary positions. +#include "qwen35/qwen35_image_prompt.h" +#include "qwen35/qwen35_vision.h" + +#include +#include +#include + +using namespace dflash::common; +using namespace dflash::vision; + +static int failures = 0; +static void check(bool condition, const char * message) { + if (!condition) { std::fprintf(stderr, "FAIL: %s\n", message); ++failures; } +} + +static Qwen35VisionConfig config() { + Qwen35VisionConfig c; + c.patch_size = 16; + c.merge = 2; + return c; +} + +// Expected sizes computed with the model's reference smart_resize +// (factor 32, 64 to 1024 image tokens). +static void test_target_size() { + const Qwen35VisionConfig c = config(); + int w = 0, h = 0; + std::string error; + check(qwen35_vision_target_size(c, 640, 480, w, h, error) && w == 640 && h == 480, "aligned size is kept"); + check(qwen35_vision_target_size(c, 4000, 3000, w, h, error) && w == 1152 && h == 864, "large image shrinks under the cap"); + check(qwen35_vision_target_size(c, 100, 50, w, h, error) && w == 384 && h == 192, "small image grows to the floor"); + check(qwen35_vision_target_size(c, 304, 272, w, h, error) && w == 320 && h == 256, "halves round to even multiples"); + check(!qwen35_vision_target_size(c, 3000, 10, w, h, error) && !error.empty(), "extreme aspect ratio is rejected"); + check(!qwen35_vision_target_size(c, 0, 10, w, h, error), "empty image is rejected"); +} + +static void test_tower_order() { + // 4 patches across, 2 down: two 2x2 blocks, each listed row by row. + std::vector rope; + qwen35_vision_rope_positions(4, 2, 2, rope); + const std::vector y = {0, 0, 1, 1, 0, 0, 1, 1}, x = {0, 1, 0, 1, 2, 3, 2, 3}; + bool ok = rope.size() == 32; + for (int i = 0; ok && i < 8; ++i) { + ok = rope[i] == y[i] && rope[8 + i] == x[i] && rope[16 + i] == y[i] && rope[24 + i] == x[i]; + } + check(ok, "rotary positions follow merged patch order"); + + // A 4x4 table read on a 4x4 grid is the table itself, in merged order. + std::vector table(16), out; + for (int i = 0; i < 16; ++i) table[i] = float(i); + qwen35_vision_position_rows(table, 4, 1, 4, 4, 2, out); + const std::vector same = {0, 1, 4, 5, 2, 3, 6, 7, 8, 9, 12, 13, 10, 11, 14, 15}; + check(out == same, "matching grid reads the table unchanged"); + + // On a 2x2 grid aligned corners land on the table's corners. + qwen35_vision_position_rows(table, 4, 1, 2, 2, 2, out); + check(out == std::vector({0, 3, 12, 15}), "coarser grid samples aligned corners"); + + // 4 points across a 3-wide table: 0, 2/3, 4/3, 2. + std::vector ramp = {0, 1, 2, 0, 1, 2, 0, 1, 2}; + qwen35_vision_position_rows(ramp, 3, 1, 4, 2, 2, out); + bool close = out.size() == 8; + const float expected[8] = {0.0f, 2.0f / 3, 0.0f, 2.0f / 3, 4.0f / 3, 2.0f, 4.0f / 3, 2.0f}; + for (int i = 0; close && i < 8; ++i) close = out[i] > expected[i] - 1e-5f && out[i] < expected[i] + 1e-5f; + check(close, "finer grid interpolates linearly"); +} + +static void test_prompt() { + constexpr int32_t PAD = 9; + std::vector tokens = {1, 2, PAD, 3, PAD, 4}; + std::vector slots(2); + slots[0].columns = 3; slots[0].rows = 2; + slots[1].columns = 1; slots[1].rows = 2; + std::string error; + check(qwen35_expand_image_tokens(tokens, PAD, slots, 64, error), "expansion succeeds"); + check(tokens == std::vector({1, 2, PAD, PAD, PAD, PAD, PAD, PAD, 3, PAD, PAD, 4}), "one pad per image token"); + check(slots[0].begin == 2 && slots[1].begin == 9, "slots record where their image starts"); + + const Qwen35RopePositions p = qwen35_image_rope_positions((int) tokens.size(), slots); + // Text 0,1. Image at 2: 2 rows x 3 columns, then text resumes at 2 + 3. + // Image at 6: 2 rows x 1 column, then text resumes at 6 + 2. + check(p.temporal == std::vector({0, 1, 2, 2, 2, 2, 2, 2, 5, 6, 6, 8}), "temporal axis holds still inside an image"); + check(p.height == std::vector({0, 1, 2, 2, 2, 3, 3, 3, 5, 6, 7, 8}), "height axis counts image rows"); + check(p.width == std::vector({0, 1, 2, 3, 4, 2, 3, 4, 5, 6, 6, 8}), "width axis counts image columns"); + check(p.next == 9, "generation resumes after the widest image side"); + + std::vector chunk(4 * 3, -1); + p.fill(chunk.data(), 7, 3); + check(chunk == std::vector({2, 5, 6, 3, 5, 6, 4, 5, 6, 0, 0, 0}), "chunk fill is axis major"); + + std::vector mismatch = {1, PAD}; + check(!qwen35_expand_image_tokens(mismatch, PAD, slots, 64, error), "marker count must match image count"); + std::vector small = {1, PAD, PAD}; + check(!qwen35_expand_image_tokens(small, PAD, slots, 8, error), "expanded prompt must fit the limit"); + check(small == std::vector({1, PAD, PAD}), "a refused prompt is left unchanged"); +} + +static void test_overwrite() { + Qwen35ImageSlot slot; + slot.begin = 3; slot.columns = 2; slot.rows = 2; // tokens 3..6 + const std::vector rows = {10, 11, 20, 21, 30, 31, 40, 41}; // hidden = 2 + std::vector chunk(3 * 2, 0.0f); + qwen35_overwrite_image_rows(slot, rows.data(), 2, chunk.data(), 0, 3); // tokens 0..2 + check(chunk == std::vector(6, 0.0f), "chunk before the image is untouched"); + qwen35_overwrite_image_rows(slot, rows.data(), 2, chunk.data(), 2, 3); // tokens 2..4 + check(chunk == std::vector({0, 0, 10, 11, 20, 21}), "image head lands at its offset"); + chunk.assign(6, 0.0f); + qwen35_overwrite_image_rows(slot, rows.data(), 2, chunk.data(), 5, 3); // tokens 5..7 + check(chunk == std::vector({30, 31, 40, 41, 0, 0}), "image tail continues in the next chunk"); +} + +int main() { + test_target_size(); + test_tower_order(); + test_prompt(); + test_overwrite(); + if (failures) { std::fprintf(stderr, "%d failure(s)\n", failures); return 1; } + std::printf("OK\n"); + return 0; +} From cbf86914df51ad90f9972a3f432676fd1acbb31f Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 17:17:26 +0200 Subject: [PATCH 108/123] fix: review findings in the page pool estimate and the MIX converter - gpu_page_pool: an amdgpu card whose GTT counter exists but cannot be read now yields no estimate, instead of a total that leaves its pages out. - ds4_mix_converter: check the safetensors data offset for overflow before the bounds test, refuse FP8 values that decode to infinity, stop requiring tokenizer_config.json (never used), drop --layer-start (only 0 was ever accepted). Co-Authored-By: Claude Fable 5.1 --- server/src/common/gpu_page_pool.cpp | 10 +++++++--- .../tools/ds4_mix_converter/ds4_mix_converter.cpp | 13 +++++-------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/server/src/common/gpu_page_pool.cpp b/server/src/common/gpu_page_pool.cpp index 5416e77a0..30291b289 100644 --- a/server/src/common/gpu_page_pool.cpp +++ b/server/src/common/gpu_page_pool.cpp @@ -24,8 +24,9 @@ constexpr const char * ACCOUNTED[] = { }; #if defined(__linux__) -// False when no amdgpu device reports its GTT use: the pool cannot then be told -// apart from pages owned by other drivers. +// False when no amdgpu device reports its GTT use, or when one has the counter +// but it cannot be read: the pool cannot then be told apart from pages that +// are in use. bool live_gpu_host_bytes(uint64_t & total) { total = 0; bool found = false; @@ -36,8 +37,11 @@ bool live_gpu_host_bytes(uint64_t & total) { // card0, card1, ... but not the connector entries such as card0-DP-1. if (name.rfind("card", 0) != 0 || name.find('-') != std::string::npos) continue; std::ifstream used("/sys/class/drm/" + name + "/device/mem_info_gtt_used"); + if (!used.is_open()) continue; // not an amdgpu device uint64_t bytes = 0; - if (used >> bytes) { total += bytes; found = true; } + if (!(used >> bytes)) { closedir(dir); return false; } + total += bytes; + found = true; } closedir(dir); return found; diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index fdff4f641..376ee1786 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -154,7 +154,6 @@ class SafeTensorSet { if (!fs::is_directory(root_)) fail("input is not a directory: " + root_.string()); config_ = read_json(root_ / "config.json"); tokenizer_ = read_json(root_ / "tokenizer.json"); - tokenizer_config_ = read_json(root_ / "tokenizer_config.json"); const json index = read_json(root_ / "model.safetensors.index.json"); if (!index.contains("weight_map") || !index["weight_map"].is_object()) { fail("model.safetensors.index.json has no object weight_map"); @@ -193,7 +192,6 @@ class SafeTensorSet { const std::unordered_map & entries() const { return entries_; } const json & config() const { return config_; } const json & tokenizer() const { return tokenizer_; } - const json & tokenizer_config() const { return tokenizer_config_; } private: static json read_json(const fs::path & path) { @@ -236,6 +234,9 @@ class SafeTensorSet { const auto range = info["data_offsets"].get>(); if (range.size() != 2 || range[1] < range[0]) fail("bad data_offsets for " + entry.name); entry.path = path; + if (range[0] > std::numeric_limits::max() - 8 - header_len) { + fail("data_offsets overflow for " + entry.name); + } entry.offset = 8 + header_len + range[0]; entry.size = range[1] - range[0]; const uint64_t expected = checked_mul(element_count(entry.shape, entry.name), dtype_size(entry.dtype), "byte size for " + entry.name); @@ -252,7 +253,6 @@ class SafeTensorSet { fs::path root_; json config_; json tokenizer_; - json tokenizer_config_; std::unordered_map entries_; }; @@ -558,14 +558,13 @@ struct Options { bool experts_only = false; bool force = false; bool validate_input_only = false; - int layer_start = 0; int layer_count = -1; int expert_limit = -1; }; void usage(const char * argv0) { std::cerr << "Usage: " << argv0 << " --input DIR --output FILE (--imatrix FILE | --absmax-only)\n" - << " [--layer-start N] [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force]\n"; + << " [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force]\n"; } int parse_nonnegative(const char * value, const std::string & option, bool allow_zero = true) { @@ -593,7 +592,6 @@ Options parse_options(int argc, char ** argv) { else if (arg == "--experts-only") out.experts_only = true; else if (arg == "--force") out.force = true; else if (arg == "--validate-input-only") out.validate_input_only = true; - else if (arg == "--layer-start") out.layer_start = parse_nonnegative(value(), arg); else if (arg == "--layer-count") out.layer_count = parse_nonnegative(value(), arg, false); else if (arg == "--expert-limit") out.expert_limit = parse_nonnegative(value(), arg, false); else if (arg == "--help" || arg == "-h") { usage(argv[0]); std::exit(0); } @@ -1081,7 +1079,7 @@ void write_dense_fp8(FILE * out, const StEntry & weight, const StEntry & scale) const uint8_t scale_byte = scales[(row/128)*scale_cols + col/128]; const float decoded = fp8_e4m3fn(input[col])*fp8_e8m0(scale_byte); const uint16_t b = float_to_bf16(decoded); - if (bf16_to_float(b) != decoded) { + if (!std::isfinite(decoded) || bf16_to_float(b) != decoded) { fail("FP8->BF16 is not exact for " + weight.name + " at row " + std::to_string(row) + " col " + std::to_string(col)); } @@ -1361,7 +1359,6 @@ int main(int argc, char ** argv) { static_assert(static_cast(GGML_TYPE_Q3_1_ROCMFP3_MIX) == static_cast(kQtypeP4Mix)); static_assert(static_cast(GGML_TYPE_Q2_1_ROCMFP2_MIX) == static_cast(kQtypeGuMix)); const Options options = parse_options(argc, argv); - if (options.layer_start != 0) fail("loader-compatible partial artifacts must start at layer 0"); SafeTensorSet source(options.input); const uint32_t source_layers = config_u32(source.config(), "num_hidden_layers"); const uint32_t source_experts = config_u32(source.config(), "n_routed_experts"); From f18750f4617b823a3d1158a6a912cb61f4d9e915 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 17:25:18 +0200 Subject: [PATCH 109/123] fix(qwen35): write the request-visible vision config once, never snapshot an image prefill load_vision() runs again on unpark; the config and capability flag that request threads read are now written only the first time. An image prefill also drops any snapshot request: snapshots are keyed by tokens and pad tokens do not identify an image. Co-Authored-By: Claude Fable 5.1 --- server/src/qwen35/qwen35_backend.cpp | 6 ++++++ server/src/qwen35/qwen35_backend_images.cpp | 8 ++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index 7688db53b..dc8bf3ef6 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1740,6 +1740,12 @@ int Qwen35Backend::do_prefill(const std::vector & tokens, std::fprintf(stderr, "prefill: an image prompt must start at position 0\n"); return -1; } + if (images) { + // Snapshots are found again by their tokens, and pads say nothing + // about which image they stood for. + snap_pos = -1; + snap_slot = -1; + } rope_delta_ = images ? images->prompt->positions.next - (int)tokens.size() : 0; // A finite --fa-window caps the full-attention layers to a sliding // window, so anything earlier than the window is invisible to them. That diff --git a/server/src/qwen35/qwen35_backend_images.cpp b/server/src/qwen35/qwen35_backend_images.cpp index 2c3b85015..e3c072597 100644 --- a/server/src/qwen35/qwen35_backend_images.cpp +++ b/server/src/qwen35/qwen35_backend_images.cpp @@ -28,9 +28,13 @@ bool Qwen35Backend::load_vision() { std::fprintf(stderr, "[vision] %s\n", error.c_str()); return false; } - vision_config_ = tower->config(); + // Request threads read these two without a lock, so they are written + // once: a reload after unpark finds them already set to the same values. + if (!image_input_) { + vision_config_ = tower->config(); + image_input_ = true; + } vision_ = std::move(tower); - image_input_ = true; std::printf("[vision] projector loaded: %d layers, %.0f MiB, up to %d tokens per image\n", vision_config_.layers, vision_->weight_bytes() / (1024.0 * 1024.0), vision_config_.max_image_tokens); From 8cca0e9484ce24fa84b555aaf1e4f34defedc9f6 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 17:50:54 +0200 Subject: [PATCH 110/123] fix(vision): review findings in the Qwen image path, the projector reader and DS4V admission - qwen35: a text request that restores an exact snapshot runs no prefill, so it could decode with the position offset the previous image request left behind. The offset is now reset on every restore. Checked with text, image, same text again: the restored request answers identically. - qwen35: a failed projector reload on unpark frees the target again instead of leaving it resident but marked parked; allocation failures while encoding become a request error and still release the tower scratch; the image count matches the server's transport limit of four. - deepseek4: with the whole model on one GPU, the per-layer prefill graph arenas are released before an image is admitted and rebuilt on demand. - server: warn at startup when a projector is loaded but image input ends up disabled (upstream forwarding or concurrent sequence scheduling). - mmproj_file: include , no pointer arithmetic on an empty array. - image_resize.h documents the one case that departs from Pillow's pass order. - docs: the image count and size limits are shared by every model; Qwen results on a Strix Halo alone. Co-Authored-By: Claude Fable 5.1 --- docs/image-input.md | 14 ++++++++------ server/src/common/vision/image_resize.h | 4 ++++ server/src/common/vision/mmproj_file.cpp | 4 +++- server/src/common/vision/mmproj_file.h | 1 + server/src/deepseek4/deepseek4_backend.cpp | 4 ++++ server/src/qwen35/qwen35_backend.cpp | 9 ++++++++- server/src/qwen35/qwen35_backend_images.cpp | 13 +++++++++---- server/src/server/http_server.cpp | 5 +++++ server/test/test_ds4v_image_integration.cpp | 1 + server/test/test_ds4v_image_policy.cpp | 1 + 10 files changed, 44 insertions(+), 12 deletions(-) diff --git a/docs/image-input.md b/docs/image-input.md index 5e1834f00..1beb8dca9 100644 --- a/docs/image-input.md +++ b/docs/image-input.md @@ -31,8 +31,9 @@ Use `POST /v1/chat/completions` with user-message content parts in display order ``` Only base64 JPEG/PNG data URLs are supported. Remote URLs, images outside user -content arrays, and image parts through other API formats are rejected. An -image is at most 16 MiB encoded. Decoder pixel and aspect limits also apply. A model's image marker cannot be supplied +content arrays, and image parts through other API formats are rejected. A +request carries at most four images, 16 MiB encoded each and 32 MiB combined. +Decoder pixel and aspect limits also apply. A model's image marker cannot be supplied as ordinary text. The server expands image markers after final rendering and tokenization, and @@ -58,7 +59,7 @@ The projector is read directly from the published file. Projectors with deepstack branches (Qwen3-VL) are refused. An image is resized the way the model was trained (bicubic, both sides to a multiple of 32 pixels) and costs one token per 32x32 pixels, between 64 and 1,024 tokens; larger images are -scaled down to the cap. Up to eight images per request. +scaled down to the cap. Image tokens take two-dimensional rotary positions, so positions run behind token counts after an image. Prefill handles that in its normal chunk loop; @@ -84,8 +85,11 @@ the published BF16 projector, thinking off: tokens). The projector adds 0.9 GiB of VRAM; the peak during image requests was 21.6 GiB against 20.8 GiB for text. +- The same requests answer correctly on a Strix Halo alone, where a + 1,012-token image prompt prefills in 4.6 s and decodes at 14 tok/s. + Not yet established: a comparison against the reference implementation on the -same questions, other GPUs, and CUDA. The tower uses only standard ggml +same questions, and CUDA. The tower uses only standard ggml operators, so nothing in it is HIP specific. ## DS4V @@ -112,8 +116,6 @@ Published llama.cpp conversions of the decoder load directly (image router bias named `blk.N.exp_probs_b_vl.bias`, no `deepseek4.vocab_size` key). Split GGUF files and llama.cpp's `clip` projector files are not read yet. -Requests permit at most four images and 32 MiB of encoded images combined. - One image request may be outstanding per backend. Its admission lease remains with the immutable payload through queueing and generation; another image request is rejected until that payload is released. This bounds simultaneous diff --git a/server/src/common/vision/image_resize.h b/server/src/common/vision/image_resize.h index 095d5b8d9..1ddad1ece 100644 --- a/server/src/common/vision/image_resize.h +++ b/server/src/common/vision/image_resize.h @@ -1,6 +1,10 @@ // Bicubic resize of 8-bit RGB that reproduces Pillow's Image.resize(BICUBIC) // byte for byte, including its antialiasing when shrinking. Vision models are // trained on images resized this way, so every model's preprocessing uses it. +// One exception: an image more than 100 times taller than wide that is being +// shortened goes vertically first to keep the intermediate buffer small, where +// Pillow always goes horizontally first; the two orders can differ by a +// rounding step. // Reference: Pillow 12.3.0 src/libImaging/Resample.c. #pragma once diff --git a/server/src/common/vision/mmproj_file.cpp b/server/src/common/vision/mmproj_file.cpp index 5ae6b5b4e..e85b32da0 100644 --- a/server/src/common/vision/mmproj_file.cpp +++ b/server/src/common/vision/mmproj_file.cpp @@ -86,8 +86,10 @@ bool MmprojFile::f32_array(const char * key, std::vector & out) const { const int64_t id = gguf_ ? gguf_find_key(gguf_, key) : -1; if (id < 0 || gguf_get_kv_type(gguf_, id) != GGUF_TYPE_ARRAY || gguf_get_arr_type(gguf_, id) != GGUF_TYPE_FLOAT32) return false; + const size_t count = gguf_get_arr_n(gguf_, id); const auto * values = static_cast(gguf_get_arr_data(gguf_, id)); - out.assign(values, values + gguf_get_arr_n(gguf_, id)); + out.clear(); + if (count > 0) out.assign(values, values + count); return true; } diff --git a/server/src/common/vision/mmproj_file.h b/server/src/common/vision/mmproj_file.h index 4470c5c2a..1de34060a 100644 --- a/server/src/common/vision/mmproj_file.h +++ b/server/src/common/vision/mmproj_file.h @@ -6,6 +6,7 @@ #include "ggml-backend.h" #include "ggml.h" +#include #include #include #include diff --git a/server/src/deepseek4/deepseek4_backend.cpp b/server/src/deepseek4/deepseek4_backend.cpp index ad38e9e8c..d45d9b762 100644 --- a/server/src/deepseek4/deepseek4_backend.cpp +++ b/server/src/deepseek4/deepseek4_backend.cpp @@ -1126,6 +1126,10 @@ bool DeepSeek4Backend::materialize_images(const DeepSeek4ImagePrompt & images, if (expert_backend_) ggml_backend_synchronize(expert_backend_); if (spec_backend_) ggml_backend_synchronize(spec_backend_); deepseek4_release_image_scratch(cache_, moe_hybrid_.get()); + // With the whole model on one GPU, text prefill keeps per-layer graph + // arenas alive between requests. They are rebuilt on demand, and the + // headroom measured below should not have to fit around them. + if (!moe_hybrid_) deepseek4_release_runtime_graphs(w_); reset_deepseek4_dspark_runtime_cache(); // Gallocr teardown leaves operator temporaries in legacy CUDA/HIP pools. // Retire their captured executables and activation memos through the diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index dc8bf3ef6..65e4555ac 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -913,7 +913,11 @@ bool Qwen35Backend::unpark(ParkTarget target) { std::fprintf(stderr, "[unpark] target: %s\n", luce_last_error()); return false; } - if (!load_vision()) return false; + if (!load_vision()) { + // Stay parked, so a retry starts from released weights. + free_target_weights(w_); + return false; + } kvflash_drafter_failed_ = false; // fresh VRAM: allow a retry target_parked_ = false; std::printf("[unpark] target restored\n"); std::fflush(stdout); @@ -1564,6 +1568,9 @@ GenerateResult Qwen35Backend::restore_and_generate_impl(int slot, out_io.emit(-1); return result; } + // An exact snapshot hit decodes without a prefill, so the offset a + // previous image request left behind must not survive into this one. + rope_delta_ = 0; if (slot < 0 || slot >= PREFIX_SLOTS || !prefix_snapshots_[slot].ctx) { result.fail(GenerateErrorCode::InvalidSnapshotSlot); out_io.emit(-1); diff --git a/server/src/qwen35/qwen35_backend_images.cpp b/server/src/qwen35/qwen35_backend_images.cpp index e3c072597..7cef73f2c 100644 --- a/server/src/qwen35/qwen35_backend_images.cpp +++ b/server/src/qwen35/qwen35_backend_images.cpp @@ -13,7 +13,7 @@ namespace luce::common { namespace { -constexpr size_t MAX_IMAGES_PER_REQUEST = 8; +constexpr size_t MAX_IMAGES_PER_REQUEST = 4; // the server's transport limit } bool Qwen35Backend::load_vision() { @@ -93,10 +93,15 @@ bool Qwen35Backend::encode_images(const Qwen35ImagePrompt & prompt, Qwen35ImageR return false; } rows.prompt = &prompt; - rows.rows.resize(prompt.pixels.size()); bool ok = true; - for (size_t i = 0; ok && i < prompt.pixels.size(); ++i) { - ok = vision_->encode(prompt.pixels[i], rows.rows[i], error); + try { + rows.rows.resize(prompt.pixels.size()); + for (size_t i = 0; ok && i < prompt.pixels.size(); ++i) { + ok = vision_->encode(prompt.pixels[i], rows.rows[i], error); + } + } catch (const std::bad_alloc &) { + error = "image encoding allocation failed"; + ok = false; } // The attention scratch is large and only needed here; give it back // before prefill sizes its own graphs. diff --git a/server/src/server/http_server.cpp b/server/src/server/http_server.cpp index 9b9e1a5c2..18d8c7973 100644 --- a/server/src/server/http_server.cpp +++ b/server/src/server/http_server.cpp @@ -1249,6 +1249,11 @@ HttpServer::HttpServer(luce::engine::LuceEngine & engine, { config_.image_input_enabled = backend_.supports_images() && config_.pflash_upstream_base.empty() && !backend_.seq_engine(); + if (backend_.supports_images() && !config_.image_input_enabled) { + std::fprintf(stderr, + "[server] WARNING: a vision projector is loaded but image input is off: it is " + "not available with upstream forwarding or concurrent sequence scheduling\n"); + } #ifdef LUCE_HAS_CURL curl_global_init(CURL_GLOBAL_DEFAULT); #endif diff --git a/server/test/test_ds4v_image_integration.cpp b/server/test/test_ds4v_image_integration.cpp index 70603b82a..45cb7967f 100644 --- a/server/test/test_ds4v_image_integration.cpp +++ b/server/test/test_ds4v_image_integration.cpp @@ -1,6 +1,7 @@ #include "deepseek4_image_budget.h" #include "deepseek4_image_spans.h" +#include #include #include #include diff --git a/server/test/test_ds4v_image_policy.cpp b/server/test/test_ds4v_image_policy.cpp index 6501f56bd..ff0e1268b 100644 --- a/server/test/test_ds4v_image_policy.cpp +++ b/server/test/test_ds4v_image_policy.cpp @@ -1,4 +1,5 @@ #include "deepseek4/deepseek4_image_policy.h" +#include #include #include #include From 27b08e7818bde80f672478e182900c468a062c5f Mon Sep 17 00:00:00 2001 From: mrciffa Date: Mon, 21 Sep 2026 18:10:52 +0200 Subject: [PATCH 111/123] fix(vision): release the projector before its backend, smaller review items - qwen35: shutdown resets the vision tower before freeing the target backend that owns its buffers; a projector reloaded after unpark must have the same geometry as the one request threads already preprocess for. - One spelling of the image pad token, shared by the loader and the prompt. - gpu_page_pool: use meminfo's Hugetlb total when present, which covers huge page pools of every size (test added). - MmprojFile documents that a failed load leaves the object unusable; drop the unused decode_error_name(); tests include for what they use. Co-Authored-By: Claude Fable 5.1 --- server/src/common/gpu_page_pool.cpp | 8 ++++++-- server/src/common/vision/image_decode.cpp | 13 ------------- server/src/common/vision/image_decode.h | 2 -- server/src/common/vision/mmproj_file.h | 1 + server/src/qwen35/gguf_target_loader.cpp | 3 ++- server/src/qwen35/qwen35_backend.cpp | 1 + server/src/qwen35/qwen35_backend_images.cpp | 5 ++++- server/src/qwen35/qwen35_image_prompt.h | 5 +++-- server/src/qwen35/qwen35_vision.h | 10 ++++++++++ server/test/test_gpu_page_pool.cpp | 9 +++++++++ server/test/test_moe_source_page_range.cpp | 1 + 11 files changed, 37 insertions(+), 21 deletions(-) diff --git a/server/src/common/gpu_page_pool.cpp b/server/src/common/gpu_page_pool.cpp index 30291b289..1189a0959 100644 --- a/server/src/common/gpu_page_pool.cpp +++ b/server/src/common/gpu_page_pool.cpp @@ -52,7 +52,8 @@ bool live_gpu_host_bytes(uint64_t & total) { uint64_t reclaimable_gpu_page_pool_bytes(const char * meminfo_text, uint64_t live_gpu_host_bytes) { if (!meminfo_text) return 0; - uint64_t total = 0, accounted = 0, huge_pages = 0, huge_page_kb = 0; + uint64_t total = 0, accounted = 0, huge_pages = 0, huge_page_kb = 0, hugetlb_kb = 0; + bool has_hugetlb = false; std::istringstream lines(meminfo_text); std::string line; while (std::getline(lines, line)) { @@ -63,9 +64,12 @@ uint64_t reclaimable_gpu_page_pool_bytes(const char * meminfo_text, uint64_t liv if (key == "MemTotal") total = value; else if (key == "HugePages_Total") huge_pages = value; else if (key == "Hugepagesize") huge_page_kb = value; + else if (key == "Hugetlb") { hugetlb_kb = value; has_hugetlb = true; } else for (const char * field : ACCOUNTED) if (key == field) accounted += value; } - accounted += huge_pages * huge_page_kb; + // Hugetlb covers pools of every page size; older kernels only report the + // default pool's page count. + accounted += has_hugetlb ? hugetlb_kb : huge_pages * huge_page_kb; if (total <= accounted) return 0; const uint64_t unattributed = (total - accounted) * 1024; const uint64_t in_use = live_gpu_host_bytes + OTHER_DRIVERS_MARGIN; diff --git a/server/src/common/vision/image_decode.cpp b/server/src/common/vision/image_decode.cpp index 6c799aba3..f9911290f 100644 --- a/server/src/common/vision/image_decode.cpp +++ b/server/src/common/vision/image_decode.cpp @@ -369,17 +369,4 @@ DecodeResult decode_image(const EncodedImageView & encoded, const DecodeLimits & #endif } -const char * decode_error_name(DecodeError error) { - switch (error) { - case DecodeError::None: return "none"; - case DecodeError::EmptyInput: return "empty_input"; - case DecodeError::EncodedTooLarge: return "encoded_too_large"; - case DecodeError::UnsupportedFormat: return "unsupported_format"; - case DecodeError::MalformedImage: return "malformed_image"; - case DecodeError::DecodedTooLarge: return "decoded_too_large"; - case DecodeError::AllocationFailed: return "allocation_failed"; - } - return "unknown"; -} - } // namespace luce::vision diff --git a/server/src/common/vision/image_decode.h b/server/src/common/vision/image_decode.h index 6836511fd..a31d81ce3 100644 --- a/server/src/common/vision/image_decode.h +++ b/server/src/common/vision/image_decode.h @@ -67,6 +67,4 @@ DecodeResult decode_image( const EncodedImageView & encoded, const DecodeLimits & limits = {}); -const char * decode_error_name(DecodeError error); - } // namespace luce::vision diff --git a/server/src/common/vision/mmproj_file.h b/server/src/common/vision/mmproj_file.h index 1de34060a..f194ab05e 100644 --- a/server/src/common/vision/mmproj_file.h +++ b/server/src/common/vision/mmproj_file.h @@ -23,6 +23,7 @@ class MmprojFile { MmprojFile & operator=(const MmprojFile &) = delete; // Loads every tensor onto `backend`, which must outlive this object. + // One attempt per object: after a failure, discard it. bool load(const std::string & path, ggml_backend_t backend, std::string & error); // "clip.projector_type", for example "qwen3vl_merger". Empty when absent. diff --git a/server/src/qwen35/gguf_target_loader.cpp b/server/src/qwen35/gguf_target_loader.cpp index 83e55032e..c3565c7aa 100644 --- a/server/src/qwen35/gguf_target_loader.cpp +++ b/server/src/qwen35/gguf_target_loader.cpp @@ -44,6 +44,7 @@ // tensor's bytes from the mmap'd file. #include "internal.h" +#include "qwen35_image_prompt.h" #include "common/derived_scalars.h" #include "common/gguf_inspect.h" #include "common/layer_split_utils.h" @@ -556,7 +557,7 @@ bool load_target_gguf_partial(const std::string & path, out.eos_chat_id = (raw_eos_chat == kEosKeyMissing) ? -1 : (int32_t)raw_eos_chat; std::printf("[loader] eos_id=%d eos_chat_id=%d\n", out.eos_id, out.eos_chat_id); } - out.image_pad_id = find_token_id(gctx, "<|image_pad|>"); + out.image_pad_id = find_token_id(gctx, QWEN35_IMAGE_PAD_TOKEN); // Compute capture layer IDs: evenly spaced through the target layers. // step = (n_layer - 2) / (N - 1), ids[k] = 1 + k * step. diff --git a/server/src/qwen35/qwen35_backend.cpp b/server/src/qwen35/qwen35_backend.cpp index 65e4555ac..2dbbaa4dd 100644 --- a/server/src/qwen35/qwen35_backend.cpp +++ b/server/src/qwen35/qwen35_backend.cpp @@ -1345,6 +1345,7 @@ void Qwen35Backend::shutdown() { free_prefix_snapshot(prefix_snapshots_[i]); } if (!target_parked_) free_target_weights(w_); + vision_.reset(); // its buffers belong to target_backend_, freed below if (!use_remote_draft && !draft_parked_) free_draft_weights(dw_); free_target_cache(cache_); if (split_gpus_ && draft_backend_) { diff --git a/server/src/qwen35/qwen35_backend_images.cpp b/server/src/qwen35/qwen35_backend_images.cpp index 7cef73f2c..152012aff 100644 --- a/server/src/qwen35/qwen35_backend_images.cpp +++ b/server/src/qwen35/qwen35_backend_images.cpp @@ -29,10 +29,13 @@ bool Qwen35Backend::load_vision() { return false; } // Request threads read these two without a lock, so they are written - // once: a reload after unpark finds them already set to the same values. + // once. A reload after unpark must bring back the same projector. if (!image_input_) { vision_config_ = tower->config(); image_input_ = true; + } else if (!tower->config().same_geometry(vision_config_)) { + std::fprintf(stderr, "[vision] the projector file changed while the model was parked\n"); + return false; } vision_ = std::move(tower); std::printf("[vision] projector loaded: %d layers, %.0f MiB, up to %d tokens per image\n", diff --git a/server/src/qwen35/qwen35_image_prompt.h b/server/src/qwen35/qwen35_image_prompt.h index 073051651..362ebdfb2 100644 --- a/server/src/qwen35/qwen35_image_prompt.h +++ b/server/src/qwen35/qwen35_image_prompt.h @@ -9,8 +9,9 @@ namespace luce::common { -// What the chat template carries for one image. The middle token is repeated -// once per image token by qwen35_expand_image_tokens(). +// What the chat template carries for one image. The middle token is the pad +// token, repeated once per image token by qwen35_expand_image_tokens(). +inline constexpr const char * QWEN35_IMAGE_PAD_TOKEN = "<|image_pad|>"; inline constexpr const char * QWEN35_IMAGE_PLACEHOLDER = "<|vision_start|><|image_pad|><|vision_end|>"; struct Qwen35ImageSlot { diff --git a/server/src/qwen35/qwen35_vision.h b/server/src/qwen35/qwen35_vision.h index 7cb21e77e..bb5a9a389 100644 --- a/server/src/qwen35/qwen35_vision.h +++ b/server/src/qwen35/qwen35_vision.h @@ -31,6 +31,16 @@ struct Qwen35VisionConfig { // bound keeps the tower's attention scratch near 1 GiB. int min_image_tokens = 64; int max_image_tokens = 1024; + + // Everything preprocessing and prompt expansion depend on. + bool same_geometry(const Qwen35VisionConfig & o) const { + return patch_size == o.patch_size && merge == o.merge && + language_dimension == o.language_dimension && + min_image_tokens == o.min_image_tokens && max_image_tokens == o.max_image_tokens && + mean[0] == o.mean[0] && mean[1] == o.mean[1] && mean[2] == o.mean[2] && + deviation[0] == o.deviation[0] && deviation[1] == o.deviation[1] && + deviation[2] == o.deviation[2]; + } }; // A resized, normalised image. `planar` is channel major ([3, height, width]). diff --git a/server/test/test_gpu_page_pool.cpp b/server/test/test_gpu_page_pool.cpp index 2fae219d3..65c0e4c2c 100644 --- a/server/test/test_gpu_page_pool.cpp +++ b/server/test/test_gpu_page_pool.cpp @@ -42,6 +42,15 @@ int main() { check(reclaimable_gpu_page_pool_bytes(huge, 0) == (100000000ULL - 10000000 - 40960000) * 1024 - GiB, "huge pages count as attributed"); + const char * mixed_huge = + "MemTotal: 100000000 kB\n" + "MemFree: 10000000 kB\n" + "HugePages_Total: 20000\n" + "Hugepagesize: 2048 kB\n" + "Hugetlb: 50000000 kB\n"; // includes a pool of another page size + check(reclaimable_gpu_page_pool_bytes(mixed_huge, 0) == (100000000ULL - 10000000 - 50000000) * 1024 - GiB, + "Hugetlb covers huge page pools of every size"); + check(reclaimable_gpu_page_pool_bytes("MemFree: 5 kB\n", 0) == 0, "no MemTotal, no estimate"); check(reclaimable_gpu_page_pool_bytes("", 0) == 0 && reclaimable_gpu_page_pool_bytes(nullptr, 0) == 0, "empty input"); diff --git a/server/test/test_moe_source_page_range.cpp b/server/test/test_moe_source_page_range.cpp index 2848bda26..dde857d4b 100644 --- a/server/test/test_moe_source_page_range.cpp +++ b/server/test/test_moe_source_page_range.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #if defined(__linux__) From 47e47038ff4d3441542c9c312328d1482f454e87 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 11:11:59 +0200 Subject: [PATCH 112/123] perf(qwen35): fused attention in the vision tower The tower attended the naive way: an F32 score matrix per head (3,900 patches squared for a 1,024-token image) through two GEMMs and a softmax, 41% of the encoder's time on an R9700. It now uses ggml's fused attention with half-precision keys and values, as the reference implementation does. Measured on the R9700 with the BF16 projector: a 975-token image encodes in 677 ms instead of 794 ms, which matches llama.cpp's 669 ms on the same image. On the 220-question AI2D/ChartQA set the scores are 85/100, 55/60 and 43/60 (before: 86, 55, 42), with 211 of 220 answers identical. The backend also logs the encode time per image request. Co-Authored-By: Claude Fable 5.1 --- docs/image-input.md | 9 ++++++--- server/src/qwen35/qwen35_backend_images.cpp | 10 ++++++++++ server/src/qwen35/qwen35_vision.cpp | 13 ++++++++----- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/image-input.md b/docs/image-input.md index 1beb8dca9..3941c430f 100644 --- a/docs/image-input.md +++ b/docs/image-input.md @@ -75,9 +75,12 @@ Measured on an R9700 alone with Qwen3.8-27B UD-IQ4_XS, the DFlash2 drafter and the published BF16 projector, thinking off: - 220 seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with - lmms-eval prompts: AI2D 86/100, ChartQA relaxed accuracy 55/60 (augmented) - and 42/60 (human), no errors. Image prompts average 448 tokens and prefill in - 0.74 s (largest 1,068 tokens, 1.9 s); decode runs at 31 to 35 tok/s. + lmms-eval prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented) + and 43/60 (human), no errors. Image prompts average 448 tokens and prefill in + 0.71 s (largest 1,068 tokens, 1.8 s); decode runs at 31 to 35 tok/s. +- llama.cpp (HIP build, `-fa on`, same GGUF, projector and image cap) answers + the same on every test image; on a 1,012-token image prompt it prefills in + 1.65 s to our 1.68 s, on a 323-token one in 0.61 s to our 0.50 s. - An image inside a 6,271-token prompt, two images in one request, and a follow-up turn after an image all answer correctly. - Text requests are byte-identical to a build without image support, at the diff --git a/server/src/qwen35/qwen35_backend_images.cpp b/server/src/qwen35/qwen35_backend_images.cpp index 152012aff..9020ebaee 100644 --- a/server/src/qwen35/qwen35_backend_images.cpp +++ b/server/src/qwen35/qwen35_backend_images.cpp @@ -7,6 +7,7 @@ #include "qwen35_image_request.h" #include +#include #include #include @@ -97,15 +98,24 @@ bool Qwen35Backend::encode_images(const Qwen35ImagePrompt & prompt, Qwen35ImageR } rows.prompt = &prompt; bool ok = true; + int tokens = 0; + const auto start = std::chrono::steady_clock::now(); try { rows.rows.resize(prompt.pixels.size()); for (size_t i = 0; ok && i < prompt.pixels.size(); ++i) { ok = vision_->encode(prompt.pixels[i], rows.rows[i], error); + tokens += prompt.pixels[i].tokens(); } } catch (const std::bad_alloc &) { error = "image encoding allocation failed"; ok = false; } + ggml_backend_synchronize(target_backend_); + const double ms = std::chrono::duration(std::chrono::steady_clock::now() - start).count(); + if (ok) { + std::printf("[vision] encoded %zu image(s), %d tokens, in %.0f ms\n", prompt.pixels.size(), tokens, ms); + std::fflush(stdout); + } // The attention scratch is large and only needed here; give it back // before prefill sizes its own graphs. vision_->release_scratch(); diff --git a/server/src/qwen35/qwen35_vision.cpp b/server/src/qwen35/qwen35_vision.cpp index c9eb72317..205894395 100644 --- a/server/src/qwen35/qwen35_vision.cpp +++ b/server/src/qwen35/qwen35_vision.cpp @@ -328,12 +328,15 @@ bool Qwen35VisionTower::encode(const Qwen35Pixels & pixels, std::vector & return ggml_rope_multi(ctx, t, rope_positions, nullptr, int(head / 2), sections, GGML_ROPE_TYPE_VISION, 32768, 10000.0f, 1.0f, 0.0f, 1.0f, 32.0f, 1.0f); }; + // Fused attention with half-precision keys and values, as the + // reference implementation runs it: the score matrix of a large + // image (3,900 patches squared per head) never touches memory. ggml_tensor * q = ggml_permute(ctx, rotate(part(0)), 0, 2, 1, 3); - ggml_tensor * k = ggml_permute(ctx, rotate(part(1)), 0, 2, 1, 3); - ggml_tensor * v = ggml_cont(ctx, ggml_permute(ctx, part(2), 1, 2, 0, 3)); - ggml_tensor * weights = ggml_soft_max_ext(ctx, ggml_mul_mat(ctx, k, q), nullptr, attention_scale, 0.0f); - ggml_tensor * mixed = ggml_permute(ctx, ggml_mul_mat(ctx, v, weights), 0, 2, 1, 3); - x = ggml_add(ctx, x, linear(ctx, b.out_w, b.out_b, ggml_cont_2d(ctx, mixed, d, patches))); + ggml_tensor * k = ggml_cast(ctx, ggml_permute(ctx, rotate(part(1)), 0, 2, 1, 3), GGML_TYPE_F16); + ggml_tensor * v = ggml_cast(ctx, ggml_permute(ctx, part(2), 0, 2, 1, 3), GGML_TYPE_F16); + ggml_tensor * mixed = ggml_flash_attn_ext(ctx, q, k, v, nullptr, attention_scale, 0.0f, 0.0f); + ggml_flash_attn_ext_set_prec(mixed, GGML_PREC_F32); + x = ggml_add(ctx, x, linear(ctx, b.out_w, b.out_b, ggml_reshape_2d(ctx, mixed, d, patches))); ggml_tensor * hidden = linear(ctx, b.up_w, b.up_b, layer_norm(ctx, x, b.ln2_w, b.ln2_b, c.epsilon)); x = ggml_add(ctx, x, linear(ctx, b.down_w, b.down_b, ggml_gelu(ctx, hidden))); From 901ef2d79087616b7edf3713d3d36120391915cc Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 11:16:42 +0200 Subject: [PATCH 113/123] docs(vision): Q8_0 projector measurement Co-Authored-By: Claude Fable 5.1 --- docs/image-input.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/image-input.md b/docs/image-input.md index 3941c430f..b209b536d 100644 --- a/docs/image-input.md +++ b/docs/image-input.md @@ -78,6 +78,10 @@ the published BF16 projector, thinking off: lmms-eval prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented) and 43/60 (human), no errors. Image prompts average 448 tokens and prefill in 0.71 s (largest 1,068 tokens, 1.8 s); decode runs at 31 to 35 tok/s. +- A projector with its weight matrices in Q8_0 (rows that are not a multiple + of 32 stay F16) encodes a 975-token image in 443 ms instead of 677 ms with + the BF16 file, with the same scores on the 220 questions and 216 identical + answers. Prefer one when available. - llama.cpp (HIP build, `-fa on`, same GGUF, projector and image cap) answers the same on every test image; on a 1,012-token image prompt it prefills in 1.65 s to our 1.68 s, on a 323-token one in 0.61 s to our 0.50 s. From c20286730c08e25691f1b9433e8562220714400b Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 13:49:22 +0200 Subject: [PATCH 114/123] perf(tools): quantize experts on every core in the MIX converter The codebook fit and the encode pass ran one expert at a time on one core: about 32 minutes for the fit and 10 hours for the encode of a 43-layer, 256-expert checkpoint. Both passes now run their per-expert work on a pool of threads and consume the results in expert order, so the output is byte for byte the file the sequential converter wrote (checked with SHA-256 on a one-layer slice) and the unit test is unchanged. --threads N overrides the core count. Co-Authored-By: Claude Fable 5.1 --- .../ds4_mix_converter/ds4_mix_converter.cpp | 144 ++++++++++++++---- 1 file changed, 117 insertions(+), 27 deletions(-) diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index 376ee1786..0681b07db 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -6,7 +6,13 @@ #include #include +#include #include +#include +#include +#include +#include +#include #include #include #include @@ -50,6 +56,63 @@ constexpr size_t kCopyChunk = 8u * 1024u * 1024u; throw std::runtime_error(message); } +// Worker threads for the per-expert passes; set once from --threads. +unsigned g_threads = 1; + +// Runs work(i) for i in [0, n) on g_threads threads and hands each result to +// consume(i, result) in index order, so the output does not depend on the +// thread count. Workers run at most two batches ahead of the consumer. +template +void for_each_expert_ordered(uint32_t n, + const std::function & work, + const std::function & consume) { + const unsigned threads = std::max(1u, std::min(g_threads, n == 0 ? 1 : n)); + const uint32_t window = threads * 2; + std::vector> slots(n); + std::mutex mutex; + std::condition_variable ready; + std::atomic next{0}; + std::exception_ptr error; + uint32_t consumed = 0; + auto worker = [&] { + for (;;) { + const uint32_t i = next.fetch_add(1); + if (i >= n) return; + { + std::unique_lock lock(mutex); + ready.wait(lock, [&] { return error || i < consumed + window; }); + if (error) return; + } + try { + Result result = work(i); + std::lock_guard lock(mutex); + slots[i] = std::move(result); + } catch (...) { + std::lock_guard lock(mutex); + if (!error) error = std::current_exception(); + } + ready.notify_all(); + } + }; + std::vector pool; + for (unsigned t = 0; t < threads; ++t) pool.emplace_back(worker); + for (uint32_t i = 0; i < n; ++i) { + Result result; + { + std::unique_lock lock(mutex); + ready.wait(lock, [&] { return error || slots[i].has_value(); }); + if (error) break; + result = std::move(*slots[i]); + slots[i].reset(); + consumed = i + 1; + } + ready.notify_all(); + consume(i, result); + } + for (auto & t : pool) t.join(); + if (error) std::rethrow_exception(error); +} + uint64_t checked_mul(uint64_t a, uint64_t b, const std::string & what) { if (a != 0 && b > std::numeric_limits::max() / a) { fail("overflow computing " + what); @@ -560,11 +623,12 @@ struct Options { bool validate_input_only = false; int layer_count = -1; int expert_limit = -1; + int threads = 0; // 0 = every core }; void usage(const char * argv0) { std::cerr << "Usage: " << argv0 << " --input DIR --output FILE (--imatrix FILE | --absmax-only)\n" - << " [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force]\n"; + << " [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force] [--threads N]\n"; } int parse_nonnegative(const char * value, const std::string & option, bool allow_zero = true) { @@ -594,6 +658,7 @@ Options parse_options(int argc, char ** argv) { else if (arg == "--validate-input-only") out.validate_input_only = true; else if (arg == "--layer-count") out.layer_count = parse_nonnegative(value(), arg, false); else if (arg == "--expert-limit") out.expert_limit = parse_nonnegative(value(), arg, false); + else if (arg == "--threads") out.threads = parse_nonnegative(value(), arg, false); else if (arg == "--help" || arg == "-h") { usage(argv[0]); std::exit(0); } else fail("unknown option " + arg); } @@ -1104,37 +1169,49 @@ void write_expert_tensor(FILE * out, const SafeTensorSet & source, ? calibration.gate_up : calibration.down; const std::string target = target_expert_name(calibration.layer, recipe); const std::vector * importance = require_imatrix(imatrix, target, expected.in); - std::vector packed, scales; - std::vector values; - std::vector q2(expected.in/kBlock); - std::vector q3(expected.in/kBlock); - for (uint32_t expert = 0; expert < experts; ++expert) { + if (recipe.qtype != GGML_TYPE_Q2_1_ROCMFP2_MIX && recipe.qtype != GGML_TYPE_Q3_1_ROCMFP3_MIX) { + fail("recipe table contains unsupported qtype"); + } + const std::function(uint32_t)> encode_expert = [&](uint32_t expert) { const TensorShape shape = validate_expert_source(source, calibration.layer, expert, recipe); if (shape.in != expected.in || shape.out != expected.out) fail("expert shape drift in " + target); const StEntry & w = source.at(source_expert_name(calibration.layer, expert, recipe, "weight")); const StEntry & s = source.at(source_expert_name(calibration.layer, expert, recipe, "scale")); OpenTensorPair input(w, s); const auto & books = registry.experts.at(expert); + std::vector packed, scales; + std::vector values; + std::vector q2(expected.in/kBlock); + std::vector q3(expected.in/kBlock); + const size_t row_bytes = recipe.qtype == GGML_TYPE_Q2_1_ROCMFP2_MIX + ? q2.size()*sizeof(q2[0]) : q3.size()*sizeof(q3[0]); + std::vector bytes; + bytes.reserve(static_cast(shape.out) * row_bytes); for (uint32_t row = 0; row < shape.out; ++row) { decode_expert_row(input, row, shape.in, packed, scales, values); + const uint8_t * encoded = nullptr; if (recipe.qtype == GGML_TYPE_Q2_1_ROCMFP2_MIX) { if (!rocmfpx_quantize_row_fp2_mix_ref(values.data(), q2.data(), shape.in, books.data(), importance ? importance->data() : nullptr)) { fail("qtype-106 reference encoder rejected " + w.name); } - fwrite_exact(out, q2.data(), q2.size()*sizeof(q2[0]), target); - } else if (recipe.qtype == GGML_TYPE_Q3_1_ROCMFP3_MIX) { + encoded = reinterpret_cast(q2.data()); + } else { if (!rocmfpx_quantize_row_fp3_mix_ref(values.data(), q3.data(), shape.in, books.data(), importance ? importance->data() : nullptr)) { fail("qtype-105 reference encoder rejected " + w.name); } - fwrite_exact(out, q3.data(), q3.size()*sizeof(q3[0]), target); - } else { - fail("recipe table contains unsupported qtype"); + encoded = reinterpret_cast(q3.data()); } + bytes.insert(bytes.end(), encoded, encoded + row_bytes); } + return bytes; + }; + const std::function &)> write = [&](uint32_t expert, std::vector & bytes) { + fwrite_exact(out, bytes.data(), bytes.size(), target); std::cerr << "[encode] " << target << " expert " << (expert + 1) << "/" << experts << "\n"; - } + }; + for_each_expert_ordered>(experts, encode_expert, write); } std::vector validate_input_layout( @@ -1170,14 +1247,19 @@ std::vector calibrate( current.layer = static_cast(layer); current.gate_up.levels = kGuLevels; current.down.levels = kP4Levels; - for (uint32_t expert = 0; expert < experts; ++expert) { + struct ExpertFit { + TensorShape gate_shape, down_shape; + std::vector gate_up, down; + std::vector repairs; + }; + const std::function fit_expert = [&](uint32_t expert) { + ExpertFit fit; HistogramFitter gate_up_fitter; - TensorShape gate_shape{}; for (const ExpertRecipe & recipe : kExpertRecipes) { if (recipe.books != BookSource::GateUpJoint) continue; const TensorShape shape = validate_expert_source(source, layer, expert, recipe); - if (gate_shape.in == 0) gate_shape = shape; - if (shape.in != gate_shape.in || shape.out != gate_shape.out) { + if (fit.gate_shape.in == 0) fit.gate_shape = shape; + if (shape.in != fit.gate_shape.in || shape.out != fit.gate_shape.out) { fail("qtype-106 gate/up shape mismatch at layer " + std::to_string(layer)); } const std::string target = target_expert_name(layer, recipe); @@ -1186,23 +1268,29 @@ std::vector calibrate( } HistogramFitter down_fitter; const ExpertRecipe & down_recipe = kExpertRecipes[2]; - const TensorShape down_shape = validate_expert_source(source, layer, expert, down_recipe); + fit.down_shape = validate_expert_source(source, layer, expert, down_recipe); const auto * down_importance = require_imatrix( - imatrix, target_expert_name(layer, down_recipe), down_shape.in); + imatrix, target_expert_name(layer, down_recipe), fit.down_shape.in); add_expert_to_fitter(source, layer, expert, down_recipe, down_importance, down_fitter); - - if (current.gate_up_shape.in == 0) current.gate_up_shape = gate_shape; - if (current.down_shape.in == 0) current.down_shape = down_shape; - if (current.gate_up_shape.in != gate_shape.in || current.gate_up_shape.out != gate_shape.out || - current.down_shape.in != down_shape.in || current.down_shape.out != down_shape.out) { + const std::string label = "layer=" + std::to_string(layer) + " expert=" + std::to_string(expert); + fit.gate_up = gate_up_fitter.fit(kGuLevels, label + " gate_up", &fit.repairs); + fit.down = down_fitter.fit(kP4Levels, label + " down", &fit.repairs); + return fit; + }; + const std::function keep = [&](uint32_t expert, ExpertFit & fit) { + if (current.gate_up_shape.in == 0) current.gate_up_shape = fit.gate_shape; + if (current.down_shape.in == 0) current.down_shape = fit.down_shape; + if (current.gate_up_shape.in != fit.gate_shape.in || current.gate_up_shape.out != fit.gate_shape.out || + current.down_shape.in != fit.down_shape.in || current.down_shape.out != fit.down_shape.out) { fail("expert shapes vary within layer " + std::to_string(layer)); } - const std::string label = "layer=" + std::to_string(layer) + " expert=" + std::to_string(expert); - current.gate_up.experts.push_back(gate_up_fitter.fit(kGuLevels, label + " gate_up", ¤t.repairs)); - current.down.experts.push_back(down_fitter.fit(kP4Levels, label + " down", ¤t.repairs)); + current.gate_up.experts.push_back(std::move(fit.gate_up)); + current.down.experts.push_back(std::move(fit.down)); + for (auto & stamp : fit.repairs) current.repairs.push_back(std::move(stamp)); std::cerr << "[calibration] layer " << layer << " expert " << (expert + 1) << "/" << experts << " fitted joint gate/up and down codebooks\n"; - } + }; + for_each_expert_ordered(experts, fit_expert, keep); result.push_back(std::move(current)); } return result; @@ -1359,6 +1447,8 @@ int main(int argc, char ** argv) { static_assert(static_cast(GGML_TYPE_Q3_1_ROCMFP3_MIX) == static_cast(kQtypeP4Mix)); static_assert(static_cast(GGML_TYPE_Q2_1_ROCMFP2_MIX) == static_cast(kQtypeGuMix)); const Options options = parse_options(argc, argv); + g_threads = options.threads > 0 ? static_cast(options.threads) + : std::max(1u, std::thread::hardware_concurrency()); SafeTensorSet source(options.input); const uint32_t source_layers = config_u32(source.config(), "num_hidden_layers"); const uint32_t source_experts = config_u32(source.config(), "n_routed_experts"); From 6e5a0ca7ed70da89ae74a5e144d6eee1c3bf6cd9 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 14:31:25 +0200 Subject: [PATCH 115/123] fix(deepseek4): grouped output projection only for a quantized tensor The layer-major prefill built the attention output projection in the grouped source layout unconditionally. That layout is read by MMQ's activation quantizer and by nothing else, so a model whose attn_output_b is stored unquantized (BF16, as the MIX converter writes dense tensors) aborted on its first prefill with GGML_ASSERT(use_mul_mat_q). Such a tensor now takes the plain projection path; quantized files are unchanged. Co-Authored-By: Claude Fable 5.1 --- server/src/deepseek4/deepseek4_graph.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/deepseek4/deepseek4_graph.cpp b/server/src/deepseek4/deepseek4_graph.cpp index b3bfb1f11..cb91d7387 100644 --- a/server/src/deepseek4/deepseek4_graph.cpp +++ b/server/src/deepseek4/deepseek4_graph.cpp @@ -2067,8 +2067,11 @@ static ggml_tensor * build_mla_output_projection( ctx, L.attn_output_a, group_dim, w.n_lora_o, w.n_out_group); ggml_tensor * attn_low = ds4_mul_mat_columns(ctx, out_a_3d, attn_out, projection_columns); + // The grouped source layout is read by MMQ's activation quantizer and by + // nothing else, so a projection stored unquantized (BF16 attention from a + // converter that leaves dense tensors alone) takes the plain path. const bool grouped_output_projection = - allow_grouped && n_tokens > 1 && + allow_grouped && n_tokens > 1 && ggml_is_quantized(L.attn_output_b->type) && !ds4_env_flag("LUCE_DS4_DISABLE_GROUPED_OUTPUT_PROJECTION"); if (grouped_output_projection) { return ggml_mul_mat_grouped_src(ctx, L.attn_output_b, attn_low); From 0a7ed37ae7cfc762d1eb963803e1a068db0e653b Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 14:31:25 +0200 Subject: [PATCH 116/123] fix(tools): name the MIX file after its checkpoint general.name was a hardcoded string from the contributor's test checkpoint; it now comes from the config's _name_or_path or the input directory. Co-Authored-By: Claude Fable 5.1 --- .../tools/ds4_mix_converter/ds4_mix_converter.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index 0681b07db..3e9f92ca1 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -974,12 +974,24 @@ std::vector make_plan( return plan; } +// The checkpoint's own name when its config carries one, else the input +// directory's name; either way marked as a MIX build. +std::string model_name(const SafeTensorSet & source) { + const json & c = source.config(); + std::string base; + if (c.contains("_name_or_path") && c["_name_or_path"].is_string()) base = c["_name_or_path"].get(); + if (base.empty()) base = source.root().filename().string(); + const auto slash = base.find_last_of('/'); + if (slash != std::string::npos) base = base.substr(slash + 1); + return base.empty() ? "DeepSeek-V4 MIX" : base + " MIX"; +} + void set_model_metadata(gguf_context * ctx, const SafeTensorSet & source, uint32_t layers, uint32_t experts, bool absmax_only, bool smoke_artifact, const std::vector & p4_blob) { const json & c = source.config(); gguf_set_val_str(ctx, "general.architecture", "deepseek4"); - gguf_set_val_str(ctx, "general.name", "DeepSeek-V4-Flash-Vision-Uncensored MIX"); + gguf_set_val_str(ctx, "general.name", model_name(source).c_str()); gguf_set_val_u32(ctx, "general.alignment", kAlignment); gguf_set_val_u32(ctx, "general.file_type", 119); gguf_set_val_str(ctx, "deepseek4.mix.calibration", From 61c07ff936c4ae83baae2c5a402bc42121634022 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 15:13:07 +0200 Subject: [PATCH 117/123] docs(vision): DS4V on our own ROCMFP MIX conversion Co-Authored-By: Claude Fable 5.1 --- docs/image-input.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/image-input.md b/docs/image-input.md index b209b536d..55eb33f42 100644 --- a/docs/image-input.md +++ b/docs/image-input.md @@ -123,6 +123,14 @@ Published llama.cpp conversions of the decoder load directly (image router bias named `blk.N.exp_probs_b_vl.bias`, no `deepseek4.vocab_size` key). Split GGUF files and llama.cpp's `clip` projector files are not read yet. +A decoder in our own ROCMFP MIX format comes from `tools/ds4_mix_converter` +run on the Vision-Exp checkpoint; it keeps the image router biases and writes +the routed experts in the MIX types (fp2 gate and up, fp3 down), dense tensors +in BF16. Pass `--imatrix` with an importance matrix (the community publishes +llama.cpp ones for this model; the converter reads one width-long vector per +expert tensor) or `--absmax-only`. The converter uses every core; the +Vision-Exp checkpoint takes about 30 minutes on 32 cores. + One image request may be outstanding per backend. Its admission lease remains with the immutable payload through queueing and generation; another image request is rejected until that payload is released. This bounds simultaneous @@ -165,6 +173,14 @@ prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented) and 43/60 220 questions. An image request prefills in about 4 s and decodes at about 23 tok/s. +With our own ROCMFP MIX conversion of the same checkpoint (importance-matrix +weighted, 114 GB with BF16 dense tensors), on a Strix Halo alone: AI2D 89/100, +ChartQA 54/60 and 44/60, 185 answers identical to the Q2_K_S run, the same +prefill time, and the sanity set (an image ahead of a 4,982-token prompt, two +images in one request) correct. Decode is slower than with the Q2_K_S file +(10 to 12 tok/s against 15 to 17) because the dense tensors are unquantized; +quantizing them as the shipped text model does is converter work still to do. + Not yet established: - The vision tower misses the fixed 0.9995 feature-cosine gate against the From 64611365b69a60230e4c154674ef981e1ff0a8c3 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 18:36:40 +0200 Subject: [PATCH 118/123] feat(tools): write the MIX file with the shipped DeepSeek-V4-Flash recipe The converter left every dense tensor in BF16 and put every down expert in fp3, which made its files 22 GB larger and slower to decode than the model we ship. It now follows the shipped recipe: dense projections and the output head in ROCmFP4, the token embedding in Q6_K, gate and up experts in fp2, and down experts in fp2 on the shipped layer set (--down-fp2-layers, default 0,2-4,6,10,11,17-20,39-42) and fp3 elsewhere. The fp2 down codebooks go to the fp2 sidecar, the fp3 table lists only the fp3 layers and is omitted when there are none. Co-Authored-By: Claude Fable 5.1 --- server/CMakeLists.txt | 3 +- .../ds4_mix_converter/ds4_mix_converter.cpp | 171 ++++++++++++++---- 2 files changed, 136 insertions(+), 38 deletions(-) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index b33578f04..8d32b7fb7 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -391,7 +391,8 @@ if(LUCE_DS4_MIX_CONVERTER AND tools/ds4_mix_converter/ds4_mix_converter.cpp) target_include_directories(ds4_mix_converter PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/include - ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/rocmfpx) + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/rocmfpx + ${CMAKE_CURRENT_SOURCE_DIR}/deps/llama.cpp/ggml/rocmfp4) target_link_libraries(ds4_mix_converter PRIVATE ggml-base nlohmann_json::nlohmann_json) if(UNIX) diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index 3e9f92ca1..a15d85952 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -1,5 +1,6 @@ #include "ggml.h" #include "gguf.h" +#include "rocmfp4.h" #include "rocmfpx.h" #include @@ -29,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -254,6 +256,7 @@ class SafeTensorSet { bool contains(const std::string & name) const { return entries_.count(name) != 0; } const std::unordered_map & entries() const { return entries_; } const json & config() const { return config_; } + const fs::path & root() const { return root_; } const json & tokenizer() const { return tokenizer_; } private: @@ -391,6 +394,37 @@ constexpr std::array kExpertRecipes{{ {"w2", "ffn_down_exps.weight", Surface::Down, GGML_TYPE_Q3_1_ROCMFP3_MIX, BookSource::DownOnly, kP4Levels}, }}; +// The shipped DeepSeek-V4-Flash recipe keeps fp2 for the down experts of a +// few layers and fp3 elsewhere. --down-fp2-layers sets the fp2 layers. +constexpr ExpertRecipe kDownFp2{"w2", "ffn_down_exps.weight", Surface::Down, GGML_TYPE_Q2_1_ROCMFP2_MIX, + BookSource::DownOnly, kGuLevels}; +constexpr const char * kDefaultDownFp2Layers = "0,2-4,6,10,11,17-20,39-42"; +std::set g_fp2_down_layers; + +const ExpertRecipe & down_recipe(int layer) { + return g_fp2_down_layers.count(layer) ? kDownFp2 : kExpertRecipes[2]; +} + +std::array layer_recipes(int layer) { + return {&kExpertRecipes[0], &kExpertRecipes[1], &down_recipe(layer)}; +} + +// "0,2-4,39-42" -> {0, 2, 3, 4, 39, 40, 41, 42} +std::set parse_layer_set(const std::string & text) { + std::set out; + std::string item; + std::stringstream in(text); + while (std::getline(in, item, ',')) { + if (item.empty()) continue; + const auto dash = item.find('-'); + const int lo = std::stoi(item.substr(0, dash)); + const int hi = dash == std::string::npos ? lo : std::stoi(item.substr(dash + 1)); + if (lo < 0 || hi < lo) fail("bad layer range: " + item); + for (int layer = lo; layer <= hi; ++layer) out.insert(layer); + } + return out; +} + std::string source_expert_name(int layer, int expert, const ExpertRecipe & recipe, const char * suffix) { return "layers." + std::to_string(layer) + ".ffn.experts." + std::to_string(expert) + "." + recipe.source_leaf + "." + suffix; @@ -624,11 +658,14 @@ struct Options { int layer_count = -1; int expert_limit = -1; int threads = 0; // 0 = every core + std::string down_fp2_layers = kDefaultDownFp2Layers; }; void usage(const char * argv0) { std::cerr << "Usage: " << argv0 << " --input DIR --output FILE (--imatrix FILE | --absmax-only)\n" - << " [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force] [--threads N]\n"; + << " [--layer-count N] [--expert-limit N] [--experts-only] [--validate-input-only] [--force] [--threads N]\n" + << " [--down-fp2-layers LIST] fp2 instead of fp3 for the down experts of these layers (default " + << kDefaultDownFp2Layers << ")\n"; } int parse_nonnegative(const char * value, const std::string & option, bool allow_zero = true) { @@ -659,6 +696,7 @@ Options parse_options(int argc, char ** argv) { else if (arg == "--layer-count") out.layer_count = parse_nonnegative(value(), arg, false); else if (arg == "--expert-limit") out.expert_limit = parse_nonnegative(value(), arg, false); else if (arg == "--threads") out.threads = parse_nonnegative(value(), arg, false); + else if (arg == "--down-fp2-layers") out.down_fp2_layers = value(); else if (arg == "--help" || arg == "-h") { usage(argv[0]); std::exit(0); } else fail("unknown option " + arg); } @@ -697,12 +735,18 @@ void append_le(std::vector & out, T value) { std::vector make_p4_blob(const std::vector & layers, uint32_t experts) { std::vector out; + std::vector fp3; + for (const LayerCalibration & layer : layers) { + if (layer.down.levels == kP4Levels) fp3.push_back(&layer); + } + if (fp3.empty()) return out; const char magic[8] = {'P','4','M','I','X','v','1','\0'}; out.insert(out.end(), magic, magic + 8); - append_le(out, static_cast(layers.size())); + append_le(out, static_cast(fp3.size())); append_le(out, 0); - for (const LayerCalibration & layer : layers) { - if (layer.down.levels != kP4Levels || layer.down.experts.size() != experts) { + for (const LayerCalibration * layer_ptr : fp3) { + const LayerCalibration & layer = *layer_ptr; + if (layer.down.experts.size() != experts) { fail("incomplete qtype-105 codebook registry at layer " + std::to_string(layer.layer)); } append_le(out, static_cast(layer.layer)); @@ -725,7 +769,9 @@ std::vector make_gumix_blob(const std::vector & layer std::vector out; const char magic[8] = {'G','U','M','I','X','s','1','\0'}; out.insert(out.end(), magic, magic + 8); - append_le(out, static_cast(layers.size()*2)); + uint32_t entries = 0; + for (const LayerCalibration & layer : layers) entries += layer.down.levels == kGuLevels ? 3 : 2; + append_le(out, entries); append_le(out, 0); for (const LayerCalibration & layer : layers) { if (layer.gate_up.levels != kGuLevels || layer.gate_up.experts.size() != experts) { @@ -745,6 +791,22 @@ std::vector make_gumix_blob(const std::vector & layer for (uint16_t value : book) append_le(out, value); } } + if (layer.down.levels != kGuLevels) continue; + if (layer.down.experts.size() != experts) { + fail("incomplete qtype-106 down codebook registry at layer " + std::to_string(layer.layer)); + } + append_le(out, static_cast(layer.layer)); + append_le(out, static_cast(Surface::Down)); + append_le(out, experts); + append_le(out, layer.down_shape.out); + append_le(out, layer.down_shape.in); + append_le(out, kCodebooks); + append_le(out, kGuLevels); + out.insert(out.end(), experts, 1u); + for (const auto & book : layer.down.experts) { + if (book.size() != kCodebooks*kGuLevels) fail("bad qtype-106 down codebook length"); + for (uint16_t value : book) append_le(out, value); + } } return out; } @@ -762,7 +824,7 @@ void write_atomic_bytes(const fs::path & path, const std::vector & byte fs::rename(temporary, path); } -enum class Producer { Raw, DenseFp8, Int64ToInt32, Expert }; +enum class Producer { Raw, Dense, Int64ToInt32, Expert }; struct TensorSpec { std::string name; @@ -871,13 +933,23 @@ TensorSpec mapped_source_spec(const std::string & target, const StEntry & source (source.shape[0] + 127)/128, (source.shape[1] + 127)/128, }; if (scale.shape != expected) fail("FP8 scale shape mismatch for " + source.name); - spec.type = GGML_TYPE_BF16; - spec.producer = Producer::DenseFp8; + spec.type = GGML_TYPE_Q4_0_ROCMFP4_FAST; + spec.producer = Producer::Dense; spec.scale = &scale; + } else if (source.dtype == "BF16" && source.shape.size() == 2 && + (target == "token_embd.weight" || target == "output.weight")) { + spec.type = target == "token_embd.weight" ? GGML_TYPE_Q6_K : GGML_TYPE_Q4_0_ROCMFP4_FAST; + spec.producer = Producer::Dense; } else { spec.type = direct_ggml_type(source.dtype); spec.producer = Producer::Raw; } + if (spec.producer == Producer::Dense) { + const int64_t block = ggml_blck_size(spec.type); + if (source.shape[1] % block != 0) { + fail("row of " + source.name + " is not a multiple of " + std::to_string(block)); + } + } spec.ne = reverse_shape(source); if (spec.name.size() >= GGML_MAX_NAME) fail("GGUF tensor name too long: " + spec.name); return spec; @@ -910,7 +982,8 @@ std::vector make_plan( source.at(src), source)); } } - for (const ExpertRecipe & recipe : kExpertRecipes) { + for (const ExpertRecipe * recipe_ptr : layer_recipes(layer.layer)) { + const ExpertRecipe & recipe = *recipe_ptr; const TensorShape shape = recipe.books == BookSource::GateUpJoint ? layer.gate_up_shape : layer.down_shape; TensorSpec spec; @@ -1087,8 +1160,10 @@ void set_model_metadata(gguf_context * ctx, const SafeTensorSet & source, for (const std::string & merge : merges) merge_ptrs.push_back(merge.c_str()); gguf_set_arr_str(ctx, "tokenizer.ggml.merges", merge_ptrs.data(), merge_ptrs.size()); - gguf_set_arr_data(ctx, "deepseek4.p4mix.sidecar", GGUF_TYPE_UINT8, - p4_blob.data(), p4_blob.size()); + if (!p4_blob.empty()) { + gguf_set_arr_data(ctx, "deepseek4.p4mix.sidecar", GGUF_TYPE_UINT8, + p4_blob.data(), p4_blob.size()); + } } std::unique_ptr make_tensor_descriptor(const TensorSpec & spec) { @@ -1141,28 +1216,47 @@ void write_int64_to_int32(FILE * out, const StEntry & source) { } } -void write_dense_fp8(FILE * out, const StEntry & weight, const StEntry & scale) { +// A dense matrix (FP8 with block scales, or BF16) quantized row by row to +// spec.type: ROCmFP4 for the projections, Q6_K for the token embedding. +void write_dense(FILE * out, const TensorSpec & spec) { + const StEntry & weight = *spec.source; const uint32_t rows = static_cast(weight.shape[0]); const uint32_t cols = static_cast(weight.shape[1]); - FileDescriptor wf(weight.path), sf(scale.path); - std::vector scales(scale.size); - pread_exact(sf.fd, scales.data(), scales.size(), scale.offset, scale.name); - std::vector input(cols); - std::vector output(cols); - const uint32_t scale_cols = static_cast(scale.shape[1]); + FileDescriptor wf(weight.path); + std::vector scales; + std::unique_ptr sf; + uint32_t scale_cols = 0; + if (spec.scale) { + sf = std::make_unique(spec.scale->path); + scales.resize(spec.scale->size); + pread_exact(sf->fd, scales.data(), scales.size(), spec.scale->offset, spec.scale->name); + scale_cols = static_cast(spec.scale->shape[1]); + } + const size_t in_bytes = spec.scale ? cols : cols * 2; + std::vector input(in_bytes); + std::vector values(cols); + const size_t row_bytes = ggml_row_size(spec.type, cols); + std::vector output(row_bytes); for (uint32_t row = 0; row < rows; ++row) { - pread_exact(wf.fd, input.data(), input.size(), weight.offset + static_cast(row)*cols, weight.name); + pread_exact(wf.fd, input.data(), input.size(), weight.offset + static_cast(row)*in_bytes, weight.name); for (uint32_t col = 0; col < cols; ++col) { - const uint8_t scale_byte = scales[(row/128)*scale_cols + col/128]; - const float decoded = fp8_e4m3fn(input[col])*fp8_e8m0(scale_byte); - const uint16_t b = float_to_bf16(decoded); - if (!std::isfinite(decoded) || bf16_to_float(b) != decoded) { - fail("FP8->BF16 is not exact for " + weight.name + " at row " + - std::to_string(row) + " col " + std::to_string(col)); + float decoded; + if (spec.scale) { + decoded = fp8_e4m3fn(input[col])*fp8_e8m0(scales[(row/128)*scale_cols + col/128]); + } else { + decoded = bf16_to_float(static_cast(input[2*col] | (input[2*col + 1] << 8))); } - output[col] = b; + if (!std::isfinite(decoded)) { + fail("non-finite weight in " + weight.name + " at row " + std::to_string(row) + " col " + std::to_string(col)); + } + values[col] = decoded; + } + if (spec.type == GGML_TYPE_Q4_0_ROCMFP4_FAST) { + rocmfp4_quantize_row_q4_0_fast_ref(values.data(), reinterpret_cast(output.data()), cols); + } else if (ggml_quantize_chunk(spec.type, values.data(), output.data(), 0, 1, cols, nullptr) != row_bytes) { + fail("quantizer wrote an unexpected row size for " + weight.name); } - fwrite_exact(out, output.data(), output.size()*sizeof(uint16_t), weight.name); + fwrite_exact(out, output.data(), output.size(), weight.name); } } @@ -1234,7 +1328,8 @@ std::vector validate_input_layout( LayerCalibration current; current.layer = static_cast(layer); for (uint32_t expert = 0; expert < experts; ++expert) { - for (const ExpertRecipe & recipe : kExpertRecipes) { + for (const ExpertRecipe * recipe_ptr : layer_recipes(static_cast(layer))) { + const ExpertRecipe & recipe = *recipe_ptr; const TensorShape shape = validate_expert_source(source, layer, expert, recipe); TensorShape & expected = recipe.books == BookSource::GateUpJoint ? current.gate_up_shape : current.down_shape; @@ -1258,7 +1353,7 @@ std::vector calibrate( LayerCalibration current; current.layer = static_cast(layer); current.gate_up.levels = kGuLevels; - current.down.levels = kP4Levels; + current.down.levels = down_recipe(static_cast(layer)).levels; struct ExpertFit { TensorShape gate_shape, down_shape; std::vector gate_up, down; @@ -1279,14 +1374,14 @@ std::vector calibrate( add_expert_to_fitter(source, layer, expert, recipe, importance, gate_up_fitter); } HistogramFitter down_fitter; - const ExpertRecipe & down_recipe = kExpertRecipes[2]; - fit.down_shape = validate_expert_source(source, layer, expert, down_recipe); + const ExpertRecipe & down = down_recipe(static_cast(layer)); + fit.down_shape = validate_expert_source(source, layer, expert, down); const auto * down_importance = require_imatrix( - imatrix, target_expert_name(layer, down_recipe), fit.down_shape.in); - add_expert_to_fitter(source, layer, expert, down_recipe, down_importance, down_fitter); + imatrix, target_expert_name(layer, down), fit.down_shape.in); + add_expert_to_fitter(source, layer, expert, down, down_importance, down_fitter); const std::string label = "layer=" + std::to_string(layer) + " expert=" + std::to_string(expert); fit.gate_up = gate_up_fitter.fit(kGuLevels, label + " gate_up", &fit.repairs); - fit.down = down_fitter.fit(kP4Levels, label + " down", &fit.repairs); + fit.down = down_fitter.fit(static_cast(down.levels), label + " down", &fit.repairs); return fit; }; const std::function keep = [&](uint32_t expert, ExpertFit & fit) { @@ -1346,7 +1441,8 @@ void verify_artifact(const fs::path & output, const fs::path & gumix_path, const uint64_t file_size = fs::file_size(output); const uint64_t data_offset = gguf_get_data_offset(ctx); const int64_t p4_key = gguf_find_key(ctx, "deepseek4.p4mix.sidecar"); - if (p4_key < 0 || gguf_get_kv_type(ctx, p4_key) != GGUF_TYPE_ARRAY || + if (expected_p4.empty() ? p4_key >= 0 : + p4_key < 0 || gguf_get_kv_type(ctx, p4_key) != GGUF_TYPE_ARRAY || gguf_get_arr_type(ctx, p4_key) != GGUF_TYPE_UINT8 || gguf_get_arr_n(ctx, p4_key) != expected_p4.size() || std::memcmp(gguf_get_arr_data(ctx, p4_key), expected_p4.data(), expected_p4.size()) != 0) { @@ -1425,8 +1521,8 @@ void write_gguf(const Options & options, const SafeTensorSet & source, const off_t before = ::ftello(out); if (spec.producer == Producer::Raw) { copy_raw(out, *spec.source); - } else if (spec.producer == Producer::DenseFp8) { - write_dense_fp8(out, *spec.source, *spec.scale); + } else if (spec.producer == Producer::Dense) { + write_dense(out, spec); } else if (spec.producer == Producer::Int64ToInt32) { write_int64_to_int32(out, *spec.source); } else { @@ -1459,6 +1555,7 @@ int main(int argc, char ** argv) { static_assert(static_cast(GGML_TYPE_Q3_1_ROCMFP3_MIX) == static_cast(kQtypeP4Mix)); static_assert(static_cast(GGML_TYPE_Q2_1_ROCMFP2_MIX) == static_cast(kQtypeGuMix)); const Options options = parse_options(argc, argv); + g_fp2_down_layers = parse_layer_set(options.down_fp2_layers); g_threads = options.threads > 0 ? static_cast(options.threads) : std::max(1u, std::thread::hardware_concurrency()); SafeTensorSet source(options.input); From 0d349b8267025b0e89fd4b8ee930a4bfe5aa2334 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 19:07:18 +0200 Subject: [PATCH 119/123] feat(tools): per-expert importance and embedded codebooks in the MIX converter The converter took one importance vector per expert tensor, so every one of the 256 experts in a layer was weighted by the average of all of them. It now also accepts llama.cpp's per-expert layout (one vector per expert, expert major) and weights each expert by its own tokens; a single shared vector still works. The fp2 codebooks are embedded in the GGUF metadata as deepseek4.gumix.sidecar, which the loader already prefers, so the output is one file like the published DeepSeek-V4-Flash builds, with no .gumix.bin beside it. The calibration note in the metadata names the imatrix file. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ds4_mix_converter/ds4_mix_converter.cpp | 82 ++++++++++--------- 1 file changed, 43 insertions(+), 39 deletions(-) diff --git a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp index a15d85952..1b15dc99c 100644 --- a/server/tools/ds4_mix_converter/ds4_mix_converter.cpp +++ b/server/tools/ds4_mix_converter/ds4_mix_converter.cpp @@ -364,16 +364,22 @@ Imatrix load_imatrix(const fs::path & path) { return result; } -const std::vector * require_imatrix( - const std::optional & imatrix, const std::string & name, size_t in) { +// Importance of each input column of one expert. An entry holds either one +// vector shared by all experts (`in` values) or one per expert (expert-major, +// as llama.cpp's imatrix collects them, at least `experts` of them); per-expert +// is better, since experts see different tokens. +const float * require_imatrix( + const std::optional & imatrix, const std::string & name, size_t in, + uint32_t expert, uint32_t experts) { if (!imatrix) return nullptr; const auto it = imatrix->find(name); if (it == imatrix->end()) fail("imatrix is missing required entry " + name); - if (it->second.values.size() != in) { - fail("imatrix entry " + name + " has " + std::to_string(it->second.values.size()) + - " values, expected " + std::to_string(in)); - } - return &it->second.values; + const std::vector & values = it->second.values; + if (values.size() == in) return values.data(); + if (values.size() % in == 0 && values.size() / in >= experts) return values.data() + static_cast(expert) * in; + fail("imatrix entry " + name + " has " + std::to_string(values.size()) + " values, expected " + + std::to_string(in) + " or one " + std::to_string(in) + "-wide vector for each of " + + std::to_string(experts) + " experts"); } enum class Surface : uint32_t { Gate = 0, Up = 1, Down = 2 }; @@ -616,7 +622,7 @@ void decode_expert_row( void add_expert_to_fitter( const SafeTensorSet & source, int layer, int expert, - const ExpertRecipe & recipe, const std::vector * importance, + const ExpertRecipe & recipe, const float * importance, HistogramFitter & fitter) { const TensorShape shape = validate_expert_source(source, layer, expert, recipe); const StEntry & w = source.at(source_expert_name(layer, expert, recipe, "weight")); @@ -628,7 +634,7 @@ void add_expert_to_fitter( decode_expert_row(input, row, shape.in, packed, scales, values); for (uint32_t col = 0; col < shape.in; col += 16) { fitter.add_half(values.data() + col, - importance ? importance->data() + col : nullptr); + importance ? importance + col : nullptr); } } } @@ -811,19 +817,6 @@ std::vector make_gumix_blob(const std::vector & layer return out; } -void write_atomic_bytes(const fs::path & path, const std::vector & bytes, bool force) { - if (!force && fs::exists(path)) fail("output exists: " + path.string()); - const fs::path temporary = path.string() + ".partial"; - if (fs::exists(temporary)) fs::remove(temporary); - std::ofstream out(temporary, std::ios::binary | std::ios::trunc); - if (!out) fail("cannot create " + temporary.string()); - out.write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); - out.close(); - if (!out) fail("failed writing " + temporary.string()); - if (force && fs::exists(path)) fs::remove(path); - fs::rename(temporary, path); -} - enum class Producer { Raw, Dense, Int64ToInt32, Expert }; struct TensorSpec { @@ -1061,14 +1054,15 @@ std::string model_name(const SafeTensorSet & source) { void set_model_metadata(gguf_context * ctx, const SafeTensorSet & source, uint32_t layers, uint32_t experts, bool absmax_only, - bool smoke_artifact, const std::vector & p4_blob) { + bool smoke_artifact, const std::vector & p4_blob, + const std::vector & gumix_blob, const std::string & calibration) { const json & c = source.config(); gguf_set_val_str(ctx, "general.architecture", "deepseek4"); gguf_set_val_str(ctx, "general.name", model_name(source).c_str()); gguf_set_val_u32(ctx, "general.alignment", kAlignment); gguf_set_val_u32(ctx, "general.file_type", 119); gguf_set_val_str(ctx, "deepseek4.mix.calibration", - absmax_only ? "absmax-only (LOWER QUALITY; no imatrix)" : "importance-matrix weighted"); + absmax_only ? "absmax-only (LOWER QUALITY; no imatrix)" : calibration.c_str()); gguf_set_val_bool(ctx, "deepseek4.mix.lower_quality_absmax_only", absmax_only); gguf_set_val_bool(ctx, "deepseek4.mix.experts_only_smoke_artifact", smoke_artifact); @@ -1164,6 +1158,8 @@ void set_model_metadata(gguf_context * ctx, const SafeTensorSet & source, gguf_set_arr_data(ctx, "deepseek4.p4mix.sidecar", GGUF_TYPE_UINT8, p4_blob.data(), p4_blob.size()); } + gguf_set_arr_data(ctx, "deepseek4.gumix.sidecar", GGUF_TYPE_UINT8, + gumix_blob.data(), gumix_blob.size()); } std::unique_ptr make_tensor_descriptor(const TensorSpec & spec) { @@ -1274,7 +1270,6 @@ void write_expert_tensor(FILE * out, const SafeTensorSet & source, const CodebookRegistry & registry = recipe.books == BookSource::GateUpJoint ? calibration.gate_up : calibration.down; const std::string target = target_expert_name(calibration.layer, recipe); - const std::vector * importance = require_imatrix(imatrix, target, expected.in); if (recipe.qtype != GGML_TYPE_Q2_1_ROCMFP2_MIX && recipe.qtype != GGML_TYPE_Q3_1_ROCMFP3_MIX) { fail("recipe table contains unsupported qtype"); } @@ -1285,6 +1280,7 @@ void write_expert_tensor(FILE * out, const SafeTensorSet & source, const StEntry & s = source.at(source_expert_name(calibration.layer, expert, recipe, "scale")); OpenTensorPair input(w, s); const auto & books = registry.experts.at(expert); + const float * importance = require_imatrix(imatrix, target, expected.in, expert, experts); std::vector packed, scales; std::vector values; std::vector q2(expected.in/kBlock); @@ -1298,13 +1294,13 @@ void write_expert_tensor(FILE * out, const SafeTensorSet & source, const uint8_t * encoded = nullptr; if (recipe.qtype == GGML_TYPE_Q2_1_ROCMFP2_MIX) { if (!rocmfpx_quantize_row_fp2_mix_ref(values.data(), q2.data(), shape.in, - books.data(), importance ? importance->data() : nullptr)) { + books.data(), importance)) { fail("qtype-106 reference encoder rejected " + w.name); } encoded = reinterpret_cast(q2.data()); } else { if (!rocmfpx_quantize_row_fp3_mix_ref(values.data(), q3.data(), shape.in, - books.data(), importance ? importance->data() : nullptr)) { + books.data(), importance)) { fail("qtype-105 reference encoder rejected " + w.name); } encoded = reinterpret_cast(q3.data()); @@ -1370,14 +1366,14 @@ std::vector calibrate( fail("qtype-106 gate/up shape mismatch at layer " + std::to_string(layer)); } const std::string target = target_expert_name(layer, recipe); - const auto * importance = require_imatrix(imatrix, target, shape.in); + const float * importance = require_imatrix(imatrix, target, shape.in, expert, experts); add_expert_to_fitter(source, layer, expert, recipe, importance, gate_up_fitter); } HistogramFitter down_fitter; const ExpertRecipe & down = down_recipe(static_cast(layer)); fit.down_shape = validate_expert_source(source, layer, expert, down); - const auto * down_importance = require_imatrix( - imatrix, target_expert_name(layer, down), fit.down_shape.in); + const float * down_importance = require_imatrix( + imatrix, target_expert_name(layer, down), fit.down_shape.in, expert, experts); add_expert_to_fitter(source, layer, expert, down, down_importance, down_fitter); const std::string label = "layer=" + std::to_string(layer) + " expert=" + std::to_string(expert); fit.gate_up = gate_up_fitter.fit(kGuLevels, label + " gate_up", &fit.repairs); @@ -1428,7 +1424,7 @@ std::vector read_file(const fs::path & path) { return out; } -void verify_artifact(const fs::path & output, const fs::path & gumix_path, +void verify_artifact(const fs::path & output, const std::vector & plan, const std::vector & expected_p4, const std::vector & expected_gumix) { @@ -1448,7 +1444,13 @@ void verify_artifact(const fs::path & output, const fs::path & gumix_path, std::memcmp(gguf_get_arr_data(ctx, p4_key), expected_p4.data(), expected_p4.size()) != 0) { fail("embedded deepseek4.p4mix.sidecar verification failed"); } - if (read_file(gumix_path) != expected_gumix) fail("qtype-106 gumix sidecar verification failed"); + const int64_t gu_key = gguf_find_key(ctx, "deepseek4.gumix.sidecar"); + if (gu_key < 0 || gguf_get_kv_type(ctx, gu_key) != GGUF_TYPE_ARRAY || + gguf_get_arr_type(ctx, gu_key) != GGUF_TYPE_UINT8 || + gguf_get_arr_n(ctx, gu_key) != expected_gumix.size() || + std::memcmp(gguf_get_arr_data(ctx, gu_key), expected_gumix.data(), expected_gumix.size()) != 0) { + fail("embedded deepseek4.gumix.sidecar verification failed"); + } FileDescriptor output_fd(output); for (const TensorSpec & spec : plan) { @@ -1469,10 +1471,11 @@ void verify_artifact(const fs::path & output, const fs::path & gumix_path, void write_gguf(const Options & options, const SafeTensorSet & source, const std::vector & calibration, uint32_t layers, uint32_t experts, const std::optional & imatrix) { - const fs::path gumix_path = options.output.string() + ".gumix.bin"; - if (!options.force && (fs::exists(options.output) || fs::exists(gumix_path))) { - fail("output or qtype-106 sidecar exists: " + options.output.string()); - } + if (!options.force && fs::exists(options.output)) fail("output exists: " + options.output.string()); + // A loose sidecar beside the file would be read instead of nothing, but + // the embedded copy is preferred; remove a stale one so there is one source. + const fs::path stale_gumix = options.output.string() + ".gumix.bin"; + if (fs::exists(stale_gumix)) fs::remove(stale_gumix); if (!options.output.parent_path().empty()) fs::create_directories(options.output.parent_path()); const fs::path temporary = options.output.string() + ".partial"; if (fs::exists(temporary)) fs::remove(temporary); @@ -1483,8 +1486,10 @@ void write_gguf(const Options & options, const SafeTensorSet & source, gguf_context * ctx = gguf_init_empty(); if (!ctx) fail("gguf_init_empty failed"); + const std::string calibration_note = options.imatrix + ? "importance-matrix weighted: " + options.imatrix->filename().string() : std::string(); set_model_metadata(ctx, source, layers, experts, options.absmax_only, - options.experts_only, p4); + options.experts_only, p4, gumix, calibration_note); std::vector repair_stamps; for (const auto & layer : calibration) for (const auto & stamp : layer.repairs) repair_stamps.push_back(stamp.c_str()); @@ -1542,10 +1547,9 @@ void write_gguf(const Options & options, const SafeTensorSet & source, } gguf_free(ctx); - write_atomic_bytes(gumix_path, gumix, options.force); if (options.force && fs::exists(options.output)) fs::remove(options.output); fs::rename(temporary, options.output); - verify_artifact(options.output, gumix_path, plan, p4, gumix); + verify_artifact(options.output, plan, p4, gumix); } } // namespace From a098ecd370fabed5a44e14be00b5f5ad9cf363b3 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Tue, 22 Sep 2026 20:37:56 +0200 Subject: [PATCH 120/123] docs(vision): DS4V MIX results with the shipped recipe Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/image-input.md | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/docs/image-input.md b/docs/image-input.md index 55eb33f42..6f61fcaaf 100644 --- a/docs/image-input.md +++ b/docs/image-input.md @@ -124,12 +124,14 @@ bias named `blk.N.exp_probs_b_vl.bias`, no `deepseek4.vocab_size` key). Split GGUF files and llama.cpp's `clip` projector files are not read yet. A decoder in our own ROCMFP MIX format comes from `tools/ds4_mix_converter` -run on the Vision-Exp checkpoint; it keeps the image router biases and writes -the routed experts in the MIX types (fp2 gate and up, fp3 down), dense tensors -in BF16. Pass `--imatrix` with an importance matrix (the community publishes -llama.cpp ones for this model; the converter reads one width-long vector per -expert tensor) or `--absmax-only`. The converter uses every core; the -Vision-Exp checkpoint takes about 30 minutes on 32 cores. +run on the Vision-Exp checkpoint. It follows the shipped DeepSeek-V4-Flash +recipe: routed gate and up experts in fp2, down experts in fp2 on the shipped +layer set and fp3 elsewhere, dense projections in ROCmFP4, the token embedding +in Q6_K, codebooks embedded in the GGUF (one file, about 100 GB). It keeps the +image router biases. Pass `--imatrix` with an importance matrix (llama.cpp's +per-expert layout is used expert by expert; the community publishes one for +this model) or `--absmax-only`. The converter uses every core: about 40 minutes +for this checkpoint on 32 cores. One image request may be outstanding per backend. Its admission lease remains with the immutable payload through queueing and generation; another image @@ -173,13 +175,18 @@ prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented) and 43/60 220 questions. An image request prefills in about 4 s and decodes at about 23 tok/s. -With our own ROCMFP MIX conversion of the same checkpoint (importance-matrix -weighted, 114 GB with BF16 dense tensors), on a Strix Halo alone: AI2D 89/100, -ChartQA 54/60 and 44/60, 185 answers identical to the Q2_K_S run, the same -prefill time, and the sanity set (an image ahead of a 4,982-token prompt, two -images in one request) correct. Decode is slower than with the Q2_K_S file -(10 to 12 tok/s against 15 to 17) because the dense tensors are unquantized; -quantizing them as the shipped text model does is converter work still to do. +With our own ROCMFP MIX conversion of the same checkpoint (per-expert +importance matrix, the shipped recipe above), on a Strix Halo alone at top-k 6: + +- Against the MXFP4 reference (native FP4 experts) on 8,176 wikitext-2 tokens: + KL 0.464 mean, 0.102 median, top-1 agreement 78.4%, perplexity 4.14 against + 2.82. The community Q2_K_S scores KL 0.511 in our engine (0.523 in + llama.cpp) and perplexity 4.22. +- AI2D 84/100, ChartQA 54/60 and 40/60 (the Q2_K_S: 85, 55, 43); the sanity + and one-to-four-image sets are all correct. +- With the published DSpark drafter and fused decode and verify, text decodes + at 25 to 37 tok/s on 256-token answers (30 mean), as fast as the shipped + text model; image requests decode without the drafter at about 22 tok/s. Not yet established: From 25735a3fc910658075e06c94cc8dc20541cfeace Mon Sep 17 00:00:00 2001 From: mrciffa Date: Wed, 23 Sep 2026 00:23:29 +0200 Subject: [PATCH 121/123] docs(vision): Qwen3.8 results with the Lucebox model file and a Q8_0 projector Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/image-input.md | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/image-input.md b/docs/image-input.md index 6f61fcaaf..9eb31d3c5 100644 --- a/docs/image-input.md +++ b/docs/image-input.md @@ -50,12 +50,13 @@ support images. `/props` reports the effective capability in ## Qwen3.5 / Qwen3.8 ``` -luce_server Qwen3.8-27B-UD-IQ4_XS.gguf --target-device hip:0 \ - --draft qwen38-dflash2-q8_0.gguf --draft-device hip:0 \ - --mmproj mmproj-Qwen3.8-27B-BF16.gguf +luce_server Qwen3.8-27B-IQ4_XS-pure.gguf --target-device hip:0 \ + --draft Qwen3.8-27B-DFlash2-Q8_0.gguf --draft-device hip:0 \ + --mmproj Qwen3.8-27B-mmproj-Q8_0.gguf ``` -The projector is read directly from the published file. Projectors with +The projector is read directly from the published `clip` file, BF16, F16 or +Q8_0; Q8_0 is recommended (below). Projectors with deepstack branches (Qwen3-VL) are refused. An image is resized the way the model was trained (bicubic, both sides to a multiple of 32 pixels) and costs one token per 32x32 pixels, between 64 and 1,024 tokens; larger images are @@ -71,8 +72,17 @@ Covered by `test_qwen35_image`: target sizes against the model's reference resize rule, the tower's patch order and position table sampling, marker expansion, rotary positions, and image rows that straddle prefill chunks. -Measured on an R9700 alone with Qwen3.8-27B UD-IQ4_XS, the DFlash2 drafter and -the published BF16 projector, thinking off: +Measured on an R9700 alone with the DFlash2 drafter, thinking off. With the +Lucebox `Qwen3.8-27B-IQ4_XS-pure` file and a Q8_0 projector: + +- 220 seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with + lmms-eval prompts: AI2D 90/100, ChartQA relaxed accuracy 56/60 (augmented) + and 42/60 (human). Image prompts prefill in 0.56 s on average; one to four + images per request all answer correctly (four images, 2,495 tokens: 3.2 s). +- Text decodes at 56 to 117 tok/s on 256-token answers (84 on average); image + requests decode without the drafter at about 36 tok/s. + +With unsloth's UD-IQ4_XS file and the published BF16 projector: - 220 seeded questions from `lmms-lab/ai2d` and `lmms-lab/ChartQA` with lmms-eval prompts: AI2D 85/100, ChartQA relaxed accuracy 55/60 (augmented) @@ -91,7 +101,6 @@ the published BF16 projector, thinking off: same speed, with or without a projector loaded (five prompts up to 19.6K tokens). The projector adds 0.9 GiB of VRAM; the peak during image requests was 21.6 GiB against 20.8 GiB for text. - - The same requests answer correctly on a Strix Halo alone, where a 1,012-token image prompt prefills in 4.6 s and decodes at 14 tok/s. From b35966e0c6dec55bd5dbd833b2022d722faf1bc5 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Wed, 23 Sep 2026 00:33:09 +0200 Subject: [PATCH 122/123] build(vision): fetch lodepng at a pinned commit instead of vendoring it Downloads lodepng.cpp and lodepng.h from the pinned commit's raw files, checks each against its SHA256, retries three times, and re-fetches a cached file whose hash is wrong. Drops 9,464 vendored lines from the tree; the license text stays in ImageCodecs.NOTICES.md. Co-Authored-By: Claude Opus 5.5 (1M context) --- server/cmake/ImageCodecs.cmake | 38 +- server/deps/lodepng/LICENSE | 21 - server/deps/lodepng/VENDOR.md | 11 - server/deps/lodepng/lodepng.cpp | 7244 ------------------------------- server/deps/lodepng/lodepng.h | 2188 ---------- 5 files changed, 34 insertions(+), 9468 deletions(-) delete mode 100644 server/deps/lodepng/LICENSE delete mode 100644 server/deps/lodepng/VENDOR.md delete mode 100644 server/deps/lodepng/lodepng.cpp delete mode 100644 server/deps/lodepng/lodepng.h diff --git a/server/cmake/ImageCodecs.cmake b/server/cmake/ImageCodecs.cmake index 8cab30b64..c44aa143b 100644 --- a/server/cmake/ImageCodecs.cmake +++ b/server/cmake/ImageCodecs.cmake @@ -1,6 +1,6 @@ # JPEG and PNG decoders behind common/vision/image_decode: libjpeg-turbo from -# its pinned release archive, lodepng vendored. License texts are in -# ImageCodecs.NOTICES.md, deps/lodepng/LICENSE and the libjpeg-turbo archive. +# its pinned release archive, lodepng from two files at a pinned commit. License +# texts are in ImageCodecs.NOTICES.md and the libjpeg-turbo archive. include_guard(GLOBAL) include(ExternalProject) @@ -42,8 +42,38 @@ set_target_properties(image_codec_jpeg PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${IMAGE_CODEC_JPEG_PREFIX}/include) add_dependencies(image_codec_jpeg libjpeg_turbo_external) -# lodepng is vendored (server/deps/lodepng): two source files, no release archives upstream. -set(IMAGE_CODEC_PNG_DIR "${CMAKE_CURRENT_LIST_DIR}/../deps/lodepng") +# lodepng has no release archives and GitHub's commit archives are not +# byte-stable, so fetch its two files at a pinned commit (raw files are) and +# check each against its hash. Retried because CI runners drop downloads. +set(IMAGE_CODEC_PNG_COMMIT ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a) +set(IMAGE_CODEC_PNG_DIR ${CMAKE_CURRENT_BINARY_DIR}/lodepng-${IMAGE_CODEC_PNG_COMMIT}) +foreach(entry + "lodepng.cpp=d98e1f40d303c1038a096ebf93b413a565a91cf2c72b9d2fa5c625c4279c3cb6" + "lodepng.h=23c27abb06883ed98184d16d0b20771b526dca1e8e13236c2397316769c0dc8b") + string(REPLACE "=" ";" entry "${entry}") + list(GET entry 0 file) + list(GET entry 1 hash) + set(target ${IMAGE_CODEC_PNG_DIR}/${file}) + set(have "") + foreach(attempt RANGE 1 3) + if(EXISTS ${target}) + file(SHA256 ${target} have) + if(have STREQUAL hash) + break() + endif() + file(REMOVE ${target}) + endif() + file(DOWNLOAD + https://raw.githubusercontent.com/lvandeve/lodepng/${IMAGE_CODEC_PNG_COMMIT}/${file} + ${target} TLS_VERIFY ON STATUS status) + endforeach() + if(EXISTS ${target}) + file(SHA256 ${target} have) + endif() + if(NOT have STREQUAL hash) + message(FATAL_ERROR "lodepng: could not fetch ${file} with SHA256 ${hash} (${status})") + endif() +endforeach() add_library(image_codec_png STATIC ${IMAGE_CODEC_PNG_DIR}/lodepng.cpp) target_include_directories(image_codec_png PUBLIC ${IMAGE_CODEC_PNG_DIR}) diff --git a/server/deps/lodepng/LICENSE b/server/deps/lodepng/LICENSE deleted file mode 100644 index a5fb0603d..000000000 --- a/server/deps/lodepng/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Copyright (c) 2005-2018 Lode Vandevenne - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source - distribution. - diff --git a/server/deps/lodepng/VENDOR.md b/server/deps/lodepng/VENDOR.md deleted file mode 100644 index 46773ca2d..000000000 --- a/server/deps/lodepng/VENDOR.md +++ /dev/null @@ -1,11 +0,0 @@ -# Vendored lodepng - -PNG decoder used by `server/src/common/vision/image_decode.cpp`. - -- Source: https://github.com/lvandeve/lodepng -- Commit: `ed6fe5825c6a4fbb7f58ab35a4231c7543cd452a` -- Archive SHA256: `c2459a3f9145258f901d262576f7a56ca08087d3b3efeee3ae033c0952120803` -- Files: `lodepng.cpp`, `lodepng.h`, `LICENSE` (zlib), unmodified. - -Vendored rather than downloaded: the project has no release archives, and -GitHub's on-the-fly commit archives fail often enough to break CI. diff --git a/server/deps/lodepng/lodepng.cpp b/server/deps/lodepng/lodepng.cpp deleted file mode 100644 index 1a9e3e27c..000000000 --- a/server/deps/lodepng/lodepng.cpp +++ /dev/null @@ -1,7244 +0,0 @@ -/* -LodePNG version 20260119 - -Copyright (c) 2005-2026 Lode Vandevenne - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source - distribution. -*/ - -/* -The manual and changelog are in the header file "lodepng.h" -Rename this file to lodepng.cpp to use it for C++, or to lodepng.c to use it for C. -*/ - -#include "lodepng.h" - -#ifdef LODEPNG_COMPILE_DISK -#include /* LONG_MAX */ -#include /* file handling */ -#endif /* LODEPNG_COMPILE_DISK */ - -#ifdef LODEPNG_COMPILE_ALLOCATORS -#include /* allocations */ -#endif /* LODEPNG_COMPILE_ALLOCATORS */ - -#if defined(_MSC_VER) && (_MSC_VER >= 1310) /*Visual Studio: A few warning types are not desired here.*/ -#pragma warning( disable : 4244 ) /*implicit conversions: not warned by gcc -Wall -Wextra and requires too much casts*/ -#pragma warning( disable : 4996 ) /*VS does not like fopen, but fopen_s is not standard C so unusable here*/ -#endif /*_MSC_VER */ - -const char* LODEPNG_VERSION_STRING = "20260119"; - -/* -This source file is divided into the following large parts. The code sections -with the "LODEPNG_COMPILE_" #defines divide this up further in an intermixed way. --Tools for C and common code for PNG and Zlib --C Code for Zlib (huffman, deflate, ...) --C Code for PNG (file format chunks, adam7, PNG filters, color conversions, ...) --The C++ wrapper around all of the above -*/ - -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* // Tools for C, and common code for PNG and Zlib. // */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ - -/*The malloc, realloc and free functions defined here with "lodepng_" in front -of the name, so that you can easily change them to others related to your -platform if needed. Everything else in the code calls these. Pass --DLODEPNG_NO_COMPILE_ALLOCATORS to the compiler, or comment out -#define LODEPNG_COMPILE_ALLOCATORS in the header, to disable the ones here and -define them in your own project's source files without needing to change -lodepng source code. Don't forget to remove "static" if you copypaste them -from here.*/ - -#ifdef LODEPNG_COMPILE_ALLOCATORS -static void* lodepng_malloc(size_t size) { -#ifdef LODEPNG_MAX_ALLOC - if(size > LODEPNG_MAX_ALLOC) return 0; -#endif - return malloc(size); -} - -/* NOTE: when realloc returns NULL, it leaves the original memory untouched */ -static void* lodepng_realloc(void* ptr, size_t new_size) { -#ifdef LODEPNG_MAX_ALLOC - if(new_size > LODEPNG_MAX_ALLOC) return 0; -#endif - return realloc(ptr, new_size); -} - -static void lodepng_free(void* ptr) { - free(ptr); -} -#else /*LODEPNG_COMPILE_ALLOCATORS*/ -/* TODO: support giving additional void* payload to the custom allocators */ -void* lodepng_malloc(size_t size); -void* lodepng_realloc(void* ptr, size_t new_size); -void lodepng_free(void* ptr); -#endif /*LODEPNG_COMPILE_ALLOCATORS*/ - -/* convince the compiler to inline a function, for use when this measurably improves performance */ -/* inline is not available in C90, but use it when supported by the compiler */ -#if (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || (defined(__cplusplus) && (__cplusplus >= 199711L)) -#define LODEPNG_INLINE inline -#else -#define LODEPNG_INLINE /* not available */ -#endif - -/* restrict is not available in C90, but use it when supported by the compiler */ -#if (defined(__GNUC__) && (__GNUC__ > 3 || (__GNUC__ == 3 && __GNUC_MINOR__ >= 1))) ||\ - (defined(_MSC_VER) && (_MSC_VER >= 1400)) || \ - (defined(__WATCOMC__) && (__WATCOMC__ >= 1250) && !defined(__cplusplus)) -#define LODEPNG_RESTRICT __restrict -#else -#define LODEPNG_RESTRICT /* not available */ -#endif - -/* Replacements for C library functions such as memcpy and strlen, to support platforms -where a full C library is not available. The compiler can recognize them and compile -to something as fast. */ - -static void lodepng_memcpy(void* LODEPNG_RESTRICT dst, - const void* LODEPNG_RESTRICT src, size_t size) { - size_t i; - for(i = 0; i < size; i++) ((char*)dst)[i] = ((const char*)src)[i]; -} - -static void lodepng_memset(void* LODEPNG_RESTRICT dst, - int value, size_t num) { - size_t i; - for(i = 0; i < num; i++) ((char*)dst)[i] = (char)value; -} - -/* does not check memory out of bounds, do not use on untrusted data */ -static size_t lodepng_strlen(const char* a) { - const char* orig = a; - /* avoid warning about unused function in case of disabled COMPILE... macros */ - (void)(&lodepng_strlen); - while(*a) a++; - return (size_t)(a - orig); -} - -#define LODEPNG_MAX(a, b) (((a) > (b)) ? (a) : (b)) -#define LODEPNG_MIN(a, b) (((a) < (b)) ? (a) : (b)) - -#if defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_DECODER) -/* Safely check if adding two integers will overflow (no undefined -behavior, compiler removing the code, etc...) and output result. */ -static int lodepng_addofl(size_t a, size_t b, size_t* result) { - *result = a + b; /* Unsigned addition is well defined and safe in C90 */ - return *result < a; -} -#endif /*defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_DECODER)*/ - -#ifdef LODEPNG_COMPILE_DECODER -/* Safely check if multiplying two integers will overflow (no undefined -behavior, compiler removing the code, etc...) and output result. */ -static int lodepng_mulofl(size_t a, size_t b, size_t* result) { - *result = a * b; /* Unsigned multiplication is well defined and safe in C90 */ - return (a != 0 && *result / a != b); -} - -#ifdef LODEPNG_COMPILE_ZLIB -/* Safely check if a + b > c, even if overflow could happen. */ -static int lodepng_gtofl(size_t a, size_t b, size_t c) { - size_t d; - if(lodepng_addofl(a, b, &d)) return 1; - return d > c; -} -#endif /*LODEPNG_COMPILE_ZLIB*/ -#endif /*LODEPNG_COMPILE_DECODER*/ - - -/* -Often in case of an error a value is assigned to a variable and then it breaks -out of a loop (to go to the cleanup phase of a function). This macro does that. -It makes the error handling code shorter and more readable. - -Example: if(!uivector_resize(&lz77_encoded, datasize)) ERROR_BREAK(83); -*/ -#define CERROR_BREAK(errorvar, code){\ - errorvar = code;\ - break;\ -} - -/*version of CERROR_BREAK that assumes the common case where the error variable is named "error"*/ -#define ERROR_BREAK(code) CERROR_BREAK(error, code) - -/*Set error var to the error code, and return it.*/ -#define CERROR_RETURN_ERROR(errorvar, code){\ - errorvar = code;\ - return code;\ -} - -/*Try the code, if it returns error, also return the error.*/ -#define CERROR_TRY_RETURN(call){\ - unsigned error_ = call;\ - if(error_) return error_;\ -} - -/*Set error var to the error code, and return from the void function.*/ -#define CERROR_RETURN(errorvar, code){\ - errorvar = code;\ - return;\ -} - -/* -About uivector, ucvector and string: --All of them wrap dynamic arrays or text strings in a similar way. --LodePNG was originally written in C++. The vectors replace the std::vectors that were used in the C++ version. --The string tools are made to avoid problems with compilers that declare things like strncat as deprecated. --They're not used in the interface, only internally in this file as static functions. --As with many other structs in this file, the init and cleanup functions serve as ctor and dtor. -*/ - -#ifdef LODEPNG_COMPILE_ZLIB -#ifdef LODEPNG_COMPILE_ENCODER -/*dynamic vector of unsigned ints*/ -typedef struct uivector { - unsigned* data; - size_t size; /*size in number of unsigned longs*/ - size_t allocsize; /*allocated size in bytes*/ -} uivector; - -static void uivector_cleanup(void* p) { - ((uivector*)p)->size = ((uivector*)p)->allocsize = 0; - lodepng_free(((uivector*)p)->data); - ((uivector*)p)->data = NULL; -} - -/*returns 1 if success, 0 if failure ==> nothing done*/ -static unsigned uivector_resize(uivector* p, size_t size) { - size_t allocsize = size * sizeof(unsigned); - if(allocsize > p->allocsize) { - size_t newsize = allocsize + (p->allocsize >> 1u); - void* data = lodepng_realloc(p->data, newsize); - if(data) { - p->allocsize = newsize; - p->data = (unsigned*)data; - } - else return 0; /*error: not enough memory*/ - } - p->size = size; - return 1; /*success*/ -} - -static void uivector_init(uivector* p) { - p->data = NULL; - p->size = p->allocsize = 0; -} - -/*returns 1 if success, 0 if failure ==> nothing done*/ -static unsigned uivector_push_back(uivector* p, unsigned c) { - if(!uivector_resize(p, p->size + 1)) return 0; - p->data[p->size - 1] = c; - return 1; -} -#endif /*LODEPNG_COMPILE_ENCODER*/ -#endif /*LODEPNG_COMPILE_ZLIB*/ - -/* /////////////////////////////////////////////////////////////////////////// */ - -/*dynamic vector of unsigned chars*/ -typedef struct ucvector { - unsigned char* data; - size_t size; /*used size*/ - size_t allocsize; /*allocated size*/ -} ucvector; - -/*returns 1 if success, 0 if failure ==> nothing done*/ -static unsigned ucvector_reserve(ucvector* p, size_t size) { - if(size > p->allocsize) { - size_t newsize = size + (p->allocsize >> 1u); - void* data = lodepng_realloc(p->data, newsize); - if(data) { - p->allocsize = newsize; - p->data = (unsigned char*)data; - } - else return 0; /*error: not enough memory*/ - } - return 1; /*success*/ -} - -/*returns 1 if success, 0 if failure ==> nothing done*/ -static unsigned ucvector_resize(ucvector* p, size_t size) { - p->size = size; - return ucvector_reserve(p, size); -} - -static ucvector ucvector_init(unsigned char* buffer, size_t size) { - ucvector v; - v.data = buffer; - v.allocsize = v.size = size; - return v; -} - -/* ////////////////////////////////////////////////////////////////////////// */ - -#ifdef LODEPNG_COMPILE_PNG -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - -/*also appends null termination character*/ -static char* alloc_string_sized(const char* in, size_t insize) { - char* out = (char*)lodepng_malloc(insize + 1); - if(out) { - lodepng_memcpy(out, in, insize); - out[insize] = 0; - } - return out; -} - -/* dynamically allocates a new string with a copy of the null terminated input text */ -static char* alloc_string(const char* in) { - return alloc_string_sized(in, lodepng_strlen(in)); -} -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ -#endif /*LODEPNG_COMPILE_PNG*/ - -/* ////////////////////////////////////////////////////////////////////////// */ - -#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_PNG) -static unsigned lodepng_read32bitInt(const unsigned char* buffer) { - return (((unsigned)buffer[0] << 24u) | ((unsigned)buffer[1] << 16u) | - ((unsigned)buffer[2] << 8u) | (unsigned)buffer[3]); -} -#endif /*defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_PNG)*/ - -#if defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_ENCODER) -/*buffer must have at least 4 allocated bytes available*/ -static void lodepng_set32bitInt(unsigned char* buffer, unsigned value) { - buffer[0] = (unsigned char)((value >> 24) & 0xff); - buffer[1] = (unsigned char)((value >> 16) & 0xff); - buffer[2] = (unsigned char)((value >> 8) & 0xff); - buffer[3] = (unsigned char)((value ) & 0xff); -} -#endif /*defined(LODEPNG_COMPILE_PNG) || defined(LODEPNG_COMPILE_ENCODER)*/ - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / File IO / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -#ifdef LODEPNG_COMPILE_DISK - -/* returns negative value on error. This should be pure C compatible, so no fstat. */ -static long lodepng_filesize(FILE* file) { - long size; - if(fseek(file, 0, SEEK_END) != 0) return -1; - size = ftell(file); - /* It may give LONG_MAX as directory size, this is invalid for us. */ - if(size == LONG_MAX) return -1; - if(fseek(file, 0, SEEK_SET) != 0) return -1; - return size; -} - -/* Allocates the output buffer to the file size and reads the file into it. Returns error code.*/ -static unsigned lodepng_load_file_(unsigned char** out, size_t* outsize, FILE* file) { - long size = lodepng_filesize(file); - if(size < 0) return 78; - *outsize = (size_t)size; - *out = (unsigned char*)lodepng_malloc((size_t)size); - if(!(*out) && size > 0) return 83; /*the above malloc failed*/ - if(fread(*out, 1, *outsize, file) != *outsize) return 78; - return 0; /*ok*/ -} - -unsigned lodepng_load_file(unsigned char** out, size_t* outsize, const char* filename) { - unsigned error; - FILE* file = fopen(filename, "rb"); - if(!file) return 78; - error = lodepng_load_file_(out, outsize, file); - fclose(file); - return error; -} - -/*write given buffer to the file, overwriting the file, it doesn't append to it.*/ -unsigned lodepng_save_file(const unsigned char* buffer, size_t buffersize, const char* filename) { - FILE* file = fopen(filename, "wb" ); - if(!file) return 79; - fwrite(buffer, 1, buffersize, file); - fclose(file); - return 0; -} - -#endif /*LODEPNG_COMPILE_DISK*/ - -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* // End of common code and tools. Begin of Zlib related code. // */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ - -#ifdef LODEPNG_COMPILE_ZLIB -#ifdef LODEPNG_COMPILE_ENCODER - -typedef struct { - ucvector* data; - unsigned char bp; /*ok to overflow, indicates bit pos inside byte*/ -} LodePNGBitWriter; - -static void LodePNGBitWriter_init(LodePNGBitWriter* writer, ucvector* data) { - writer->data = data; - writer->bp = 0; -} - -/*TODO: this ignores potential out of memory errors*/ -#define WRITEBIT(writer, bit){\ - /* append new byte */\ - if(((writer->bp) & 7u) == 0) {\ - if(!ucvector_resize(writer->data, writer->data->size + 1)) return;\ - writer->data->data[writer->data->size - 1] = 0;\ - }\ - (writer->data->data[writer->data->size - 1]) |= (bit << ((writer->bp) & 7u));\ - ++writer->bp;\ -} - -/* LSB of value is written first, and LSB of bytes is used first */ -static void writeBits(LodePNGBitWriter* writer, unsigned value, size_t nbits) { - if(nbits == 1) { /* compiler should statically compile this case if nbits == 1 */ - WRITEBIT(writer, value); - } else { - /* TODO: increase output size only once here rather than in each WRITEBIT */ - size_t i; - for(i = 0; i != nbits; ++i) { - WRITEBIT(writer, (unsigned char)((value >> i) & 1)); - } - } -} - -/* This one is to use for adding huffman symbol, the value bits are written MSB first */ -static void writeBitsReversed(LodePNGBitWriter* writer, unsigned value, size_t nbits) { - size_t i; - for(i = 0; i != nbits; ++i) { - /* TODO: increase output size only once here rather than in each WRITEBIT */ - WRITEBIT(writer, (unsigned char)((value >> (nbits - 1u - i)) & 1u)); - } -} -#endif /*LODEPNG_COMPILE_ENCODER*/ - -#ifdef LODEPNG_COMPILE_DECODER - -typedef struct { - const unsigned char* data; - size_t size; /*size of data in bytes*/ - size_t bitsize; /*size of data in bits, end of valid bp values, should be 8*size*/ - size_t bp; - unsigned buffer; /*buffer for reading bits. NOTE: 'unsigned' must support at least 32 bits*/ -} LodePNGBitReader; - -/* data size argument is in bytes. Returns error if size too large causing overflow */ -static unsigned LodePNGBitReader_init(LodePNGBitReader* reader, const unsigned char* data, size_t size) { - size_t temp; - reader->data = data; - reader->size = size; - /* size in bits, return error if overflow (if size_t is 32 bit this supports up to 500MB) */ - if(lodepng_mulofl(size, 8u, &reader->bitsize)) return 105; - /*ensure incremented bp can be compared to bitsize without overflow even when it would be incremented 32 too much and - trying to ensure 32 more bits*/ - if(lodepng_addofl(reader->bitsize, 64u, &temp)) return 105; - reader->bp = 0; - reader->buffer = 0; - return 0; /*ok*/ -} - -/* -ensureBits functions: -Ensures the reader can at least read nbits bits in one or more readBits calls, -safely even if not enough bits are available. -The nbits parameter is unused but is given for documentation purposes, error -checking for amount of bits must be done beforehand. -*/ - -/*See ensureBits documentation above. This one ensures up to 9 bits */ -static LODEPNG_INLINE void ensureBits9(LodePNGBitReader* reader, size_t nbits) { - size_t start = reader->bp >> 3u; - size_t size = reader->size; - if(start + 1u < size) { - reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u); - reader->buffer >>= (reader->bp & 7u); - } else { - reader->buffer = 0; - if(start + 0u < size) reader->buffer = reader->data[start + 0]; - reader->buffer >>= (reader->bp & 7u); - } - (void)nbits; -} - -/*See ensureBits documentation above. This one ensures up to 17 bits */ -static LODEPNG_INLINE void ensureBits17(LodePNGBitReader* reader, size_t nbits) { - size_t start = reader->bp >> 3u; - size_t size = reader->size; - if(start + 2u < size) { - reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u) | - ((unsigned)reader->data[start + 2] << 16u); - reader->buffer >>= (reader->bp & 7u); - } else { - reader->buffer = 0; - if(start + 0u < size) reader->buffer |= reader->data[start + 0]; - if(start + 1u < size) reader->buffer |= ((unsigned)reader->data[start + 1] << 8u); - reader->buffer >>= (reader->bp & 7u); - } - (void)nbits; -} - -/*See ensureBits documentation above. This one ensures up to 25 bits */ -static LODEPNG_INLINE void ensureBits25(LodePNGBitReader* reader, size_t nbits) { - size_t start = reader->bp >> 3u; - size_t size = reader->size; - if(start + 3u < size) { - reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u) | - ((unsigned)reader->data[start + 2] << 16u) | ((unsigned)reader->data[start + 3] << 24u); - reader->buffer >>= (reader->bp & 7u); - } else { - reader->buffer = 0; - if(start + 0u < size) reader->buffer |= reader->data[start + 0]; - if(start + 1u < size) reader->buffer |= ((unsigned)reader->data[start + 1] << 8u); - if(start + 2u < size) reader->buffer |= ((unsigned)reader->data[start + 2] << 16u); - reader->buffer >>= (reader->bp & 7u); - } - (void)nbits; -} - -/*See ensureBits documentation above. This one ensures up to 32 bits */ -static LODEPNG_INLINE void ensureBits32(LodePNGBitReader* reader, size_t nbits) { - size_t start = reader->bp >> 3u; - size_t size = reader->size; - if(start + 4u < size) { - reader->buffer = (unsigned)reader->data[start + 0] | ((unsigned)reader->data[start + 1] << 8u) | - ((unsigned)reader->data[start + 2] << 16u) | ((unsigned)reader->data[start + 3] << 24u); - reader->buffer >>= (reader->bp & 7u); - reader->buffer |= (((unsigned)reader->data[start + 4] << 24u) << (8u - (reader->bp & 7u))); - } else { - reader->buffer = 0; - if(start + 0u < size) reader->buffer |= reader->data[start + 0]; - if(start + 1u < size) reader->buffer |= ((unsigned)reader->data[start + 1] << 8u); - if(start + 2u < size) reader->buffer |= ((unsigned)reader->data[start + 2] << 16u); - if(start + 3u < size) reader->buffer |= ((unsigned)reader->data[start + 3] << 24u); - reader->buffer >>= (reader->bp & 7u); - } - (void)nbits; -} - -/* Get bits without advancing the bit pointer. Must have enough bits available with ensureBits. Max nbits is 31. */ -static LODEPNG_INLINE unsigned peekBits(LodePNGBitReader* reader, size_t nbits) { - /* The shift allows nbits to be only up to 31. */ - return reader->buffer & ((1u << nbits) - 1u); -} - -/* Must have enough bits available with ensureBits */ -static LODEPNG_INLINE void advanceBits(LodePNGBitReader* reader, size_t nbits) { - reader->buffer >>= nbits; - reader->bp += nbits; -} - -/* Must have enough bits available with ensureBits */ -static LODEPNG_INLINE unsigned readBits(LodePNGBitReader* reader, size_t nbits) { - unsigned result = peekBits(reader, nbits); - advanceBits(reader, nbits); - return result; -} -#endif /*LODEPNG_COMPILE_DECODER*/ - -static unsigned reverseBits(unsigned bits, unsigned num) { - /*TODO: implement faster lookup table based version when needed*/ - unsigned i, result = 0; - for(i = 0; i < num; i++) result |= ((bits >> (num - i - 1u)) & 1u) << i; - return result; -} - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / Deflate - Huffman / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -#define FIRST_LENGTH_CODE_INDEX 257 -#define LAST_LENGTH_CODE_INDEX 285 -/*256 literals, the end code, some length codes, and 2 unused codes*/ -#define NUM_DEFLATE_CODE_SYMBOLS 288 -/*the distance codes have their own symbols, 30 used, 2 unused*/ -#define NUM_DISTANCE_SYMBOLS 32 -/*the code length codes. 0-15: code lengths, 16: copy previous 3-6 times, 17: 3-10 zeros, 18: 11-138 zeros*/ -#define NUM_CODE_LENGTH_CODES 19 - -/*the base lengths represented by codes 257-285*/ -static const unsigned LENGTHBASE[29] - = {3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, - 67, 83, 99, 115, 131, 163, 195, 227, 258}; - -/*the extra bits used by codes 257-285 (added to base length)*/ -static const unsigned LENGTHEXTRA[29] - = {0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, - 4, 4, 4, 4, 5, 5, 5, 5, 0}; - -/*the base backwards distances (the bits of distance codes appear after length codes and use their own huffman tree)*/ -static const unsigned DISTANCEBASE[30] - = {1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, - 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577}; - -/*the extra bits of backwards distances (added to base)*/ -static const unsigned DISTANCEEXTRA[30] - = {0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, - 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13}; - -/*the order in which "code length alphabet code lengths" are stored as specified by deflate, out of this the huffman -tree of the dynamic huffman tree lengths is generated*/ -static const unsigned CLCL_ORDER[NUM_CODE_LENGTH_CODES] - = {16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15}; - -/* ////////////////////////////////////////////////////////////////////////// */ - -/* -Huffman tree struct, containing multiple representations of the tree -*/ -typedef struct HuffmanTree { - unsigned* codes; /*the huffman codes (bit patterns representing the symbols)*/ - unsigned* lengths; /*the lengths of the huffman codes*/ - unsigned maxbitlen; /*maximum number of bits a single code can get*/ - unsigned numcodes; /*number of symbols in the alphabet = number of codes*/ - /* for reading only */ - unsigned char* table_len; /*length of symbol from lookup table, or max length if secondary lookup needed*/ - unsigned short* table_value; /*value of symbol from lookup table, or pointer to secondary table if needed*/ -} HuffmanTree; - -static void HuffmanTree_init(HuffmanTree* tree) { - tree->codes = 0; - tree->lengths = 0; - tree->table_len = 0; - tree->table_value = 0; -} - -static void HuffmanTree_cleanup(HuffmanTree* tree) { - lodepng_free(tree->codes); - lodepng_free(tree->lengths); - lodepng_free(tree->table_len); - lodepng_free(tree->table_value); -} - -/* amount of bits for first huffman table lookup (aka root bits), see HuffmanTree_makeTable and huffmanDecodeSymbol.*/ -/* values 8u and 9u work the fastest */ -#define FIRSTBITS 9u - -/* a symbol value too big to represent any valid symbol, to indicate reading disallowed huffman bits combination, -which is possible in case of only 0 or 1 present symbols. */ -#define INVALIDSYMBOL 65535u - -/* make table for huffman decoding */ -static unsigned HuffmanTree_makeTable(HuffmanTree* tree) { - static const unsigned headsize = 1u << FIRSTBITS; /*size of the first table*/ - static const unsigned mask = (1u << FIRSTBITS) /*headsize*/ - 1u; - size_t i, numpresent, pointer, size; /*total table size*/ - unsigned* maxlens = (unsigned*)lodepng_malloc(headsize * sizeof(unsigned)); - if(!maxlens) return 83; /*alloc fail*/ - - /* compute maxlens: max total bit length of symbols sharing prefix in the first table*/ - lodepng_memset(maxlens, 0, headsize * sizeof(*maxlens)); - for(i = 0; i < tree->numcodes; i++) { - unsigned symbol = tree->codes[i]; - unsigned l = tree->lengths[i]; - unsigned index; - if(l <= FIRSTBITS) continue; /*symbols that fit in first table don't increase secondary table size*/ - /*get the FIRSTBITS MSBs, the MSBs of the symbol are encoded first. See later comment about the reversing*/ - index = reverseBits(symbol >> (l - FIRSTBITS), FIRSTBITS); - maxlens[index] = LODEPNG_MAX(maxlens[index], l); - } - /* compute total table size: size of first table plus all secondary tables for symbols longer than FIRSTBITS */ - size = headsize; - for(i = 0; i < headsize; ++i) { - unsigned l = maxlens[i]; - if(l > FIRSTBITS) size += (((size_t)1) << (l - FIRSTBITS)); - } - tree->table_len = (unsigned char*)lodepng_malloc(size * sizeof(*tree->table_len)); - tree->table_value = (unsigned short*)lodepng_malloc(size * sizeof(*tree->table_value)); - if(!tree->table_len || !tree->table_value) { - lodepng_free(maxlens); - /* freeing tree->table values is done at a higher scope */ - return 83; /*alloc fail*/ - } - /*initialize with an invalid length to indicate unused entries*/ - for(i = 0; i < size; ++i) tree->table_len[i] = 16; - - /*fill in the first table for long symbols: max prefix size and pointer to secondary tables*/ - pointer = headsize; - for(i = 0; i < headsize; ++i) { - unsigned l = maxlens[i]; - if(l <= FIRSTBITS) continue; - tree->table_len[i] = l; - tree->table_value[i] = (unsigned short)pointer; - pointer += (((size_t)1) << (l - FIRSTBITS)); - } - lodepng_free(maxlens); - - /*fill in the first table for short symbols, or secondary table for long symbols*/ - numpresent = 0; - for(i = 0; i < tree->numcodes; ++i) { - unsigned l = tree->lengths[i]; - unsigned symbol, reverse; - if(l == 0) continue; - symbol = tree->codes[i]; /*the huffman bit pattern. i itself is the value.*/ - /*reverse bits, because the huffman bits are given in MSB first order but the bit reader reads LSB first*/ - reverse = reverseBits(symbol, l); - numpresent++; - - if(l <= FIRSTBITS) { - /*short symbol, fully in first table, replicated num times if l < FIRSTBITS*/ - unsigned num = 1u << (FIRSTBITS - l); - unsigned j; - for(j = 0; j < num; ++j) { - /*bit reader will read the l bits of symbol first, the remaining FIRSTBITS - l bits go to the MSB's*/ - unsigned index = reverse | (j << l); - if(tree->table_len[index] != 16) return 55; /*invalid tree: long symbol shares prefix with short symbol*/ - tree->table_len[index] = l; - tree->table_value[index] = (unsigned short)i; - } - } else { - /*long symbol, shares prefix with other long symbols in first lookup table, needs second lookup*/ - /*the FIRSTBITS MSBs of the symbol are the first table index*/ - unsigned index = reverse & mask; - unsigned maxlen = tree->table_len[index]; - /*log2 of secondary table length, should be >= l - FIRSTBITS*/ - unsigned tablelen = maxlen - FIRSTBITS; - unsigned start = tree->table_value[index]; /*starting index in secondary table*/ - unsigned num = 1u << (tablelen - (l - FIRSTBITS)); /*amount of entries of this symbol in secondary table*/ - unsigned j; - if(maxlen < l) return 55; /*invalid tree: long symbol shares prefix with short symbol*/ - for(j = 0; j < num; ++j) { - unsigned reverse2 = reverse >> FIRSTBITS; /* l - FIRSTBITS bits */ - unsigned index2 = start + (reverse2 | (j << (l - FIRSTBITS))); - tree->table_len[index2] = l; - tree->table_value[index2] = (unsigned short)i; - } - } - } - - if(numpresent < 2) { - /* In case of exactly 1 symbol, in theory the huffman symbol needs 0 bits, - but deflate uses 1 bit instead. In case of 0 symbols, no symbols can - appear at all, but such huffman tree could still exist (e.g. if distance - codes are never used). In both cases, not all symbols of the table will be - filled in. Fill them in with an invalid symbol value so returning them from - huffmanDecodeSymbol will cause error. */ - for(i = 0; i < size; ++i) { - if(tree->table_len[i] == 16) { - /* As length, use a value smaller than FIRSTBITS for the head table, - and a value larger than FIRSTBITS for the secondary table, to ensure - valid behavior for advanceBits when reading this symbol. */ - tree->table_len[i] = (i < headsize) ? 1 : (FIRSTBITS + 1); - tree->table_value[i] = INVALIDSYMBOL; - } - } - } else { - /* A good huffman tree has N * 2 - 1 nodes, of which N - 1 are internal nodes. - If that is not the case (due to too long length codes), the table will not - have been fully used, and this is an error (not all bit combinations can be - decoded): an oversubscribed huffman tree, indicated by error 55. */ - for(i = 0; i < size; ++i) { - if(tree->table_len[i] == 16) return 55; - } - } - - return 0; -} - -/* -Second step for the ...makeFromLengths and ...makeFromFrequencies functions. -numcodes, lengths and maxbitlen must already be filled in correctly. return -value is error. -*/ -static unsigned HuffmanTree_makeFromLengths2(HuffmanTree* tree) { - unsigned* blcount; - unsigned* nextcode; - unsigned error = 0; - unsigned bits, n; - - tree->codes = (unsigned*)lodepng_malloc(tree->numcodes * sizeof(unsigned)); - blcount = (unsigned*)lodepng_malloc((tree->maxbitlen + 1) * sizeof(unsigned)); - nextcode = (unsigned*)lodepng_malloc((tree->maxbitlen + 1) * sizeof(unsigned)); - if(!tree->codes || !blcount || !nextcode) error = 83; /*alloc fail*/ - - if(!error) { - for(n = 0; n != tree->maxbitlen + 1; n++) blcount[n] = nextcode[n] = 0; - /*step 1: count number of instances of each code length*/ - for(bits = 0; bits != tree->numcodes; ++bits) ++blcount[tree->lengths[bits]]; - /*step 2: generate the nextcode values*/ - for(bits = 1; bits <= tree->maxbitlen; ++bits) { - nextcode[bits] = (nextcode[bits - 1] + blcount[bits - 1]) << 1u; - } - /*step 3: generate all the codes*/ - for(n = 0; n != tree->numcodes; ++n) { - if(tree->lengths[n] != 0) { - tree->codes[n] = nextcode[tree->lengths[n]]++; - /*remove superfluous bits from the code*/ - tree->codes[n] &= ((1u << tree->lengths[n]) - 1u); - } - } - } - - lodepng_free(blcount); - lodepng_free(nextcode); - - if(!error) error = HuffmanTree_makeTable(tree); - return error; -} - -/* -given the code lengths (as stored in the PNG file), generate the tree as defined -by Deflate. maxbitlen is the maximum bits that a code in the tree can have. -return value is error. -*/ -static unsigned HuffmanTree_makeFromLengths(HuffmanTree* tree, const unsigned* bitlen, - size_t numcodes, unsigned maxbitlen) { - unsigned i; - tree->lengths = (unsigned*)lodepng_malloc(numcodes * sizeof(unsigned)); - if(!tree->lengths) return 83; /*alloc fail*/ - for(i = 0; i != numcodes; ++i) tree->lengths[i] = bitlen[i]; - tree->numcodes = (unsigned)numcodes; /*number of symbols*/ - tree->maxbitlen = maxbitlen; - return HuffmanTree_makeFromLengths2(tree); -} - -#ifdef LODEPNG_COMPILE_ENCODER - -/*BPM: Boundary Package Merge, see "A Fast and Space-Economical Algorithm for Length-Limited Coding", -Jyrki Katajainen, Alistair Moffat, Andrew Turpin, 1995.*/ - -/*chain node for boundary package merge*/ -typedef struct BPMNode { - int weight; /*the sum of all weights in this chain*/ - unsigned index; /*index of this leaf node (called "count" in the paper)*/ - struct BPMNode* tail; /*the next nodes in this chain (null if last)*/ - int in_use; -} BPMNode; - -/*lists of chains*/ -typedef struct BPMLists { - /*memory pool*/ - unsigned memsize; - BPMNode* memory; - unsigned numfree; - unsigned nextfree; - BPMNode** freelist; - /*two heads of lookahead chains per list*/ - unsigned listsize; - BPMNode** chains0; - BPMNode** chains1; -} BPMLists; - -/*creates a new chain node with the given parameters, from the memory in the lists */ -static BPMNode* bpmnode_create(BPMLists* lists, int weight, unsigned index, BPMNode* tail) { - unsigned i; - BPMNode* result; - - /*memory full, so garbage collect*/ - if(lists->nextfree >= lists->numfree) { - /*mark only those that are in use*/ - for(i = 0; i != lists->memsize; ++i) lists->memory[i].in_use = 0; - for(i = 0; i != lists->listsize; ++i) { - BPMNode* node; - for(node = lists->chains0[i]; node != 0; node = node->tail) node->in_use = 1; - for(node = lists->chains1[i]; node != 0; node = node->tail) node->in_use = 1; - } - /*collect those that are free*/ - lists->numfree = 0; - for(i = 0; i != lists->memsize; ++i) { - if(!lists->memory[i].in_use) lists->freelist[lists->numfree++] = &lists->memory[i]; - } - lists->nextfree = 0; - } - - result = lists->freelist[lists->nextfree++]; - result->weight = weight; - result->index = index; - result->tail = tail; - return result; -} - -/*sort the leaves with stable mergesort*/ -static void bpmnode_sort(BPMNode* leaves, size_t num) { - BPMNode* mem = (BPMNode*)lodepng_malloc(sizeof(*leaves) * num); - size_t width, counter = 0; - for(width = 1; width < num; width *= 2) { - BPMNode* a = (counter & 1) ? mem : leaves; - BPMNode* b = (counter & 1) ? leaves : mem; - size_t p; - for(p = 0; p < num; p += 2 * width) { - size_t q = (p + width > num) ? num : (p + width); - size_t r = (p + 2 * width > num) ? num : (p + 2 * width); - size_t i = p, j = q, k; - for(k = p; k < r; k++) { - if(i < q && (j >= r || a[i].weight <= a[j].weight)) b[k] = a[i++]; - else b[k] = a[j++]; - } - } - counter++; - } - if(counter & 1) lodepng_memcpy(leaves, mem, sizeof(*leaves) * num); - lodepng_free(mem); -} - -/*Boundary Package Merge step, numpresent is the amount of leaves, and c is the current chain.*/ -static void boundaryPM(BPMLists* lists, BPMNode* leaves, size_t numpresent, int c, int num) { - unsigned lastindex = lists->chains1[c]->index; - - if(c == 0) { - if(lastindex >= numpresent) return; - lists->chains0[c] = lists->chains1[c]; - lists->chains1[c] = bpmnode_create(lists, leaves[lastindex].weight, lastindex + 1, 0); - } else { - /*sum of the weights of the head nodes of the previous lookahead chains.*/ - int sum = lists->chains0[c - 1]->weight + lists->chains1[c - 1]->weight; - lists->chains0[c] = lists->chains1[c]; - if(lastindex < numpresent && sum > leaves[lastindex].weight) { - lists->chains1[c] = bpmnode_create(lists, leaves[lastindex].weight, lastindex + 1, lists->chains1[c]->tail); - return; - } - lists->chains1[c] = bpmnode_create(lists, sum, lastindex, lists->chains1[c - 1]); - /*in the end we are only interested in the chain of the last list, so no - need to recurse if we're at the last one (this gives measurable speedup)*/ - if(num + 1 < (int)(2 * numpresent - 2)) { - boundaryPM(lists, leaves, numpresent, c - 1, num); - boundaryPM(lists, leaves, numpresent, c - 1, num); - } - } -} - -unsigned lodepng_huffman_code_lengths(unsigned* lengths, const unsigned* frequencies, - size_t numcodes, unsigned maxbitlen) { - unsigned error = 0; - unsigned i; - size_t numpresent = 0; /*number of symbols with non-zero frequency*/ - BPMNode* leaves; /*the symbols, only those with > 0 frequency*/ - - if(numcodes == 0) return 80; /*error: a tree of 0 symbols is not supposed to be made*/ - if((1u << maxbitlen) < (unsigned)numcodes) return 80; /*error: represent all symbols*/ - - leaves = (BPMNode*)lodepng_malloc(numcodes * sizeof(*leaves)); - if(!leaves) return 83; /*alloc fail*/ - - for(i = 0; i != numcodes; ++i) { - if(frequencies[i] > 0) { - leaves[numpresent].weight = (int)frequencies[i]; - leaves[numpresent].index = i; - ++numpresent; - } - } - - lodepng_memset(lengths, 0, numcodes * sizeof(*lengths)); - - /*ensure at least two present symbols. There should be at least one symbol - according to RFC 1951 section 3.2.7. Some decoders incorrectly require two. To - make these work as well ensure there are at least two symbols. The - Package-Merge code below also doesn't work correctly if there's only one - symbol, it'd give it the theoretical 0 bits but in practice zlib wants 1 bit*/ - if(numpresent == 0) { - lengths[0] = lengths[1] = 1; /*note that for RFC 1951 section 3.2.7, only lengths[0] = 1 is needed*/ - } else if(numpresent == 1) { - lengths[leaves[0].index] = 1; - lengths[leaves[0].index == 0 ? 1 : 0] = 1; - } else { - BPMLists lists; - BPMNode* node; - - bpmnode_sort(leaves, numpresent); - - lists.listsize = maxbitlen; - lists.memsize = 2 * maxbitlen * (maxbitlen + 1); - lists.nextfree = 0; - lists.numfree = lists.memsize; - lists.memory = (BPMNode*)lodepng_malloc(lists.memsize * sizeof(*lists.memory)); - lists.freelist = (BPMNode**)lodepng_malloc(lists.memsize * sizeof(BPMNode*)); - lists.chains0 = (BPMNode**)lodepng_malloc(lists.listsize * sizeof(BPMNode*)); - lists.chains1 = (BPMNode**)lodepng_malloc(lists.listsize * sizeof(BPMNode*)); - if(!lists.memory || !lists.freelist || !lists.chains0 || !lists.chains1) error = 83; /*alloc fail*/ - - if(!error) { - for(i = 0; i != lists.memsize; ++i) lists.freelist[i] = &lists.memory[i]; - - bpmnode_create(&lists, leaves[0].weight, 1, 0); - bpmnode_create(&lists, leaves[1].weight, 2, 0); - - for(i = 0; i != lists.listsize; ++i) { - lists.chains0[i] = &lists.memory[0]; - lists.chains1[i] = &lists.memory[1]; - } - - /*each boundaryPM call adds one chain to the last list, and we need 2 * numpresent - 2 chains.*/ - for(i = 2; i != 2 * numpresent - 2; ++i) boundaryPM(&lists, leaves, numpresent, (int)maxbitlen - 1, (int)i); - - for(node = lists.chains1[maxbitlen - 1]; node; node = node->tail) { - for(i = 0; i != node->index; ++i) ++lengths[leaves[i].index]; - } - } - - lodepng_free(lists.memory); - lodepng_free(lists.freelist); - lodepng_free(lists.chains0); - lodepng_free(lists.chains1); - } - - lodepng_free(leaves); - return error; -} - -/*Create the Huffman tree given the symbol frequencies*/ -static unsigned HuffmanTree_makeFromFrequencies(HuffmanTree* tree, const unsigned* frequencies, - size_t mincodes, size_t numcodes, unsigned maxbitlen) { - unsigned error = 0; - while(!frequencies[numcodes - 1] && numcodes > mincodes) --numcodes; /*trim zeroes*/ - tree->lengths = (unsigned*)lodepng_malloc(numcodes * sizeof(unsigned)); - if(!tree->lengths) return 83; /*alloc fail*/ - tree->maxbitlen = maxbitlen; - tree->numcodes = (unsigned)numcodes; /*number of symbols*/ - - error = lodepng_huffman_code_lengths(tree->lengths, frequencies, numcodes, maxbitlen); - if(!error) error = HuffmanTree_makeFromLengths2(tree); - return error; -} -#endif /*LODEPNG_COMPILE_ENCODER*/ - -/*get the literal and length code tree of a deflated block with fixed tree, as per the deflate specification*/ -static unsigned generateFixedLitLenTree(HuffmanTree* tree) { - unsigned i, error = 0; - unsigned* bitlen = (unsigned*)lodepng_malloc(NUM_DEFLATE_CODE_SYMBOLS * sizeof(unsigned)); - if(!bitlen) return 83; /*alloc fail*/ - - /*288 possible codes: 0-255=literals, 256=endcode, 257-285=lengthcodes, 286-287=unused*/ - for(i = 0; i <= 143; ++i) bitlen[i] = 8; - for(i = 144; i <= 255; ++i) bitlen[i] = 9; - for(i = 256; i <= 279; ++i) bitlen[i] = 7; - for(i = 280; i <= 287; ++i) bitlen[i] = 8; - - error = HuffmanTree_makeFromLengths(tree, bitlen, NUM_DEFLATE_CODE_SYMBOLS, 15); - - lodepng_free(bitlen); - return error; -} - -/*get the distance code tree of a deflated block with fixed tree, as specified in the deflate specification*/ -static unsigned generateFixedDistanceTree(HuffmanTree* tree) { - unsigned i, error = 0; - unsigned* bitlen = (unsigned*)lodepng_malloc(NUM_DISTANCE_SYMBOLS * sizeof(unsigned)); - if(!bitlen) return 83; /*alloc fail*/ - - /*there are 32 distance codes, but 30-31 are unused*/ - for(i = 0; i != NUM_DISTANCE_SYMBOLS; ++i) bitlen[i] = 5; - error = HuffmanTree_makeFromLengths(tree, bitlen, NUM_DISTANCE_SYMBOLS, 15); - - lodepng_free(bitlen); - return error; -} - -#ifdef LODEPNG_COMPILE_DECODER - -/* -returns the code. The bit reader must already have been ensured at least 15 bits -*/ -static unsigned huffmanDecodeSymbol(LodePNGBitReader* reader, const HuffmanTree* codetree) { - unsigned short code = peekBits(reader, FIRSTBITS); - unsigned short l = codetree->table_len[code]; - unsigned short value = codetree->table_value[code]; - if(l <= FIRSTBITS) { - advanceBits(reader, l); - return value; - } else { - advanceBits(reader, FIRSTBITS); - value += peekBits(reader, l - FIRSTBITS); - advanceBits(reader, codetree->table_len[value] - FIRSTBITS); - return codetree->table_value[value]; - } -} -#endif /*LODEPNG_COMPILE_DECODER*/ - -#ifdef LODEPNG_COMPILE_DECODER - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / Inflator (Decompressor) / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -/*get the tree of a deflated block with fixed tree, as specified in the deflate specification -Returns error code.*/ -static unsigned getTreeInflateFixed(HuffmanTree* tree_ll, HuffmanTree* tree_d) { - unsigned error = generateFixedLitLenTree(tree_ll); - if(error) return error; - return generateFixedDistanceTree(tree_d); -} - -/*get the tree of a deflated block with dynamic tree, the tree itself is also Huffman compressed with a known tree*/ -static unsigned getTreeInflateDynamic(HuffmanTree* tree_ll, HuffmanTree* tree_d, - LodePNGBitReader* reader) { - /*make sure that length values that aren't filled in will be 0, or a wrong tree will be generated*/ - unsigned error = 0; - unsigned n, HLIT, HDIST, HCLEN, i; - - /*see comments in deflateDynamic for explanation of the context and these variables, it is analogous*/ - unsigned* bitlen_ll = 0; /*lit,len code lengths*/ - unsigned* bitlen_d = 0; /*dist code lengths*/ - /*code length code lengths ("clcl"), the bit lengths of the huffman tree used to compress bitlen_ll and bitlen_d*/ - unsigned* bitlen_cl = 0; - HuffmanTree tree_cl; /*the code tree for code length codes (the huffman tree for compressed huffman trees)*/ - - if(reader->bitsize - reader->bp < 14) return 49; /*error: the bit pointer is or will go past the memory*/ - ensureBits17(reader, 14); - - /*number of literal/length codes + 257. Unlike the spec, the value 257 is added to it here already*/ - HLIT = readBits(reader, 5) + 257; - /*number of distance codes. Unlike the spec, the value 1 is added to it here already*/ - HDIST = readBits(reader, 5) + 1; - /*number of code length codes. Unlike the spec, the value 4 is added to it here already*/ - HCLEN = readBits(reader, 4) + 4; - - bitlen_cl = (unsigned*)lodepng_malloc(NUM_CODE_LENGTH_CODES * sizeof(unsigned)); - if(!bitlen_cl) return 83 /*alloc fail*/; - - HuffmanTree_init(&tree_cl); - - while(!error) { - /*read the code length codes out of 3 * (amount of code length codes) bits*/ - if(lodepng_gtofl(reader->bp, HCLEN * 3, reader->bitsize)) { - ERROR_BREAK(50); /*error: the bit pointer is or will go past the memory*/ - } - for(i = 0; i != HCLEN; ++i) { - ensureBits9(reader, 3); /*out of bounds already checked above */ - bitlen_cl[CLCL_ORDER[i]] = readBits(reader, 3); - } - for(i = HCLEN; i != NUM_CODE_LENGTH_CODES; ++i) { - bitlen_cl[CLCL_ORDER[i]] = 0; - } - - error = HuffmanTree_makeFromLengths(&tree_cl, bitlen_cl, NUM_CODE_LENGTH_CODES, 7); - if(error) break; - - /*now we can use this tree to read the lengths for the tree that this function will return*/ - bitlen_ll = (unsigned*)lodepng_malloc(NUM_DEFLATE_CODE_SYMBOLS * sizeof(unsigned)); - bitlen_d = (unsigned*)lodepng_malloc(NUM_DISTANCE_SYMBOLS * sizeof(unsigned)); - if(!bitlen_ll || !bitlen_d) ERROR_BREAK(83 /*alloc fail*/); - lodepng_memset(bitlen_ll, 0, NUM_DEFLATE_CODE_SYMBOLS * sizeof(*bitlen_ll)); - lodepng_memset(bitlen_d, 0, NUM_DISTANCE_SYMBOLS * sizeof(*bitlen_d)); - - /*i is the current symbol we're reading in the part that contains the code lengths of lit/len and dist codes*/ - i = 0; - while(i < HLIT + HDIST) { - unsigned code; - ensureBits25(reader, 22); /* up to 15 bits for huffman code, up to 7 extra bits below*/ - code = huffmanDecodeSymbol(reader, &tree_cl); - if(code <= 15) /*a length code*/ { - if(i < HLIT) bitlen_ll[i] = code; - else bitlen_d[i - HLIT] = code; - ++i; - } else if(code == 16) /*repeat previous*/ { - unsigned replength = 3; /*read in the 2 bits that indicate repeat length (3-6)*/ - unsigned value; /*set value to the previous code*/ - - if(i == 0) ERROR_BREAK(54); /*can't repeat previous if i is 0*/ - - replength += readBits(reader, 2); - - if(i < HLIT + 1) value = bitlen_ll[i - 1]; - else value = bitlen_d[i - HLIT - 1]; - /*repeat this value in the next lengths*/ - for(n = 0; n < replength; ++n) { - if(i >= HLIT + HDIST) ERROR_BREAK(13); /*error: i is larger than the amount of codes*/ - if(i < HLIT) bitlen_ll[i] = value; - else bitlen_d[i - HLIT] = value; - ++i; - } - } else if(code == 17) /*repeat "0" 3-10 times*/ { - unsigned replength = 3; /*read in the bits that indicate repeat length*/ - replength += readBits(reader, 3); - - /*repeat this value in the next lengths*/ - for(n = 0; n < replength; ++n) { - if(i >= HLIT + HDIST) ERROR_BREAK(14); /*error: i is larger than the amount of codes*/ - - if(i < HLIT) bitlen_ll[i] = 0; - else bitlen_d[i - HLIT] = 0; - ++i; - } - } else if(code == 18) /*repeat "0" 11-138 times*/ { - unsigned replength = 11; /*read in the bits that indicate repeat length*/ - replength += readBits(reader, 7); - - /*repeat this value in the next lengths*/ - for(n = 0; n < replength; ++n) { - if(i >= HLIT + HDIST) ERROR_BREAK(15); /*error: i is larger than the amount of codes*/ - - if(i < HLIT) bitlen_ll[i] = 0; - else bitlen_d[i - HLIT] = 0; - ++i; - } - } else /*if(code == INVALIDSYMBOL)*/ { - ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ - } - /*check if any of the ensureBits above went out of bounds*/ - if(reader->bp > reader->bitsize) { - /*return error code 10 or 11 depending on the situation that happened in huffmanDecodeSymbol - (10=no endcode, 11=wrong jump outside of tree)*/ - /* TODO: revise error codes 10,11,50: the above comment is no longer valid */ - ERROR_BREAK(50); /*error, bit pointer jumps past memory*/ - } - } - if(error) break; - - if(bitlen_ll[256] == 0) ERROR_BREAK(64); /*the length of the end code 256 must be larger than 0*/ - - /*now we've finally got HLIT and HDIST, so generate the code trees, and the function is done*/ - error = HuffmanTree_makeFromLengths(tree_ll, bitlen_ll, NUM_DEFLATE_CODE_SYMBOLS, 15); - if(error) break; - error = HuffmanTree_makeFromLengths(tree_d, bitlen_d, NUM_DISTANCE_SYMBOLS, 15); - - break; /*end of error-while*/ - } - - lodepng_free(bitlen_cl); - lodepng_free(bitlen_ll); - lodepng_free(bitlen_d); - HuffmanTree_cleanup(&tree_cl); - - return error; -} - -/*inflate a block with dynamic of fixed Huffman tree. btype must be 1 or 2.*/ -static unsigned inflateHuffmanBlock(ucvector* out, LodePNGBitReader* reader, - unsigned btype, size_t max_output_size) { - unsigned error = 0; - HuffmanTree tree_ll; /*the huffman tree for literal and length codes*/ - HuffmanTree tree_d; /*the huffman tree for distance codes*/ - const size_t reserved_size = 260; /* must be at least 258 for max length, and a few extra for adding a few extra literals */ - int done = 0; - - if(!ucvector_reserve(out, out->size + reserved_size)) return 83; /*alloc fail*/ - - HuffmanTree_init(&tree_ll); - HuffmanTree_init(&tree_d); - - if(btype == 1) error = getTreeInflateFixed(&tree_ll, &tree_d); - else /*if(btype == 2)*/ error = getTreeInflateDynamic(&tree_ll, &tree_d, reader); - - - while(!error && !done) /*decode all symbols until end reached, breaks at end code*/ { - /*code_ll is literal, length or end code*/ - unsigned code_ll; - /* ensure enough bits for 2 huffman code reads (15 bits each): if the first is a literal, a second literal is read at once. This - appears to be slightly faster, than ensuring 20 bits here for 1 huffman symbol and the potential 5 extra bits for the length symbol.*/ - ensureBits32(reader, 30); - code_ll = huffmanDecodeSymbol(reader, &tree_ll); - if(code_ll <= 255) { - /*slightly faster code path if multiple literals in a row*/ - out->data[out->size++] = (unsigned char)code_ll; - code_ll = huffmanDecodeSymbol(reader, &tree_ll); - } - if(code_ll <= 255) /*literal symbol*/ { - out->data[out->size++] = (unsigned char)code_ll; - } else if(code_ll >= FIRST_LENGTH_CODE_INDEX && code_ll <= LAST_LENGTH_CODE_INDEX) /*length code*/ { - unsigned code_d, distance; - unsigned numextrabits_l, numextrabits_d; /*extra bits for length and distance*/ - size_t start, backward, length; - - /*part 1: get length base*/ - length = LENGTHBASE[code_ll - FIRST_LENGTH_CODE_INDEX]; - - /*part 2: get extra bits and add the value of that to length*/ - numextrabits_l = LENGTHEXTRA[code_ll - FIRST_LENGTH_CODE_INDEX]; - if(numextrabits_l != 0) { - /* bits already ensured above */ - ensureBits25(reader, 5); - length += readBits(reader, numextrabits_l); - } - - /*part 3: get distance code*/ - ensureBits32(reader, 28); /* up to 15 for the huffman symbol, up to 13 for the extra bits */ - code_d = huffmanDecodeSymbol(reader, &tree_d); - if(code_d > 29) { - if(code_d <= 31) { - ERROR_BREAK(18); /*error: invalid distance code (30-31 are never used)*/ - } else /* if(code_d == INVALIDSYMBOL) */{ - ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ - } - } - distance = DISTANCEBASE[code_d]; - - /*part 4: get extra bits from distance*/ - numextrabits_d = DISTANCEEXTRA[code_d]; - if(numextrabits_d != 0) { - /* bits already ensured above */ - distance += readBits(reader, numextrabits_d); - } - - /*part 5: fill in all the out[n] values based on the length and dist*/ - start = out->size; - if(distance > start) ERROR_BREAK(52); /*too long backward distance*/ - backward = start - distance; - - out->size += length; - if(distance < length) { - size_t forward; - lodepng_memcpy(out->data + start, out->data + backward, distance); - start += distance; - for(forward = distance; forward < length; ++forward) { - out->data[start++] = out->data[backward++]; - } - } else { - lodepng_memcpy(out->data + start, out->data + backward, length); - } - } else if(code_ll == 256) { - done = 1; /*end code, finish the loop*/ - } else /*if(code_ll == INVALIDSYMBOL)*/ { - ERROR_BREAK(16); /*error: tried to read disallowed huffman symbol*/ - } - if(out->allocsize - out->size < reserved_size) { - if(!ucvector_reserve(out, out->size + reserved_size)) ERROR_BREAK(83); /*alloc fail*/ - } - /*check if any of the ensureBits above went out of bounds*/ - if(reader->bp > reader->bitsize) { - /*return error code 10 or 11 depending on the situation that happened in huffmanDecodeSymbol - (10=no endcode, 11=wrong jump outside of tree)*/ - /* TODO: revise error codes 10,11,50: the above comment is no longer valid */ - ERROR_BREAK(51); /*error, bit pointer jumps past memory*/ - } - if(max_output_size && out->size > max_output_size) { - ERROR_BREAK(109); /*error, larger than max size*/ - } - } - - HuffmanTree_cleanup(&tree_ll); - HuffmanTree_cleanup(&tree_d); - - return error; -} - -static unsigned inflateNoCompression(ucvector* out, LodePNGBitReader* reader, - const LodePNGDecompressSettings* settings) { - size_t bytepos; - size_t size = reader->size; - unsigned LEN, NLEN, error = 0; - - /*go to first boundary of byte*/ - bytepos = (reader->bp + 7u) >> 3u; - - /*read LEN (2 bytes) and NLEN (2 bytes)*/ - if(bytepos + 4 >= size) return 52; /*error, bit pointer will jump past memory*/ - LEN = (unsigned)reader->data[bytepos] + ((unsigned)reader->data[bytepos + 1] << 8u); bytepos += 2; - NLEN = (unsigned)reader->data[bytepos] + ((unsigned)reader->data[bytepos + 1] << 8u); bytepos += 2; - - /*check if 16-bit NLEN is really the one's complement of LEN*/ - if(!settings->ignore_nlen && LEN + NLEN != 65535) { - return 21; /*error: NLEN is not one's complement of LEN*/ - } - - if(!ucvector_resize(out, out->size + LEN)) return 83; /*alloc fail*/ - - /*read the literal data: LEN bytes are now stored in the out buffer*/ - if(bytepos + LEN > size) return 23; /*error: reading outside of in buffer*/ - - /*out->data can be NULL (when LEN is zero), and arithmetic on NULL ptr is undefined*/ - if (LEN) { - lodepng_memcpy(out->data + out->size - LEN, reader->data + bytepos, LEN); - bytepos += LEN; - } - - reader->bp = bytepos << 3u; - - return error; -} - -static unsigned lodepng_inflatev(ucvector* out, - const unsigned char* in, size_t insize, - const LodePNGDecompressSettings* settings) { - unsigned BFINAL = 0; - LodePNGBitReader reader; - unsigned error = LodePNGBitReader_init(&reader, in, insize); - - if(error) return error; - - while(!BFINAL) { - unsigned BTYPE; - if(reader.bitsize - reader.bp < 3) return 52; /*error, bit pointer will jump past memory*/ - ensureBits9(&reader, 3); - BFINAL = readBits(&reader, 1); - BTYPE = readBits(&reader, 2); - - if(BTYPE == 3) return 20; /*error: invalid BTYPE*/ - else if(BTYPE == 0) error = inflateNoCompression(out, &reader, settings); /*no compression*/ - else error = inflateHuffmanBlock(out, &reader, BTYPE, settings->max_output_size); /*compression, BTYPE 01 or 10*/ - if(!error && settings->max_output_size && out->size > settings->max_output_size) error = 109; - if(error) break; - } - - return error; -} - -unsigned lodepng_inflate(unsigned char** out, size_t* outsize, - const unsigned char* in, size_t insize, - const LodePNGDecompressSettings* settings) { - ucvector v = ucvector_init(*out, *outsize); - unsigned error = lodepng_inflatev(&v, in, insize, settings); - *out = v.data; - *outsize = v.size; - return error; -} - -static unsigned inflatev(ucvector* out, const unsigned char* in, size_t insize, - const LodePNGDecompressSettings* settings) { - if(settings->custom_inflate) { - unsigned error = settings->custom_inflate(&out->data, &out->size, in, insize, settings); - out->allocsize = out->size; - if(error) { - /*the custom inflate is allowed to have its own error codes, however, we translate it to code 110*/ - error = 110; - /*if there's a max output size, and the custom zlib returned error, then indicate that error instead*/ - if(settings->max_output_size && out->size > settings->max_output_size) error = 109; - } - return error; - } else { - return lodepng_inflatev(out, in, insize, settings); - } -} - -#endif /*LODEPNG_COMPILE_DECODER*/ - -#ifdef LODEPNG_COMPILE_ENCODER - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / Deflator (Compressor) / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -static const unsigned MAX_SUPPORTED_DEFLATE_LENGTH = 258; - -/*search the index in the array, that has the largest value smaller than or equal to the given value, -given array must be sorted (if no value is smaller, it returns the size of the given array)*/ -static size_t searchCodeIndex(const unsigned* array, size_t array_size, size_t value) { - /*binary search (only small gain over linear). TODO: use CPU log2 instruction for getting symbols instead*/ - size_t left = 1; - size_t right = array_size - 1; - - while(left <= right) { - size_t mid = (left + right) >> 1; - if(array[mid] >= value) right = mid - 1; - else left = mid + 1; - } - if(left >= array_size || array[left] > value) left--; - return left; -} - -static void addLengthDistance(uivector* values, size_t length, size_t distance) { - /*values in encoded vector are those used by deflate: - 0-255: literal bytes - 256: end - 257-285: length/distance pair (length code, followed by extra length bits, distance code, extra distance bits) - 286-287: invalid*/ - - unsigned length_code = (unsigned)searchCodeIndex(LENGTHBASE, 29, length); - unsigned extra_length = (unsigned)(length - LENGTHBASE[length_code]); - unsigned dist_code = (unsigned)searchCodeIndex(DISTANCEBASE, 30, distance); - unsigned extra_distance = (unsigned)(distance - DISTANCEBASE[dist_code]); - - size_t pos = values->size; - /*TODO: return error when this fails (out of memory)*/ - unsigned ok = uivector_resize(values, values->size + 4); - if(ok) { - values->data[pos + 0] = length_code + FIRST_LENGTH_CODE_INDEX; - values->data[pos + 1] = extra_length; - values->data[pos + 2] = dist_code; - values->data[pos + 3] = extra_distance; - } -} - -/*3 bytes of data get encoded into two bytes. The hash cannot use more than 3 -bytes as input because 3 is the minimum match length for deflate*/ -static const unsigned HASH_NUM_VALUES = 65536; -static const unsigned HASH_BIT_MASK = 65535; /*HASH_NUM_VALUES - 1, but C90 does not like that as initializer*/ - -typedef struct Hash { - int* head; /*hash value to head circular pos - can be outdated if went around window*/ - /*circular pos to prev circular pos*/ - unsigned short* chain; - int* val; /*circular pos to hash value*/ - - /*TODO: do this not only for zeros but for any repeated byte. However for PNG - it's always going to be the zeros that dominate, so not important for PNG*/ - int* headz; /*similar to head, but for chainz*/ - unsigned short* chainz; /*those with same amount of zeros*/ - unsigned short* zeros; /*length of zeros streak, used as a second hash chain*/ -} Hash; - -static unsigned hash_init(Hash* hash, unsigned windowsize) { - unsigned i; - hash->head = (int*)lodepng_malloc(sizeof(int) * HASH_NUM_VALUES); - hash->val = (int*)lodepng_malloc(sizeof(int) * windowsize); - hash->chain = (unsigned short*)lodepng_malloc(sizeof(unsigned short) * windowsize); - - hash->zeros = (unsigned short*)lodepng_malloc(sizeof(unsigned short) * windowsize); - hash->headz = (int*)lodepng_malloc(sizeof(int) * (MAX_SUPPORTED_DEFLATE_LENGTH + 1)); - hash->chainz = (unsigned short*)lodepng_malloc(sizeof(unsigned short) * windowsize); - - if(!hash->head || !hash->chain || !hash->val || !hash->headz|| !hash->chainz || !hash->zeros) { - return 83; /*alloc fail*/ - } - - /*initialize hash table*/ - for(i = 0; i != HASH_NUM_VALUES; ++i) hash->head[i] = -1; - for(i = 0; i != windowsize; ++i) hash->val[i] = -1; - for(i = 0; i != windowsize; ++i) hash->chain[i] = i; /*same value as index indicates uninitialized*/ - - for(i = 0; i <= MAX_SUPPORTED_DEFLATE_LENGTH; ++i) hash->headz[i] = -1; - for(i = 0; i != windowsize; ++i) hash->chainz[i] = i; /*same value as index indicates uninitialized*/ - - return 0; -} - -static void hash_cleanup(Hash* hash) { - lodepng_free(hash->head); - lodepng_free(hash->val); - lodepng_free(hash->chain); - - lodepng_free(hash->zeros); - lodepng_free(hash->headz); - lodepng_free(hash->chainz); -} - - - -static unsigned getHash(const unsigned char* data, size_t size, size_t pos) { - unsigned result = 0; - if(pos + 2 < size) { - /*A simple shift and xor hash is used. Since the data of PNGs is dominated - by zeroes due to the filters, a better hash does not have a significant - effect on speed in traversing the chain, and causes more time spend on - calculating the hash.*/ - result ^= ((unsigned)data[pos + 0] << 0u); - result ^= ((unsigned)data[pos + 1] << 4u); - result ^= ((unsigned)data[pos + 2] << 8u); - } else { - size_t amount, i; - if(pos >= size) return 0; - amount = size - pos; - for(i = 0; i != amount; ++i) result ^= ((unsigned)data[pos + i] << (i * 8u)); - } - return result & HASH_BIT_MASK; -} - -static unsigned countZeros(const unsigned char* data, size_t size, size_t pos) { - const unsigned char* start = data + pos; - const unsigned char* end = start + MAX_SUPPORTED_DEFLATE_LENGTH; - if(end > data + size) end = data + size; - data = start; - while(data != end && *data == 0) ++data; - /*subtracting two addresses returned as 32-bit number (max value is MAX_SUPPORTED_DEFLATE_LENGTH)*/ - return (unsigned)(data - start); -} - -/*wpos = pos & (windowsize - 1)*/ -static void updateHashChain(Hash* hash, size_t wpos, unsigned hashval, unsigned short numzeros) { - hash->val[wpos] = (int)hashval; - if(hash->head[hashval] != -1) hash->chain[wpos] = hash->head[hashval]; - hash->head[hashval] = (int)wpos; - - hash->zeros[wpos] = numzeros; - if(hash->headz[numzeros] != -1) hash->chainz[wpos] = hash->headz[numzeros]; - hash->headz[numzeros] = (int)wpos; -} - -/* -LZ77-encode the data. Return value is error code. The input are raw bytes, the output -is in the form of unsigned integers with codes representing for example literal bytes, or -length/distance pairs. -It uses a hash table technique to let it encode faster. When doing LZ77 encoding, a -sliding window (of windowsize) is used, and all past bytes in that window can be used as -the "dictionary". A brute force search through all possible distances would be slow, and -this hash technique is one out of several ways to speed this up. -*/ -static unsigned encodeLZ77(uivector* out, Hash* hash, - const unsigned char* in, size_t inpos, size_t insize, unsigned windowsize, - unsigned minmatch, unsigned nicematch, unsigned lazymatching) { - size_t pos; - unsigned i, error = 0; - /*for large window lengths, assume the user wants no compression loss. Otherwise, max hash chain length speedup.*/ - unsigned maxchainlength = windowsize >= 8192 ? windowsize : windowsize / 8u; - unsigned maxlazymatch = windowsize >= 8192 ? MAX_SUPPORTED_DEFLATE_LENGTH : 64; - - unsigned usezeros = 1; /*not sure if setting it to false for windowsize < 8192 is better or worse*/ - unsigned numzeros = 0; - - unsigned offset; /*the offset represents the distance in LZ77 terminology*/ - unsigned length; - unsigned lazy = 0; - unsigned lazylength = 0, lazyoffset = 0; - unsigned hashval; - unsigned current_offset, current_length; - unsigned prev_offset; - const unsigned char *lastptr, *foreptr, *backptr; - unsigned hashpos; - - if(windowsize == 0 || windowsize > 32768) return 60; /*error: windowsize smaller/larger than allowed*/ - if((windowsize & (windowsize - 1)) != 0) return 90; /*error: must be power of two*/ - - if(nicematch > MAX_SUPPORTED_DEFLATE_LENGTH) nicematch = MAX_SUPPORTED_DEFLATE_LENGTH; - - for(pos = inpos; pos < insize; ++pos) { - size_t wpos = pos & (windowsize - 1); /*position for in 'circular' hash buffers*/ - unsigned chainlength = 0; - - hashval = getHash(in, insize, pos); - - if(usezeros && hashval == 0) { - if(numzeros == 0) numzeros = countZeros(in, insize, pos); - else if(pos + numzeros > insize || in[pos + numzeros - 1] != 0) --numzeros; - } else { - numzeros = 0; - } - - updateHashChain(hash, wpos, hashval, numzeros); - - /*the length and offset found for the current position*/ - length = 0; - offset = 0; - - hashpos = hash->chain[wpos]; - - lastptr = &in[insize < pos + MAX_SUPPORTED_DEFLATE_LENGTH ? insize : pos + MAX_SUPPORTED_DEFLATE_LENGTH]; - - /*search for the longest string*/ - prev_offset = 0; - for(;;) { - if(chainlength++ >= maxchainlength) break; - current_offset = (unsigned)(hashpos <= wpos ? wpos - hashpos : wpos - hashpos + windowsize); - - if(current_offset < prev_offset) break; /*stop when went completely around the circular buffer*/ - prev_offset = current_offset; - if(current_offset > 0) { - /*test the next characters*/ - foreptr = &in[pos]; - backptr = &in[pos - current_offset]; - - /*common case in PNGs is lots of zeros. Quickly skip over them as a speedup*/ - if(numzeros >= 3) { - unsigned skip = hash->zeros[hashpos]; - if(skip > numzeros) skip = numzeros; - backptr += skip; - foreptr += skip; - } - - while(foreptr != lastptr && *backptr == *foreptr) /*maximum supported length by deflate is max length*/ { - ++backptr; - ++foreptr; - } - current_length = (unsigned)(foreptr - &in[pos]); - - if(current_length > length) { - length = current_length; /*the longest length*/ - offset = current_offset; /*the offset that is related to this longest length*/ - /*jump out once a length of max length is found (speed gain). This also jumps - out if length is MAX_SUPPORTED_DEFLATE_LENGTH*/ - if(current_length >= nicematch) break; - } - } - - if(hashpos == hash->chain[hashpos]) break; - - if(numzeros >= 3 && length > numzeros) { - hashpos = hash->chainz[hashpos]; - if(hash->zeros[hashpos] != numzeros) break; - } else { - hashpos = hash->chain[hashpos]; - /*outdated hash value, happens if particular value was not encountered in whole last window*/ - if(hash->val[hashpos] != (int)hashval) break; - } - } - - if(lazymatching) { - if(!lazy && length >= 3 && length <= maxlazymatch && length < MAX_SUPPORTED_DEFLATE_LENGTH) { - lazy = 1; - lazylength = length; - lazyoffset = offset; - continue; /*try the next byte*/ - } - if(lazy) { - lazy = 0; - if(pos == 0) ERROR_BREAK(81); - if(length > lazylength + 1) { - /*push the previous character as literal*/ - if(!uivector_push_back(out, in[pos - 1])) ERROR_BREAK(83 /*alloc fail*/); - } else { - length = lazylength; - offset = lazyoffset; - hash->head[hashval] = -1; /*the same hashchain update will be done, this ensures no wrong alteration*/ - hash->headz[numzeros] = -1; /*idem*/ - --pos; - } - } - } - if(length >= 3 && offset > windowsize) ERROR_BREAK(86 /*too big (or overflown negative) offset*/); - - /*encode it as length/distance pair or literal value*/ - if(length < 3) /*only lengths of 3 or higher are supported as length/distance pair*/ { - if(!uivector_push_back(out, in[pos])) ERROR_BREAK(83 /*alloc fail*/); - } else if(length < minmatch || (length == 3 && offset > 4096)) { - /*compensate for the fact that longer offsets have more extra bits, a - length of only 3 may be not worth it then*/ - if(!uivector_push_back(out, in[pos])) ERROR_BREAK(83 /*alloc fail*/); - } else { - addLengthDistance(out, length, offset); - for(i = 1; i < length; ++i) { - ++pos; - wpos = pos & (windowsize - 1); - hashval = getHash(in, insize, pos); - if(usezeros && hashval == 0) { - if(numzeros == 0) numzeros = countZeros(in, insize, pos); - else if(pos + numzeros > insize || in[pos + numzeros - 1] != 0) --numzeros; - } else { - numzeros = 0; - } - updateHashChain(hash, wpos, hashval, numzeros); - } - } - } /*end of the loop through each character of input*/ - - return error; -} - -/* /////////////////////////////////////////////////////////////////////////// */ - -static unsigned deflateNoCompression(ucvector* out, const unsigned char* data, size_t datasize) { - /*non compressed deflate block data: 1 bit BFINAL,2 bits BTYPE,(5 bits): it jumps to start of next byte, - 2 bytes LEN, 2 bytes NLEN, LEN bytes literal DATA*/ - - size_t i, numdeflateblocks = (datasize + 65534u) / 65535u; - size_t datapos = 0; - for(i = 0; i != numdeflateblocks; ++i) { - unsigned BFINAL, BTYPE, LEN, NLEN; - unsigned char firstbyte; - size_t pos = out->size; - - BFINAL = (i == numdeflateblocks - 1); - BTYPE = 0; - - LEN = 65535; - if(datasize - datapos < 65535u) LEN = (unsigned)datasize - (unsigned)datapos; - NLEN = 65535 - LEN; - - if(!ucvector_resize(out, out->size + LEN + 5)) return 83; /*alloc fail*/ - - firstbyte = (unsigned char)(BFINAL + ((BTYPE & 1u) << 1u) + ((BTYPE & 2u) << 1u)); - out->data[pos + 0] = firstbyte; - out->data[pos + 1] = (unsigned char)(LEN & 255); - out->data[pos + 2] = (unsigned char)(LEN >> 8u); - out->data[pos + 3] = (unsigned char)(NLEN & 255); - out->data[pos + 4] = (unsigned char)(NLEN >> 8u); - lodepng_memcpy(out->data + pos + 5, data + datapos, LEN); - datapos += LEN; - } - - return 0; -} - -/* -write the lz77-encoded data, which has lit, len and dist codes, to compressed stream using huffman trees. -tree_ll: the tree for lit and len codes. -tree_d: the tree for distance codes. -*/ -static void writeLZ77data(LodePNGBitWriter* writer, const uivector* lz77_encoded, - const HuffmanTree* tree_ll, const HuffmanTree* tree_d) { - size_t i = 0; - for(i = 0; i != lz77_encoded->size; ++i) { - unsigned val = lz77_encoded->data[i]; - writeBitsReversed(writer, tree_ll->codes[val], tree_ll->lengths[val]); - if(val > 256) /*for a length code, 3 more things have to be added*/ { - unsigned length_index = val - FIRST_LENGTH_CODE_INDEX; - unsigned n_length_extra_bits = LENGTHEXTRA[length_index]; - unsigned length_extra_bits = lz77_encoded->data[++i]; - - unsigned distance_code = lz77_encoded->data[++i]; - - unsigned distance_index = distance_code; - unsigned n_distance_extra_bits = DISTANCEEXTRA[distance_index]; - unsigned distance_extra_bits = lz77_encoded->data[++i]; - - writeBits(writer, length_extra_bits, n_length_extra_bits); - writeBitsReversed(writer, tree_d->codes[distance_code], tree_d->lengths[distance_code]); - writeBits(writer, distance_extra_bits, n_distance_extra_bits); - } - } -} - -/*Deflate for a block of type "dynamic", that is, with freely, optimally, created huffman trees*/ -static unsigned deflateDynamic(LodePNGBitWriter* writer, Hash* hash, - const unsigned char* data, size_t datapos, size_t dataend, - const LodePNGCompressSettings* settings, unsigned final) { - unsigned error = 0; - - /* - A block is compressed as follows: The PNG data is lz77 encoded, resulting in - literal bytes and length/distance pairs. This is then huffman compressed with - two huffman trees. One huffman tree is used for the lit and len values ("ll"), - another huffman tree is used for the dist values ("d"). These two trees are - stored using their code lengths, and to compress even more these code lengths - are also run-length encoded and huffman compressed. This gives a huffman tree - of code lengths "cl". The code lengths used to describe this third tree are - the code length code lengths ("clcl"). - */ - - /*The lz77 encoded data, represented with integers since there will also be length and distance codes in it*/ - uivector lz77_encoded; - HuffmanTree tree_ll; /*tree for lit,len values*/ - HuffmanTree tree_d; /*tree for distance codes*/ - HuffmanTree tree_cl; /*tree for encoding the code lengths representing tree_ll and tree_d*/ - unsigned* frequencies_ll = 0; /*frequency of lit,len codes*/ - unsigned* frequencies_d = 0; /*frequency of dist codes*/ - unsigned* frequencies_cl = 0; /*frequency of code length codes*/ - unsigned* bitlen_lld = 0; /*lit,len,dist code lengths (int bits), literally (without repeat codes).*/ - unsigned* bitlen_lld_e = 0; /*bitlen_lld encoded with repeat codes (this is a rudimentary run length compression)*/ - size_t datasize = dataend - datapos; - - /* - If we could call "bitlen_cl" the the code length code lengths ("clcl"), that is the bit lengths of codes to represent - tree_cl in CLCL_ORDER, then due to the huffman compression of huffman tree representations ("two levels"), there are - some analogies: - bitlen_lld is to tree_cl what data is to tree_ll and tree_d. - bitlen_lld_e is to bitlen_lld what lz77_encoded is to data. - bitlen_cl is to bitlen_lld_e what bitlen_lld is to lz77_encoded. - */ - - unsigned BFINAL = final; - size_t i; - size_t numcodes_ll, numcodes_d, numcodes_lld, numcodes_lld_e, numcodes_cl; - unsigned HLIT, HDIST, HCLEN; - - uivector_init(&lz77_encoded); - HuffmanTree_init(&tree_ll); - HuffmanTree_init(&tree_d); - HuffmanTree_init(&tree_cl); - /* could fit on stack, but >1KB is on the larger side so allocate instead */ - frequencies_ll = (unsigned*)lodepng_malloc(286 * sizeof(*frequencies_ll)); - frequencies_d = (unsigned*)lodepng_malloc(30 * sizeof(*frequencies_d)); - frequencies_cl = (unsigned*)lodepng_malloc(NUM_CODE_LENGTH_CODES * sizeof(*frequencies_cl)); - - if(!frequencies_ll || !frequencies_d || !frequencies_cl) error = 83; /*alloc fail*/ - - /*This while loop never loops due to a break at the end, it is here to - allow breaking out of it to the cleanup phase on error conditions.*/ - while(!error) { - lodepng_memset(frequencies_ll, 0, 286 * sizeof(*frequencies_ll)); - lodepng_memset(frequencies_d, 0, 30 * sizeof(*frequencies_d)); - lodepng_memset(frequencies_cl, 0, NUM_CODE_LENGTH_CODES * sizeof(*frequencies_cl)); - - if(settings->use_lz77) { - error = encodeLZ77(&lz77_encoded, hash, data, datapos, dataend, settings->windowsize, - settings->minmatch, settings->nicematch, settings->lazymatching); - if(error) break; - } else { - if(!uivector_resize(&lz77_encoded, datasize)) ERROR_BREAK(83 /*alloc fail*/); - for(i = datapos; i < dataend; ++i) lz77_encoded.data[i - datapos] = data[i]; /*no LZ77, but still will be Huffman compressed*/ - } - - /*Count the frequencies of lit, len and dist codes*/ - for(i = 0; i != lz77_encoded.size; ++i) { - unsigned symbol = lz77_encoded.data[i]; - ++frequencies_ll[symbol]; - if(symbol > 256) { - unsigned dist = lz77_encoded.data[i + 2]; - ++frequencies_d[dist]; - i += 3; - } - } - frequencies_ll[256] = 1; /*there will be exactly 1 end code, at the end of the block*/ - - /*Make both huffman trees, one for the lit and len codes, one for the dist codes*/ - error = HuffmanTree_makeFromFrequencies(&tree_ll, frequencies_ll, 257, 286, 15); - if(error) break; - /*2, not 1, is chosen for mincodes: some buggy PNG decoders require at least 2 symbols in the dist tree*/ - error = HuffmanTree_makeFromFrequencies(&tree_d, frequencies_d, 2, 30, 15); - if(error) break; - - numcodes_ll = LODEPNG_MIN(tree_ll.numcodes, 286); - numcodes_d = LODEPNG_MIN(tree_d.numcodes, 30); - /*store the code lengths of both generated trees in bitlen_lld*/ - numcodes_lld = numcodes_ll + numcodes_d; - bitlen_lld = (unsigned*)lodepng_malloc(numcodes_lld * sizeof(*bitlen_lld)); - /*numcodes_lld_e never needs more size than bitlen_lld*/ - bitlen_lld_e = (unsigned*)lodepng_malloc(numcodes_lld * sizeof(*bitlen_lld_e)); - if(!bitlen_lld || !bitlen_lld_e) ERROR_BREAK(83); /*alloc fail*/ - numcodes_lld_e = 0; - - for(i = 0; i != numcodes_ll; ++i) bitlen_lld[i] = tree_ll.lengths[i]; - for(i = 0; i != numcodes_d; ++i) bitlen_lld[numcodes_ll + i] = tree_d.lengths[i]; - - /*run-length compress bitlen_ldd into bitlen_lld_e by using repeat codes 16 (copy length 3-6 times), - 17 (3-10 zeroes), 18 (11-138 zeroes)*/ - for(i = 0; i != numcodes_lld; ++i) { - unsigned j = 0; /*amount of repetitions*/ - while(i + j + 1 < numcodes_lld && bitlen_lld[i + j + 1] == bitlen_lld[i]) ++j; - - if(bitlen_lld[i] == 0 && j >= 2) /*repeat code for zeroes*/ { - ++j; /*include the first zero*/ - if(j <= 10) /*repeat code 17 supports max 10 zeroes*/ { - bitlen_lld_e[numcodes_lld_e++] = 17; - bitlen_lld_e[numcodes_lld_e++] = j - 3; - } else /*repeat code 18 supports max 138 zeroes*/ { - if(j > 138) j = 138; - bitlen_lld_e[numcodes_lld_e++] = 18; - bitlen_lld_e[numcodes_lld_e++] = j - 11; - } - i += (j - 1); - } else if(j >= 3) /*repeat code for value other than zero*/ { - size_t k; - unsigned num = j / 6u, rest = j % 6u; - bitlen_lld_e[numcodes_lld_e++] = bitlen_lld[i]; - for(k = 0; k < num; ++k) { - bitlen_lld_e[numcodes_lld_e++] = 16; - bitlen_lld_e[numcodes_lld_e++] = 6 - 3; - } - if(rest >= 3) { - bitlen_lld_e[numcodes_lld_e++] = 16; - bitlen_lld_e[numcodes_lld_e++] = rest - 3; - } - else j -= rest; - i += j; - } else /*too short to benefit from repeat code*/ { - bitlen_lld_e[numcodes_lld_e++] = bitlen_lld[i]; - } - } - - /*generate tree_cl, the huffmantree of huffmantrees*/ - for(i = 0; i != numcodes_lld_e; ++i) { - ++frequencies_cl[bitlen_lld_e[i]]; - /*after a repeat code come the bits that specify the number of repetitions, - those don't need to be in the frequencies_cl calculation*/ - if(bitlen_lld_e[i] >= 16) ++i; - } - - error = HuffmanTree_makeFromFrequencies(&tree_cl, frequencies_cl, - NUM_CODE_LENGTH_CODES, NUM_CODE_LENGTH_CODES, 7); - if(error) break; - - /*compute amount of code-length-code-lengths to output*/ - numcodes_cl = NUM_CODE_LENGTH_CODES; - /*trim zeros at the end (using CLCL_ORDER), but minimum size must be 4 (see HCLEN below)*/ - while(numcodes_cl > 4u && tree_cl.lengths[CLCL_ORDER[numcodes_cl - 1u]] == 0) { - numcodes_cl--; - } - - /* - Write everything into the output - - After the BFINAL and BTYPE, the dynamic block consists out of the following: - - 5 bits HLIT, 5 bits HDIST, 4 bits HCLEN - - (HCLEN+4)*3 bits code lengths of code length alphabet - - HLIT + 257 code lengths of lit/length alphabet (encoded using the code length - alphabet, + possible repetition codes 16, 17, 18) - - HDIST + 1 code lengths of distance alphabet (encoded using the code length - alphabet, + possible repetition codes 16, 17, 18) - - compressed data - - 256 (end code) - */ - - /*Write block type*/ - writeBits(writer, BFINAL, 1); - writeBits(writer, 0, 1); /*first bit of BTYPE "dynamic"*/ - writeBits(writer, 1, 1); /*second bit of BTYPE "dynamic"*/ - - /*write the HLIT, HDIST and HCLEN values*/ - /*all three sizes take trimmed ending zeroes into account, done either by HuffmanTree_makeFromFrequencies - or in the loop for numcodes_cl above, which saves space. */ - HLIT = (unsigned)(numcodes_ll - 257); - HDIST = (unsigned)(numcodes_d - 1); - HCLEN = (unsigned)(numcodes_cl - 4); - writeBits(writer, HLIT, 5); - writeBits(writer, HDIST, 5); - writeBits(writer, HCLEN, 4); - - /*write the code lengths of the code length alphabet ("bitlen_cl")*/ - for(i = 0; i != numcodes_cl; ++i) writeBits(writer, tree_cl.lengths[CLCL_ORDER[i]], 3); - - /*write the lengths of the lit/len AND the dist alphabet*/ - for(i = 0; i != numcodes_lld_e; ++i) { - writeBitsReversed(writer, tree_cl.codes[bitlen_lld_e[i]], tree_cl.lengths[bitlen_lld_e[i]]); - /*extra bits of repeat codes*/ - if(bitlen_lld_e[i] == 16) writeBits(writer, bitlen_lld_e[++i], 2); - else if(bitlen_lld_e[i] == 17) writeBits(writer, bitlen_lld_e[++i], 3); - else if(bitlen_lld_e[i] == 18) writeBits(writer, bitlen_lld_e[++i], 7); - } - - /*write the compressed data symbols*/ - writeLZ77data(writer, &lz77_encoded, &tree_ll, &tree_d); - /*error: the length of the end code 256 must be larger than 0*/ - if(tree_ll.lengths[256] == 0) ERROR_BREAK(64); - - /*write the end code*/ - writeBitsReversed(writer, tree_ll.codes[256], tree_ll.lengths[256]); - - break; /*end of error-while*/ - } - - /*cleanup*/ - uivector_cleanup(&lz77_encoded); - HuffmanTree_cleanup(&tree_ll); - HuffmanTree_cleanup(&tree_d); - HuffmanTree_cleanup(&tree_cl); - lodepng_free(frequencies_ll); - lodepng_free(frequencies_d); - lodepng_free(frequencies_cl); - lodepng_free(bitlen_lld); - lodepng_free(bitlen_lld_e); - - return error; -} - -static unsigned deflateFixed(LodePNGBitWriter* writer, Hash* hash, - const unsigned char* data, - size_t datapos, size_t dataend, - const LodePNGCompressSettings* settings, unsigned final) { - HuffmanTree tree_ll; /*tree for literal values and length codes*/ - HuffmanTree tree_d; /*tree for distance codes*/ - - unsigned BFINAL = final; - unsigned error = 0; - size_t i; - - HuffmanTree_init(&tree_ll); - HuffmanTree_init(&tree_d); - - error = generateFixedLitLenTree(&tree_ll); - if(!error) error = generateFixedDistanceTree(&tree_d); - - if(!error) { - writeBits(writer, BFINAL, 1); - writeBits(writer, 1, 1); /*first bit of BTYPE*/ - writeBits(writer, 0, 1); /*second bit of BTYPE*/ - - if(settings->use_lz77) /*LZ77 encoded*/ { - uivector lz77_encoded; - uivector_init(&lz77_encoded); - error = encodeLZ77(&lz77_encoded, hash, data, datapos, dataend, settings->windowsize, - settings->minmatch, settings->nicematch, settings->lazymatching); - if(!error) writeLZ77data(writer, &lz77_encoded, &tree_ll, &tree_d); - uivector_cleanup(&lz77_encoded); - } else /*no LZ77, but still will be Huffman compressed*/ { - for(i = datapos; i < dataend; ++i) { - writeBitsReversed(writer, tree_ll.codes[data[i]], tree_ll.lengths[data[i]]); - } - } - /*add END code*/ - if(!error) writeBitsReversed(writer,tree_ll.codes[256], tree_ll.lengths[256]); - } - - /*cleanup*/ - HuffmanTree_cleanup(&tree_ll); - HuffmanTree_cleanup(&tree_d); - - return error; -} - -static unsigned lodepng_deflatev(ucvector* out, const unsigned char* in, size_t insize, - const LodePNGCompressSettings* settings) { - unsigned error = 0; - size_t i, blocksize, numdeflateblocks; - Hash hash; - LodePNGBitWriter writer; - - LodePNGBitWriter_init(&writer, out); - - if(settings->btype > 2) return 61; - else if(settings->btype == 0) return deflateNoCompression(out, in, insize); - else if(settings->btype == 1) blocksize = insize; - else /*if(settings->btype == 2)*/ { - /*on PNGs, deflate blocks of 65-262k seem to give most dense encoding*/ - blocksize = insize / 8u + 8; - if(blocksize < 65536) blocksize = 65536; - if(blocksize > 262144) blocksize = 262144; - } - - numdeflateblocks = (insize + blocksize - 1) / blocksize; - if(numdeflateblocks == 0) numdeflateblocks = 1; - - error = hash_init(&hash, settings->windowsize); - - if(!error) { - for(i = 0; i != numdeflateblocks && !error; ++i) { - unsigned final = (i == numdeflateblocks - 1); - size_t start = i * blocksize; - size_t end = start + blocksize; - if(end > insize) end = insize; - - if(settings->btype == 1) error = deflateFixed(&writer, &hash, in, start, end, settings, final); - else if(settings->btype == 2) error = deflateDynamic(&writer, &hash, in, start, end, settings, final); - } - } - - hash_cleanup(&hash); - - return error; -} - -unsigned lodepng_deflate(unsigned char** out, size_t* outsize, - const unsigned char* in, size_t insize, - const LodePNGCompressSettings* settings) { - ucvector v = ucvector_init(*out, *outsize); - unsigned error = lodepng_deflatev(&v, in, insize, settings); - *out = v.data; - *outsize = v.size; - return error; -} - -static unsigned deflate(unsigned char** out, size_t* outsize, - const unsigned char* in, size_t insize, - const LodePNGCompressSettings* settings) { - if(settings->custom_deflate) { - unsigned error = settings->custom_deflate(out, outsize, in, insize, settings); - /*the custom deflate is allowed to have its own error codes, however, we translate it to code 111*/ - return error ? 111 : 0; - } else { - return lodepng_deflate(out, outsize, in, insize, settings); - } -} - -#endif /*LODEPNG_COMPILE_DECODER*/ - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / Adler32 / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -static unsigned update_adler32(unsigned adler, const unsigned char* data, unsigned len) { - unsigned s1 = adler & 0xffffu; - unsigned s2 = (adler >> 16u) & 0xffffu; - - while(len != 0u) { - unsigned i; - /*at least 5552 sums can be done before the sums overflow, saving a lot of module divisions*/ - unsigned amount = len > 5552u ? 5552u : len; - len -= amount; - for(i = 0; i != amount; ++i) { - s1 += (*data++); - s2 += s1; - } - s1 %= 65521u; - s2 %= 65521u; - } - - return (s2 << 16u) | s1; -} - -/*Return the adler32 of the bytes data[0..len-1]*/ -static unsigned adler32(const unsigned char* data, unsigned len) { - return update_adler32(1u, data, len); -} - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / Zlib / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -#ifdef LODEPNG_COMPILE_DECODER - -static unsigned lodepng_zlib_decompressv(ucvector* out, - const unsigned char* in, size_t insize, - const LodePNGDecompressSettings* settings) { - unsigned error = 0; - unsigned CM, CINFO, FDICT; - - if(insize < 2) return 53; /*error, size of zlib data too small*/ - /*read information from zlib header*/ - if((in[0] * 256 + in[1]) % 31 != 0) { - /*error: 256 * in[0] + in[1] must be a multiple of 31, the FCHECK value is supposed to be made that way*/ - return 24; - } - - CM = in[0] & 15; - CINFO = (in[0] >> 4) & 15; - /*FCHECK = in[1] & 31;*/ /*FCHECK is already tested above*/ - FDICT = (in[1] >> 5) & 1; - /*FLEVEL = (in[1] >> 6) & 3;*/ /*FLEVEL is not used here*/ - - if(CM != 8 || CINFO > 7) { - /*error: only compression method 8: inflate with sliding window of 32k is supported by the PNG spec*/ - return 25; - } - if(FDICT != 0) { - /*error: the specification of PNG says about the zlib stream: - "The additional flags shall not specify a preset dictionary."*/ - return 26; - } - - error = inflatev(out, in + 2, insize - 2, settings); - if(error) return error; - - if(!settings->ignore_adler32) { - unsigned ADLER32 = lodepng_read32bitInt(&in[insize - 4]); - unsigned checksum = adler32(out->data, (unsigned)(out->size)); - if(checksum != ADLER32) return 58; /*error, adler checksum not correct, data must be corrupted*/ - } - - return 0; /*no error*/ -} - - -unsigned lodepng_zlib_decompress(unsigned char** out, size_t* outsize, const unsigned char* in, - size_t insize, const LodePNGDecompressSettings* settings) { - ucvector v = ucvector_init(*out, *outsize); - unsigned error = lodepng_zlib_decompressv(&v, in, insize, settings); - *out = v.data; - *outsize = v.size; - return error; -} - -/*expected_size is expected output size, to avoid intermediate allocations. Set to 0 if not known. */ -static unsigned zlib_decompress(unsigned char** out, size_t* outsize, size_t expected_size, - const unsigned char* in, size_t insize, const LodePNGDecompressSettings* settings) { - unsigned error; - if(settings->custom_zlib) { - error = settings->custom_zlib(out, outsize, in, insize, settings); - if(error) { - /*the custom zlib is allowed to have its own error codes, however, we translate it to code 110*/ - error = 110; - /*if there's a max output size, and the custom zlib returned error, then indicate that error instead*/ - if(settings->max_output_size && *outsize > settings->max_output_size) error = 109; - } - } else { - ucvector v = ucvector_init(*out, *outsize); - if(expected_size) { - /*reserve the memory to avoid intermediate reallocations*/ - ucvector_resize(&v, *outsize + expected_size); - v.size = *outsize; - } - error = lodepng_zlib_decompressv(&v, in, insize, settings); - *out = v.data; - *outsize = v.size; - } - return error; -} - -#endif /*LODEPNG_COMPILE_DECODER*/ - -#ifdef LODEPNG_COMPILE_ENCODER - -unsigned lodepng_zlib_compress(unsigned char** out, size_t* outsize, const unsigned char* in, - size_t insize, const LodePNGCompressSettings* settings) { - size_t i; - unsigned error; - unsigned char* deflatedata = 0; - size_t deflatesize = 0; - - error = deflate(&deflatedata, &deflatesize, in, insize, settings); - - *out = NULL; - *outsize = 0; - if(!error) { - *outsize = deflatesize + 6; - *out = (unsigned char*)lodepng_malloc(*outsize); - if(!*out) error = 83; /*alloc fail*/ - } - - if(!error) { - unsigned ADLER32 = adler32(in, (unsigned)insize); - /*zlib data: 1 byte CMF (CM+CINFO), 1 byte FLG, deflate data, 4 byte ADLER32 checksum of the Decompressed data*/ - unsigned CMF = 120; /*0b01111000: CM 8, CINFO 7. With CINFO 7, any window size up to 32768 can be used.*/ - unsigned FLEVEL = 0; - unsigned FDICT = 0; - unsigned CMFFLG = 256 * CMF + FDICT * 32 + FLEVEL * 64; - unsigned FCHECK = 31 - CMFFLG % 31; - CMFFLG += FCHECK; - - (*out)[0] = (unsigned char)(CMFFLG >> 8); - (*out)[1] = (unsigned char)(CMFFLG & 255); - for(i = 0; i != deflatesize; ++i) (*out)[i + 2] = deflatedata[i]; - lodepng_set32bitInt(&(*out)[*outsize - 4], ADLER32); - } - - lodepng_free(deflatedata); - return error; -} - -/* compress using the default or custom zlib function */ -static unsigned zlib_compress(unsigned char** out, size_t* outsize, const unsigned char* in, - size_t insize, const LodePNGCompressSettings* settings) { - if(settings->custom_zlib) { - unsigned error = settings->custom_zlib(out, outsize, in, insize, settings); - /*the custom zlib is allowed to have its own error codes, however, we translate it to code 111*/ - return error ? 111 : 0; - } else { - return lodepng_zlib_compress(out, outsize, in, insize, settings); - } -} - -#endif /*LODEPNG_COMPILE_ENCODER*/ - -#else /*no LODEPNG_COMPILE_ZLIB*/ - -#ifdef LODEPNG_COMPILE_DECODER -static unsigned zlib_decompress(unsigned char** out, size_t* outsize, size_t expected_size, - const unsigned char* in, size_t insize, const LodePNGDecompressSettings* settings) { - if(!settings->custom_zlib) return 87; /*no custom zlib function provided */ - (void)expected_size; - return settings->custom_zlib(out, outsize, in, insize, settings); -} -#endif /*LODEPNG_COMPILE_DECODER*/ -#ifdef LODEPNG_COMPILE_ENCODER -static unsigned zlib_compress(unsigned char** out, size_t* outsize, const unsigned char* in, - size_t insize, const LodePNGCompressSettings* settings) { - if(!settings->custom_zlib) return 87; /*no custom zlib function provided */ - return settings->custom_zlib(out, outsize, in, insize, settings); -} -#endif /*LODEPNG_COMPILE_ENCODER*/ - -#endif /*LODEPNG_COMPILE_ZLIB*/ - -/* ////////////////////////////////////////////////////////////////////////// */ - -#ifdef LODEPNG_COMPILE_ENCODER - -/*this is a good tradeoff between speed and compression ratio*/ -#define DEFAULT_WINDOWSIZE 2048 - -void lodepng_compress_settings_init(LodePNGCompressSettings* settings) { - /*compress with dynamic huffman tree (not in the mathematical sense, just not the predefined one)*/ - settings->btype = 2; - settings->use_lz77 = 1; - settings->windowsize = DEFAULT_WINDOWSIZE; - settings->minmatch = 3; - settings->nicematch = 128; - settings->lazymatching = 1; - - settings->custom_zlib = 0; - settings->custom_deflate = 0; - settings->custom_context = 0; -} - -const LodePNGCompressSettings lodepng_default_compress_settings = {2, 1, DEFAULT_WINDOWSIZE, 3, 128, 1, 0, 0, 0}; - - -#endif /*LODEPNG_COMPILE_ENCODER*/ - -#ifdef LODEPNG_COMPILE_DECODER - -void lodepng_decompress_settings_init(LodePNGDecompressSettings* settings) { - settings->ignore_adler32 = 0; - settings->ignore_nlen = 0; - settings->max_output_size = 0; - - settings->custom_zlib = 0; - settings->custom_inflate = 0; - settings->custom_context = 0; -} - -const LodePNGDecompressSettings lodepng_default_decompress_settings = {0, 0, 0, 0, 0, 0}; - -#endif /*LODEPNG_COMPILE_DECODER*/ - -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* // End of Zlib related code. Begin of PNG related code. // */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ - -#ifdef LODEPNG_COMPILE_PNG - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / CRC32 / */ -/* ////////////////////////////////////////////////////////////////////////// */ - - -#ifdef LODEPNG_COMPILE_CRC - -static const unsigned lodepng_crc32_table0[256] = { - 0x00000000u, 0x77073096u, 0xee0e612cu, 0x990951bau, 0x076dc419u, 0x706af48fu, 0xe963a535u, 0x9e6495a3u, - 0x0edb8832u, 0x79dcb8a4u, 0xe0d5e91eu, 0x97d2d988u, 0x09b64c2bu, 0x7eb17cbdu, 0xe7b82d07u, 0x90bf1d91u, - 0x1db71064u, 0x6ab020f2u, 0xf3b97148u, 0x84be41deu, 0x1adad47du, 0x6ddde4ebu, 0xf4d4b551u, 0x83d385c7u, - 0x136c9856u, 0x646ba8c0u, 0xfd62f97au, 0x8a65c9ecu, 0x14015c4fu, 0x63066cd9u, 0xfa0f3d63u, 0x8d080df5u, - 0x3b6e20c8u, 0x4c69105eu, 0xd56041e4u, 0xa2677172u, 0x3c03e4d1u, 0x4b04d447u, 0xd20d85fdu, 0xa50ab56bu, - 0x35b5a8fau, 0x42b2986cu, 0xdbbbc9d6u, 0xacbcf940u, 0x32d86ce3u, 0x45df5c75u, 0xdcd60dcfu, 0xabd13d59u, - 0x26d930acu, 0x51de003au, 0xc8d75180u, 0xbfd06116u, 0x21b4f4b5u, 0x56b3c423u, 0xcfba9599u, 0xb8bda50fu, - 0x2802b89eu, 0x5f058808u, 0xc60cd9b2u, 0xb10be924u, 0x2f6f7c87u, 0x58684c11u, 0xc1611dabu, 0xb6662d3du, - 0x76dc4190u, 0x01db7106u, 0x98d220bcu, 0xefd5102au, 0x71b18589u, 0x06b6b51fu, 0x9fbfe4a5u, 0xe8b8d433u, - 0x7807c9a2u, 0x0f00f934u, 0x9609a88eu, 0xe10e9818u, 0x7f6a0dbbu, 0x086d3d2du, 0x91646c97u, 0xe6635c01u, - 0x6b6b51f4u, 0x1c6c6162u, 0x856530d8u, 0xf262004eu, 0x6c0695edu, 0x1b01a57bu, 0x8208f4c1u, 0xf50fc457u, - 0x65b0d9c6u, 0x12b7e950u, 0x8bbeb8eau, 0xfcb9887cu, 0x62dd1ddfu, 0x15da2d49u, 0x8cd37cf3u, 0xfbd44c65u, - 0x4db26158u, 0x3ab551ceu, 0xa3bc0074u, 0xd4bb30e2u, 0x4adfa541u, 0x3dd895d7u, 0xa4d1c46du, 0xd3d6f4fbu, - 0x4369e96au, 0x346ed9fcu, 0xad678846u, 0xda60b8d0u, 0x44042d73u, 0x33031de5u, 0xaa0a4c5fu, 0xdd0d7cc9u, - 0x5005713cu, 0x270241aau, 0xbe0b1010u, 0xc90c2086u, 0x5768b525u, 0x206f85b3u, 0xb966d409u, 0xce61e49fu, - 0x5edef90eu, 0x29d9c998u, 0xb0d09822u, 0xc7d7a8b4u, 0x59b33d17u, 0x2eb40d81u, 0xb7bd5c3bu, 0xc0ba6cadu, - 0xedb88320u, 0x9abfb3b6u, 0x03b6e20cu, 0x74b1d29au, 0xead54739u, 0x9dd277afu, 0x04db2615u, 0x73dc1683u, - 0xe3630b12u, 0x94643b84u, 0x0d6d6a3eu, 0x7a6a5aa8u, 0xe40ecf0bu, 0x9309ff9du, 0x0a00ae27u, 0x7d079eb1u, - 0xf00f9344u, 0x8708a3d2u, 0x1e01f268u, 0x6906c2feu, 0xf762575du, 0x806567cbu, 0x196c3671u, 0x6e6b06e7u, - 0xfed41b76u, 0x89d32be0u, 0x10da7a5au, 0x67dd4accu, 0xf9b9df6fu, 0x8ebeeff9u, 0x17b7be43u, 0x60b08ed5u, - 0xd6d6a3e8u, 0xa1d1937eu, 0x38d8c2c4u, 0x4fdff252u, 0xd1bb67f1u, 0xa6bc5767u, 0x3fb506ddu, 0x48b2364bu, - 0xd80d2bdau, 0xaf0a1b4cu, 0x36034af6u, 0x41047a60u, 0xdf60efc3u, 0xa867df55u, 0x316e8eefu, 0x4669be79u, - 0xcb61b38cu, 0xbc66831au, 0x256fd2a0u, 0x5268e236u, 0xcc0c7795u, 0xbb0b4703u, 0x220216b9u, 0x5505262fu, - 0xc5ba3bbeu, 0xb2bd0b28u, 0x2bb45a92u, 0x5cb36a04u, 0xc2d7ffa7u, 0xb5d0cf31u, 0x2cd99e8bu, 0x5bdeae1du, - 0x9b64c2b0u, 0xec63f226u, 0x756aa39cu, 0x026d930au, 0x9c0906a9u, 0xeb0e363fu, 0x72076785u, 0x05005713u, - 0x95bf4a82u, 0xe2b87a14u, 0x7bb12baeu, 0x0cb61b38u, 0x92d28e9bu, 0xe5d5be0du, 0x7cdcefb7u, 0x0bdbdf21u, - 0x86d3d2d4u, 0xf1d4e242u, 0x68ddb3f8u, 0x1fda836eu, 0x81be16cdu, 0xf6b9265bu, 0x6fb077e1u, 0x18b74777u, - 0x88085ae6u, 0xff0f6a70u, 0x66063bcau, 0x11010b5cu, 0x8f659effu, 0xf862ae69u, 0x616bffd3u, 0x166ccf45u, - 0xa00ae278u, 0xd70dd2eeu, 0x4e048354u, 0x3903b3c2u, 0xa7672661u, 0xd06016f7u, 0x4969474du, 0x3e6e77dbu, - 0xaed16a4au, 0xd9d65adcu, 0x40df0b66u, 0x37d83bf0u, 0xa9bcae53u, 0xdebb9ec5u, 0x47b2cf7fu, 0x30b5ffe9u, - 0xbdbdf21cu, 0xcabac28au, 0x53b39330u, 0x24b4a3a6u, 0xbad03605u, 0xcdd70693u, 0x54de5729u, 0x23d967bfu, - 0xb3667a2eu, 0xc4614ab8u, 0x5d681b02u, 0x2a6f2b94u, 0xb40bbe37u, 0xc30c8ea1u, 0x5a05df1bu, 0x2d02ef8du -}; - -static const unsigned lodepng_crc32_table1[256] = { - 0x00000000u, 0x191b3141u, 0x32366282u, 0x2b2d53c3u, 0x646cc504u, 0x7d77f445u, 0x565aa786u, 0x4f4196c7u, - 0xc8d98a08u, 0xd1c2bb49u, 0xfaefe88au, 0xe3f4d9cbu, 0xacb54f0cu, 0xb5ae7e4du, 0x9e832d8eu, 0x87981ccfu, - 0x4ac21251u, 0x53d92310u, 0x78f470d3u, 0x61ef4192u, 0x2eaed755u, 0x37b5e614u, 0x1c98b5d7u, 0x05838496u, - 0x821b9859u, 0x9b00a918u, 0xb02dfadbu, 0xa936cb9au, 0xe6775d5du, 0xff6c6c1cu, 0xd4413fdfu, 0xcd5a0e9eu, - 0x958424a2u, 0x8c9f15e3u, 0xa7b24620u, 0xbea97761u, 0xf1e8e1a6u, 0xe8f3d0e7u, 0xc3de8324u, 0xdac5b265u, - 0x5d5daeaau, 0x44469febu, 0x6f6bcc28u, 0x7670fd69u, 0x39316baeu, 0x202a5aefu, 0x0b07092cu, 0x121c386du, - 0xdf4636f3u, 0xc65d07b2u, 0xed705471u, 0xf46b6530u, 0xbb2af3f7u, 0xa231c2b6u, 0x891c9175u, 0x9007a034u, - 0x179fbcfbu, 0x0e848dbau, 0x25a9de79u, 0x3cb2ef38u, 0x73f379ffu, 0x6ae848beu, 0x41c51b7du, 0x58de2a3cu, - 0xf0794f05u, 0xe9627e44u, 0xc24f2d87u, 0xdb541cc6u, 0x94158a01u, 0x8d0ebb40u, 0xa623e883u, 0xbf38d9c2u, - 0x38a0c50du, 0x21bbf44cu, 0x0a96a78fu, 0x138d96ceu, 0x5ccc0009u, 0x45d73148u, 0x6efa628bu, 0x77e153cau, - 0xbabb5d54u, 0xa3a06c15u, 0x888d3fd6u, 0x91960e97u, 0xded79850u, 0xc7cca911u, 0xece1fad2u, 0xf5facb93u, - 0x7262d75cu, 0x6b79e61du, 0x4054b5deu, 0x594f849fu, 0x160e1258u, 0x0f152319u, 0x243870dau, 0x3d23419bu, - 0x65fd6ba7u, 0x7ce65ae6u, 0x57cb0925u, 0x4ed03864u, 0x0191aea3u, 0x188a9fe2u, 0x33a7cc21u, 0x2abcfd60u, - 0xad24e1afu, 0xb43fd0eeu, 0x9f12832du, 0x8609b26cu, 0xc94824abu, 0xd05315eau, 0xfb7e4629u, 0xe2657768u, - 0x2f3f79f6u, 0x362448b7u, 0x1d091b74u, 0x04122a35u, 0x4b53bcf2u, 0x52488db3u, 0x7965de70u, 0x607eef31u, - 0xe7e6f3feu, 0xfefdc2bfu, 0xd5d0917cu, 0xcccba03du, 0x838a36fau, 0x9a9107bbu, 0xb1bc5478u, 0xa8a76539u, - 0x3b83984bu, 0x2298a90au, 0x09b5fac9u, 0x10aecb88u, 0x5fef5d4fu, 0x46f46c0eu, 0x6dd93fcdu, 0x74c20e8cu, - 0xf35a1243u, 0xea412302u, 0xc16c70c1u, 0xd8774180u, 0x9736d747u, 0x8e2de606u, 0xa500b5c5u, 0xbc1b8484u, - 0x71418a1au, 0x685abb5bu, 0x4377e898u, 0x5a6cd9d9u, 0x152d4f1eu, 0x0c367e5fu, 0x271b2d9cu, 0x3e001cddu, - 0xb9980012u, 0xa0833153u, 0x8bae6290u, 0x92b553d1u, 0xddf4c516u, 0xc4eff457u, 0xefc2a794u, 0xf6d996d5u, - 0xae07bce9u, 0xb71c8da8u, 0x9c31de6bu, 0x852aef2au, 0xca6b79edu, 0xd37048acu, 0xf85d1b6fu, 0xe1462a2eu, - 0x66de36e1u, 0x7fc507a0u, 0x54e85463u, 0x4df36522u, 0x02b2f3e5u, 0x1ba9c2a4u, 0x30849167u, 0x299fa026u, - 0xe4c5aeb8u, 0xfdde9ff9u, 0xd6f3cc3au, 0xcfe8fd7bu, 0x80a96bbcu, 0x99b25afdu, 0xb29f093eu, 0xab84387fu, - 0x2c1c24b0u, 0x350715f1u, 0x1e2a4632u, 0x07317773u, 0x4870e1b4u, 0x516bd0f5u, 0x7a468336u, 0x635db277u, - 0xcbfad74eu, 0xd2e1e60fu, 0xf9ccb5ccu, 0xe0d7848du, 0xaf96124au, 0xb68d230bu, 0x9da070c8u, 0x84bb4189u, - 0x03235d46u, 0x1a386c07u, 0x31153fc4u, 0x280e0e85u, 0x674f9842u, 0x7e54a903u, 0x5579fac0u, 0x4c62cb81u, - 0x8138c51fu, 0x9823f45eu, 0xb30ea79du, 0xaa1596dcu, 0xe554001bu, 0xfc4f315au, 0xd7626299u, 0xce7953d8u, - 0x49e14f17u, 0x50fa7e56u, 0x7bd72d95u, 0x62cc1cd4u, 0x2d8d8a13u, 0x3496bb52u, 0x1fbbe891u, 0x06a0d9d0u, - 0x5e7ef3ecu, 0x4765c2adu, 0x6c48916eu, 0x7553a02fu, 0x3a1236e8u, 0x230907a9u, 0x0824546au, 0x113f652bu, - 0x96a779e4u, 0x8fbc48a5u, 0xa4911b66u, 0xbd8a2a27u, 0xf2cbbce0u, 0xebd08da1u, 0xc0fdde62u, 0xd9e6ef23u, - 0x14bce1bdu, 0x0da7d0fcu, 0x268a833fu, 0x3f91b27eu, 0x70d024b9u, 0x69cb15f8u, 0x42e6463bu, 0x5bfd777au, - 0xdc656bb5u, 0xc57e5af4u, 0xee530937u, 0xf7483876u, 0xb809aeb1u, 0xa1129ff0u, 0x8a3fcc33u, 0x9324fd72u -}; - -static const unsigned lodepng_crc32_table2[256] = { - 0x00000000u, 0x01c26a37u, 0x0384d46eu, 0x0246be59u, 0x0709a8dcu, 0x06cbc2ebu, 0x048d7cb2u, 0x054f1685u, - 0x0e1351b8u, 0x0fd13b8fu, 0x0d9785d6u, 0x0c55efe1u, 0x091af964u, 0x08d89353u, 0x0a9e2d0au, 0x0b5c473du, - 0x1c26a370u, 0x1de4c947u, 0x1fa2771eu, 0x1e601d29u, 0x1b2f0bacu, 0x1aed619bu, 0x18abdfc2u, 0x1969b5f5u, - 0x1235f2c8u, 0x13f798ffu, 0x11b126a6u, 0x10734c91u, 0x153c5a14u, 0x14fe3023u, 0x16b88e7au, 0x177ae44du, - 0x384d46e0u, 0x398f2cd7u, 0x3bc9928eu, 0x3a0bf8b9u, 0x3f44ee3cu, 0x3e86840bu, 0x3cc03a52u, 0x3d025065u, - 0x365e1758u, 0x379c7d6fu, 0x35dac336u, 0x3418a901u, 0x3157bf84u, 0x3095d5b3u, 0x32d36beau, 0x331101ddu, - 0x246be590u, 0x25a98fa7u, 0x27ef31feu, 0x262d5bc9u, 0x23624d4cu, 0x22a0277bu, 0x20e69922u, 0x2124f315u, - 0x2a78b428u, 0x2bbade1fu, 0x29fc6046u, 0x283e0a71u, 0x2d711cf4u, 0x2cb376c3u, 0x2ef5c89au, 0x2f37a2adu, - 0x709a8dc0u, 0x7158e7f7u, 0x731e59aeu, 0x72dc3399u, 0x7793251cu, 0x76514f2bu, 0x7417f172u, 0x75d59b45u, - 0x7e89dc78u, 0x7f4bb64fu, 0x7d0d0816u, 0x7ccf6221u, 0x798074a4u, 0x78421e93u, 0x7a04a0cau, 0x7bc6cafdu, - 0x6cbc2eb0u, 0x6d7e4487u, 0x6f38fadeu, 0x6efa90e9u, 0x6bb5866cu, 0x6a77ec5bu, 0x68315202u, 0x69f33835u, - 0x62af7f08u, 0x636d153fu, 0x612bab66u, 0x60e9c151u, 0x65a6d7d4u, 0x6464bde3u, 0x662203bau, 0x67e0698du, - 0x48d7cb20u, 0x4915a117u, 0x4b531f4eu, 0x4a917579u, 0x4fde63fcu, 0x4e1c09cbu, 0x4c5ab792u, 0x4d98dda5u, - 0x46c49a98u, 0x4706f0afu, 0x45404ef6u, 0x448224c1u, 0x41cd3244u, 0x400f5873u, 0x4249e62au, 0x438b8c1du, - 0x54f16850u, 0x55330267u, 0x5775bc3eu, 0x56b7d609u, 0x53f8c08cu, 0x523aaabbu, 0x507c14e2u, 0x51be7ed5u, - 0x5ae239e8u, 0x5b2053dfu, 0x5966ed86u, 0x58a487b1u, 0x5deb9134u, 0x5c29fb03u, 0x5e6f455au, 0x5fad2f6du, - 0xe1351b80u, 0xe0f771b7u, 0xe2b1cfeeu, 0xe373a5d9u, 0xe63cb35cu, 0xe7fed96bu, 0xe5b86732u, 0xe47a0d05u, - 0xef264a38u, 0xeee4200fu, 0xeca29e56u, 0xed60f461u, 0xe82fe2e4u, 0xe9ed88d3u, 0xebab368au, 0xea695cbdu, - 0xfd13b8f0u, 0xfcd1d2c7u, 0xfe976c9eu, 0xff5506a9u, 0xfa1a102cu, 0xfbd87a1bu, 0xf99ec442u, 0xf85cae75u, - 0xf300e948u, 0xf2c2837fu, 0xf0843d26u, 0xf1465711u, 0xf4094194u, 0xf5cb2ba3u, 0xf78d95fau, 0xf64fffcdu, - 0xd9785d60u, 0xd8ba3757u, 0xdafc890eu, 0xdb3ee339u, 0xde71f5bcu, 0xdfb39f8bu, 0xddf521d2u, 0xdc374be5u, - 0xd76b0cd8u, 0xd6a966efu, 0xd4efd8b6u, 0xd52db281u, 0xd062a404u, 0xd1a0ce33u, 0xd3e6706au, 0xd2241a5du, - 0xc55efe10u, 0xc49c9427u, 0xc6da2a7eu, 0xc7184049u, 0xc25756ccu, 0xc3953cfbu, 0xc1d382a2u, 0xc011e895u, - 0xcb4dafa8u, 0xca8fc59fu, 0xc8c97bc6u, 0xc90b11f1u, 0xcc440774u, 0xcd866d43u, 0xcfc0d31au, 0xce02b92du, - 0x91af9640u, 0x906dfc77u, 0x922b422eu, 0x93e92819u, 0x96a63e9cu, 0x976454abu, 0x9522eaf2u, 0x94e080c5u, - 0x9fbcc7f8u, 0x9e7eadcfu, 0x9c381396u, 0x9dfa79a1u, 0x98b56f24u, 0x99770513u, 0x9b31bb4au, 0x9af3d17du, - 0x8d893530u, 0x8c4b5f07u, 0x8e0de15eu, 0x8fcf8b69u, 0x8a809decu, 0x8b42f7dbu, 0x89044982u, 0x88c623b5u, - 0x839a6488u, 0x82580ebfu, 0x801eb0e6u, 0x81dcdad1u, 0x8493cc54u, 0x8551a663u, 0x8717183au, 0x86d5720du, - 0xa9e2d0a0u, 0xa820ba97u, 0xaa6604ceu, 0xaba46ef9u, 0xaeeb787cu, 0xaf29124bu, 0xad6fac12u, 0xacadc625u, - 0xa7f18118u, 0xa633eb2fu, 0xa4755576u, 0xa5b73f41u, 0xa0f829c4u, 0xa13a43f3u, 0xa37cfdaau, 0xa2be979du, - 0xb5c473d0u, 0xb40619e7u, 0xb640a7beu, 0xb782cd89u, 0xb2cddb0cu, 0xb30fb13bu, 0xb1490f62u, 0xb08b6555u, - 0xbbd72268u, 0xba15485fu, 0xb853f606u, 0xb9919c31u, 0xbcde8ab4u, 0xbd1ce083u, 0xbf5a5edau, 0xbe9834edu -}; - -static const unsigned lodepng_crc32_table3[256] = { - 0x00000000u, 0xb8bc6765u, 0xaa09c88bu, 0x12b5afeeu, 0x8f629757u, 0x37def032u, 0x256b5fdcu, 0x9dd738b9u, - 0xc5b428efu, 0x7d084f8au, 0x6fbde064u, 0xd7018701u, 0x4ad6bfb8u, 0xf26ad8ddu, 0xe0df7733u, 0x58631056u, - 0x5019579fu, 0xe8a530fau, 0xfa109f14u, 0x42acf871u, 0xdf7bc0c8u, 0x67c7a7adu, 0x75720843u, 0xcdce6f26u, - 0x95ad7f70u, 0x2d111815u, 0x3fa4b7fbu, 0x8718d09eu, 0x1acfe827u, 0xa2738f42u, 0xb0c620acu, 0x087a47c9u, - 0xa032af3eu, 0x188ec85bu, 0x0a3b67b5u, 0xb28700d0u, 0x2f503869u, 0x97ec5f0cu, 0x8559f0e2u, 0x3de59787u, - 0x658687d1u, 0xdd3ae0b4u, 0xcf8f4f5au, 0x7733283fu, 0xeae41086u, 0x525877e3u, 0x40edd80du, 0xf851bf68u, - 0xf02bf8a1u, 0x48979fc4u, 0x5a22302au, 0xe29e574fu, 0x7f496ff6u, 0xc7f50893u, 0xd540a77du, 0x6dfcc018u, - 0x359fd04eu, 0x8d23b72bu, 0x9f9618c5u, 0x272a7fa0u, 0xbafd4719u, 0x0241207cu, 0x10f48f92u, 0xa848e8f7u, - 0x9b14583du, 0x23a83f58u, 0x311d90b6u, 0x89a1f7d3u, 0x1476cf6au, 0xaccaa80fu, 0xbe7f07e1u, 0x06c36084u, - 0x5ea070d2u, 0xe61c17b7u, 0xf4a9b859u, 0x4c15df3cu, 0xd1c2e785u, 0x697e80e0u, 0x7bcb2f0eu, 0xc377486bu, - 0xcb0d0fa2u, 0x73b168c7u, 0x6104c729u, 0xd9b8a04cu, 0x446f98f5u, 0xfcd3ff90u, 0xee66507eu, 0x56da371bu, - 0x0eb9274du, 0xb6054028u, 0xa4b0efc6u, 0x1c0c88a3u, 0x81dbb01au, 0x3967d77fu, 0x2bd27891u, 0x936e1ff4u, - 0x3b26f703u, 0x839a9066u, 0x912f3f88u, 0x299358edu, 0xb4446054u, 0x0cf80731u, 0x1e4da8dfu, 0xa6f1cfbau, - 0xfe92dfecu, 0x462eb889u, 0x549b1767u, 0xec277002u, 0x71f048bbu, 0xc94c2fdeu, 0xdbf98030u, 0x6345e755u, - 0x6b3fa09cu, 0xd383c7f9u, 0xc1366817u, 0x798a0f72u, 0xe45d37cbu, 0x5ce150aeu, 0x4e54ff40u, 0xf6e89825u, - 0xae8b8873u, 0x1637ef16u, 0x048240f8u, 0xbc3e279du, 0x21e91f24u, 0x99557841u, 0x8be0d7afu, 0x335cb0cau, - 0xed59b63bu, 0x55e5d15eu, 0x47507eb0u, 0xffec19d5u, 0x623b216cu, 0xda874609u, 0xc832e9e7u, 0x708e8e82u, - 0x28ed9ed4u, 0x9051f9b1u, 0x82e4565fu, 0x3a58313au, 0xa78f0983u, 0x1f336ee6u, 0x0d86c108u, 0xb53aa66du, - 0xbd40e1a4u, 0x05fc86c1u, 0x1749292fu, 0xaff54e4au, 0x322276f3u, 0x8a9e1196u, 0x982bbe78u, 0x2097d91du, - 0x78f4c94bu, 0xc048ae2eu, 0xd2fd01c0u, 0x6a4166a5u, 0xf7965e1cu, 0x4f2a3979u, 0x5d9f9697u, 0xe523f1f2u, - 0x4d6b1905u, 0xf5d77e60u, 0xe762d18eu, 0x5fdeb6ebu, 0xc2098e52u, 0x7ab5e937u, 0x680046d9u, 0xd0bc21bcu, - 0x88df31eau, 0x3063568fu, 0x22d6f961u, 0x9a6a9e04u, 0x07bda6bdu, 0xbf01c1d8u, 0xadb46e36u, 0x15080953u, - 0x1d724e9au, 0xa5ce29ffu, 0xb77b8611u, 0x0fc7e174u, 0x9210d9cdu, 0x2aacbea8u, 0x38191146u, 0x80a57623u, - 0xd8c66675u, 0x607a0110u, 0x72cfaefeu, 0xca73c99bu, 0x57a4f122u, 0xef189647u, 0xfdad39a9u, 0x45115eccu, - 0x764dee06u, 0xcef18963u, 0xdc44268du, 0x64f841e8u, 0xf92f7951u, 0x41931e34u, 0x5326b1dau, 0xeb9ad6bfu, - 0xb3f9c6e9u, 0x0b45a18cu, 0x19f00e62u, 0xa14c6907u, 0x3c9b51beu, 0x842736dbu, 0x96929935u, 0x2e2efe50u, - 0x2654b999u, 0x9ee8defcu, 0x8c5d7112u, 0x34e11677u, 0xa9362eceu, 0x118a49abu, 0x033fe645u, 0xbb838120u, - 0xe3e09176u, 0x5b5cf613u, 0x49e959fdu, 0xf1553e98u, 0x6c820621u, 0xd43e6144u, 0xc68bceaau, 0x7e37a9cfu, - 0xd67f4138u, 0x6ec3265du, 0x7c7689b3u, 0xc4caeed6u, 0x591dd66fu, 0xe1a1b10au, 0xf3141ee4u, 0x4ba87981u, - 0x13cb69d7u, 0xab770eb2u, 0xb9c2a15cu, 0x017ec639u, 0x9ca9fe80u, 0x241599e5u, 0x36a0360bu, 0x8e1c516eu, - 0x866616a7u, 0x3eda71c2u, 0x2c6fde2cu, 0x94d3b949u, 0x090481f0u, 0xb1b8e695u, 0xa30d497bu, 0x1bb12e1eu, - 0x43d23e48u, 0xfb6e592du, 0xe9dbf6c3u, 0x516791a6u, 0xccb0a91fu, 0x740cce7au, 0x66b96194u, 0xde0506f1u -}; - -static const unsigned lodepng_crc32_table4[256] = { - 0x00000000u, 0x3d6029b0u, 0x7ac05360u, 0x47a07ad0u, 0xf580a6c0u, 0xc8e08f70u, 0x8f40f5a0u, 0xb220dc10u, - 0x30704bc1u, 0x0d106271u, 0x4ab018a1u, 0x77d03111u, 0xc5f0ed01u, 0xf890c4b1u, 0xbf30be61u, 0x825097d1u, - 0x60e09782u, 0x5d80be32u, 0x1a20c4e2u, 0x2740ed52u, 0x95603142u, 0xa80018f2u, 0xefa06222u, 0xd2c04b92u, - 0x5090dc43u, 0x6df0f5f3u, 0x2a508f23u, 0x1730a693u, 0xa5107a83u, 0x98705333u, 0xdfd029e3u, 0xe2b00053u, - 0xc1c12f04u, 0xfca106b4u, 0xbb017c64u, 0x866155d4u, 0x344189c4u, 0x0921a074u, 0x4e81daa4u, 0x73e1f314u, - 0xf1b164c5u, 0xccd14d75u, 0x8b7137a5u, 0xb6111e15u, 0x0431c205u, 0x3951ebb5u, 0x7ef19165u, 0x4391b8d5u, - 0xa121b886u, 0x9c419136u, 0xdbe1ebe6u, 0xe681c256u, 0x54a11e46u, 0x69c137f6u, 0x2e614d26u, 0x13016496u, - 0x9151f347u, 0xac31daf7u, 0xeb91a027u, 0xd6f18997u, 0x64d15587u, 0x59b17c37u, 0x1e1106e7u, 0x23712f57u, - 0x58f35849u, 0x659371f9u, 0x22330b29u, 0x1f532299u, 0xad73fe89u, 0x9013d739u, 0xd7b3ade9u, 0xead38459u, - 0x68831388u, 0x55e33a38u, 0x124340e8u, 0x2f236958u, 0x9d03b548u, 0xa0639cf8u, 0xe7c3e628u, 0xdaa3cf98u, - 0x3813cfcbu, 0x0573e67bu, 0x42d39cabu, 0x7fb3b51bu, 0xcd93690bu, 0xf0f340bbu, 0xb7533a6bu, 0x8a3313dbu, - 0x0863840au, 0x3503adbau, 0x72a3d76au, 0x4fc3fedau, 0xfde322cau, 0xc0830b7au, 0x872371aau, 0xba43581au, - 0x9932774du, 0xa4525efdu, 0xe3f2242du, 0xde920d9du, 0x6cb2d18du, 0x51d2f83du, 0x167282edu, 0x2b12ab5du, - 0xa9423c8cu, 0x9422153cu, 0xd3826fecu, 0xeee2465cu, 0x5cc29a4cu, 0x61a2b3fcu, 0x2602c92cu, 0x1b62e09cu, - 0xf9d2e0cfu, 0xc4b2c97fu, 0x8312b3afu, 0xbe729a1fu, 0x0c52460fu, 0x31326fbfu, 0x7692156fu, 0x4bf23cdfu, - 0xc9a2ab0eu, 0xf4c282beu, 0xb362f86eu, 0x8e02d1deu, 0x3c220dceu, 0x0142247eu, 0x46e25eaeu, 0x7b82771eu, - 0xb1e6b092u, 0x8c869922u, 0xcb26e3f2u, 0xf646ca42u, 0x44661652u, 0x79063fe2u, 0x3ea64532u, 0x03c66c82u, - 0x8196fb53u, 0xbcf6d2e3u, 0xfb56a833u, 0xc6368183u, 0x74165d93u, 0x49767423u, 0x0ed60ef3u, 0x33b62743u, - 0xd1062710u, 0xec660ea0u, 0xabc67470u, 0x96a65dc0u, 0x248681d0u, 0x19e6a860u, 0x5e46d2b0u, 0x6326fb00u, - 0xe1766cd1u, 0xdc164561u, 0x9bb63fb1u, 0xa6d61601u, 0x14f6ca11u, 0x2996e3a1u, 0x6e369971u, 0x5356b0c1u, - 0x70279f96u, 0x4d47b626u, 0x0ae7ccf6u, 0x3787e546u, 0x85a73956u, 0xb8c710e6u, 0xff676a36u, 0xc2074386u, - 0x4057d457u, 0x7d37fde7u, 0x3a978737u, 0x07f7ae87u, 0xb5d77297u, 0x88b75b27u, 0xcf1721f7u, 0xf2770847u, - 0x10c70814u, 0x2da721a4u, 0x6a075b74u, 0x576772c4u, 0xe547aed4u, 0xd8278764u, 0x9f87fdb4u, 0xa2e7d404u, - 0x20b743d5u, 0x1dd76a65u, 0x5a7710b5u, 0x67173905u, 0xd537e515u, 0xe857cca5u, 0xaff7b675u, 0x92979fc5u, - 0xe915e8dbu, 0xd475c16bu, 0x93d5bbbbu, 0xaeb5920bu, 0x1c954e1bu, 0x21f567abu, 0x66551d7bu, 0x5b3534cbu, - 0xd965a31au, 0xe4058aaau, 0xa3a5f07au, 0x9ec5d9cau, 0x2ce505dau, 0x11852c6au, 0x562556bau, 0x6b457f0au, - 0x89f57f59u, 0xb49556e9u, 0xf3352c39u, 0xce550589u, 0x7c75d999u, 0x4115f029u, 0x06b58af9u, 0x3bd5a349u, - 0xb9853498u, 0x84e51d28u, 0xc34567f8u, 0xfe254e48u, 0x4c059258u, 0x7165bbe8u, 0x36c5c138u, 0x0ba5e888u, - 0x28d4c7dfu, 0x15b4ee6fu, 0x521494bfu, 0x6f74bd0fu, 0xdd54611fu, 0xe03448afu, 0xa794327fu, 0x9af41bcfu, - 0x18a48c1eu, 0x25c4a5aeu, 0x6264df7eu, 0x5f04f6ceu, 0xed242adeu, 0xd044036eu, 0x97e479beu, 0xaa84500eu, - 0x4834505du, 0x755479edu, 0x32f4033du, 0x0f942a8du, 0xbdb4f69du, 0x80d4df2du, 0xc774a5fdu, 0xfa148c4du, - 0x78441b9cu, 0x4524322cu, 0x028448fcu, 0x3fe4614cu, 0x8dc4bd5cu, 0xb0a494ecu, 0xf704ee3cu, 0xca64c78cu -}; - -static const unsigned lodepng_crc32_table5[256] = { - 0x00000000u, 0xcb5cd3a5u, 0x4dc8a10bu, 0x869472aeu, 0x9b914216u, 0x50cd91b3u, 0xd659e31du, 0x1d0530b8u, - 0xec53826du, 0x270f51c8u, 0xa19b2366u, 0x6ac7f0c3u, 0x77c2c07bu, 0xbc9e13deu, 0x3a0a6170u, 0xf156b2d5u, - 0x03d6029bu, 0xc88ad13eu, 0x4e1ea390u, 0x85427035u, 0x9847408du, 0x531b9328u, 0xd58fe186u, 0x1ed33223u, - 0xef8580f6u, 0x24d95353u, 0xa24d21fdu, 0x6911f258u, 0x7414c2e0u, 0xbf481145u, 0x39dc63ebu, 0xf280b04eu, - 0x07ac0536u, 0xccf0d693u, 0x4a64a43du, 0x81387798u, 0x9c3d4720u, 0x57619485u, 0xd1f5e62bu, 0x1aa9358eu, - 0xebff875bu, 0x20a354feu, 0xa6372650u, 0x6d6bf5f5u, 0x706ec54du, 0xbb3216e8u, 0x3da66446u, 0xf6fab7e3u, - 0x047a07adu, 0xcf26d408u, 0x49b2a6a6u, 0x82ee7503u, 0x9feb45bbu, 0x54b7961eu, 0xd223e4b0u, 0x197f3715u, - 0xe82985c0u, 0x23755665u, 0xa5e124cbu, 0x6ebdf76eu, 0x73b8c7d6u, 0xb8e41473u, 0x3e7066ddu, 0xf52cb578u, - 0x0f580a6cu, 0xc404d9c9u, 0x4290ab67u, 0x89cc78c2u, 0x94c9487au, 0x5f959bdfu, 0xd901e971u, 0x125d3ad4u, - 0xe30b8801u, 0x28575ba4u, 0xaec3290au, 0x659ffaafu, 0x789aca17u, 0xb3c619b2u, 0x35526b1cu, 0xfe0eb8b9u, - 0x0c8e08f7u, 0xc7d2db52u, 0x4146a9fcu, 0x8a1a7a59u, 0x971f4ae1u, 0x5c439944u, 0xdad7ebeau, 0x118b384fu, - 0xe0dd8a9au, 0x2b81593fu, 0xad152b91u, 0x6649f834u, 0x7b4cc88cu, 0xb0101b29u, 0x36846987u, 0xfdd8ba22u, - 0x08f40f5au, 0xc3a8dcffu, 0x453cae51u, 0x8e607df4u, 0x93654d4cu, 0x58399ee9u, 0xdeadec47u, 0x15f13fe2u, - 0xe4a78d37u, 0x2ffb5e92u, 0xa96f2c3cu, 0x6233ff99u, 0x7f36cf21u, 0xb46a1c84u, 0x32fe6e2au, 0xf9a2bd8fu, - 0x0b220dc1u, 0xc07ede64u, 0x46eaaccau, 0x8db67f6fu, 0x90b34fd7u, 0x5bef9c72u, 0xdd7beedcu, 0x16273d79u, - 0xe7718facu, 0x2c2d5c09u, 0xaab92ea7u, 0x61e5fd02u, 0x7ce0cdbau, 0xb7bc1e1fu, 0x31286cb1u, 0xfa74bf14u, - 0x1eb014d8u, 0xd5ecc77du, 0x5378b5d3u, 0x98246676u, 0x852156ceu, 0x4e7d856bu, 0xc8e9f7c5u, 0x03b52460u, - 0xf2e396b5u, 0x39bf4510u, 0xbf2b37beu, 0x7477e41bu, 0x6972d4a3u, 0xa22e0706u, 0x24ba75a8u, 0xefe6a60du, - 0x1d661643u, 0xd63ac5e6u, 0x50aeb748u, 0x9bf264edu, 0x86f75455u, 0x4dab87f0u, 0xcb3ff55eu, 0x006326fbu, - 0xf135942eu, 0x3a69478bu, 0xbcfd3525u, 0x77a1e680u, 0x6aa4d638u, 0xa1f8059du, 0x276c7733u, 0xec30a496u, - 0x191c11eeu, 0xd240c24bu, 0x54d4b0e5u, 0x9f886340u, 0x828d53f8u, 0x49d1805du, 0xcf45f2f3u, 0x04192156u, - 0xf54f9383u, 0x3e134026u, 0xb8873288u, 0x73dbe12du, 0x6eded195u, 0xa5820230u, 0x2316709eu, 0xe84aa33bu, - 0x1aca1375u, 0xd196c0d0u, 0x5702b27eu, 0x9c5e61dbu, 0x815b5163u, 0x4a0782c6u, 0xcc93f068u, 0x07cf23cdu, - 0xf6999118u, 0x3dc542bdu, 0xbb513013u, 0x700de3b6u, 0x6d08d30eu, 0xa65400abu, 0x20c07205u, 0xeb9ca1a0u, - 0x11e81eb4u, 0xdab4cd11u, 0x5c20bfbfu, 0x977c6c1au, 0x8a795ca2u, 0x41258f07u, 0xc7b1fda9u, 0x0ced2e0cu, - 0xfdbb9cd9u, 0x36e74f7cu, 0xb0733dd2u, 0x7b2fee77u, 0x662adecfu, 0xad760d6au, 0x2be27fc4u, 0xe0beac61u, - 0x123e1c2fu, 0xd962cf8au, 0x5ff6bd24u, 0x94aa6e81u, 0x89af5e39u, 0x42f38d9cu, 0xc467ff32u, 0x0f3b2c97u, - 0xfe6d9e42u, 0x35314de7u, 0xb3a53f49u, 0x78f9ececu, 0x65fcdc54u, 0xaea00ff1u, 0x28347d5fu, 0xe368aefau, - 0x16441b82u, 0xdd18c827u, 0x5b8cba89u, 0x90d0692cu, 0x8dd55994u, 0x46898a31u, 0xc01df89fu, 0x0b412b3au, - 0xfa1799efu, 0x314b4a4au, 0xb7df38e4u, 0x7c83eb41u, 0x6186dbf9u, 0xaada085cu, 0x2c4e7af2u, 0xe712a957u, - 0x15921919u, 0xdececabcu, 0x585ab812u, 0x93066bb7u, 0x8e035b0fu, 0x455f88aau, 0xc3cbfa04u, 0x089729a1u, - 0xf9c19b74u, 0x329d48d1u, 0xb4093a7fu, 0x7f55e9dau, 0x6250d962u, 0xa90c0ac7u, 0x2f987869u, 0xe4c4abccu -}; - -static const unsigned lodepng_crc32_table6[256] = { - 0x00000000u, 0xa6770bb4u, 0x979f1129u, 0x31e81a9du, 0xf44f2413u, 0x52382fa7u, 0x63d0353au, 0xc5a73e8eu, - 0x33ef4e67u, 0x959845d3u, 0xa4705f4eu, 0x020754fau, 0xc7a06a74u, 0x61d761c0u, 0x503f7b5du, 0xf64870e9u, - 0x67de9cceu, 0xc1a9977au, 0xf0418de7u, 0x56368653u, 0x9391b8ddu, 0x35e6b369u, 0x040ea9f4u, 0xa279a240u, - 0x5431d2a9u, 0xf246d91du, 0xc3aec380u, 0x65d9c834u, 0xa07ef6bau, 0x0609fd0eu, 0x37e1e793u, 0x9196ec27u, - 0xcfbd399cu, 0x69ca3228u, 0x582228b5u, 0xfe552301u, 0x3bf21d8fu, 0x9d85163bu, 0xac6d0ca6u, 0x0a1a0712u, - 0xfc5277fbu, 0x5a257c4fu, 0x6bcd66d2u, 0xcdba6d66u, 0x081d53e8u, 0xae6a585cu, 0x9f8242c1u, 0x39f54975u, - 0xa863a552u, 0x0e14aee6u, 0x3ffcb47bu, 0x998bbfcfu, 0x5c2c8141u, 0xfa5b8af5u, 0xcbb39068u, 0x6dc49bdcu, - 0x9b8ceb35u, 0x3dfbe081u, 0x0c13fa1cu, 0xaa64f1a8u, 0x6fc3cf26u, 0xc9b4c492u, 0xf85cde0fu, 0x5e2bd5bbu, - 0x440b7579u, 0xe27c7ecdu, 0xd3946450u, 0x75e36fe4u, 0xb044516au, 0x16335adeu, 0x27db4043u, 0x81ac4bf7u, - 0x77e43b1eu, 0xd19330aau, 0xe07b2a37u, 0x460c2183u, 0x83ab1f0du, 0x25dc14b9u, 0x14340e24u, 0xb2430590u, - 0x23d5e9b7u, 0x85a2e203u, 0xb44af89eu, 0x123df32au, 0xd79acda4u, 0x71edc610u, 0x4005dc8du, 0xe672d739u, - 0x103aa7d0u, 0xb64dac64u, 0x87a5b6f9u, 0x21d2bd4du, 0xe47583c3u, 0x42028877u, 0x73ea92eau, 0xd59d995eu, - 0x8bb64ce5u, 0x2dc14751u, 0x1c295dccu, 0xba5e5678u, 0x7ff968f6u, 0xd98e6342u, 0xe86679dfu, 0x4e11726bu, - 0xb8590282u, 0x1e2e0936u, 0x2fc613abu, 0x89b1181fu, 0x4c162691u, 0xea612d25u, 0xdb8937b8u, 0x7dfe3c0cu, - 0xec68d02bu, 0x4a1fdb9fu, 0x7bf7c102u, 0xdd80cab6u, 0x1827f438u, 0xbe50ff8cu, 0x8fb8e511u, 0x29cfeea5u, - 0xdf879e4cu, 0x79f095f8u, 0x48188f65u, 0xee6f84d1u, 0x2bc8ba5fu, 0x8dbfb1ebu, 0xbc57ab76u, 0x1a20a0c2u, - 0x8816eaf2u, 0x2e61e146u, 0x1f89fbdbu, 0xb9fef06fu, 0x7c59cee1u, 0xda2ec555u, 0xebc6dfc8u, 0x4db1d47cu, - 0xbbf9a495u, 0x1d8eaf21u, 0x2c66b5bcu, 0x8a11be08u, 0x4fb68086u, 0xe9c18b32u, 0xd82991afu, 0x7e5e9a1bu, - 0xefc8763cu, 0x49bf7d88u, 0x78576715u, 0xde206ca1u, 0x1b87522fu, 0xbdf0599bu, 0x8c184306u, 0x2a6f48b2u, - 0xdc27385bu, 0x7a5033efu, 0x4bb82972u, 0xedcf22c6u, 0x28681c48u, 0x8e1f17fcu, 0xbff70d61u, 0x198006d5u, - 0x47abd36eu, 0xe1dcd8dau, 0xd034c247u, 0x7643c9f3u, 0xb3e4f77du, 0x1593fcc9u, 0x247be654u, 0x820cede0u, - 0x74449d09u, 0xd23396bdu, 0xe3db8c20u, 0x45ac8794u, 0x800bb91au, 0x267cb2aeu, 0x1794a833u, 0xb1e3a387u, - 0x20754fa0u, 0x86024414u, 0xb7ea5e89u, 0x119d553du, 0xd43a6bb3u, 0x724d6007u, 0x43a57a9au, 0xe5d2712eu, - 0x139a01c7u, 0xb5ed0a73u, 0x840510eeu, 0x22721b5au, 0xe7d525d4u, 0x41a22e60u, 0x704a34fdu, 0xd63d3f49u, - 0xcc1d9f8bu, 0x6a6a943fu, 0x5b828ea2u, 0xfdf58516u, 0x3852bb98u, 0x9e25b02cu, 0xafcdaab1u, 0x09baa105u, - 0xfff2d1ecu, 0x5985da58u, 0x686dc0c5u, 0xce1acb71u, 0x0bbdf5ffu, 0xadcafe4bu, 0x9c22e4d6u, 0x3a55ef62u, - 0xabc30345u, 0x0db408f1u, 0x3c5c126cu, 0x9a2b19d8u, 0x5f8c2756u, 0xf9fb2ce2u, 0xc813367fu, 0x6e643dcbu, - 0x982c4d22u, 0x3e5b4696u, 0x0fb35c0bu, 0xa9c457bfu, 0x6c636931u, 0xca146285u, 0xfbfc7818u, 0x5d8b73acu, - 0x03a0a617u, 0xa5d7ada3u, 0x943fb73eu, 0x3248bc8au, 0xf7ef8204u, 0x519889b0u, 0x6070932du, 0xc6079899u, - 0x304fe870u, 0x9638e3c4u, 0xa7d0f959u, 0x01a7f2edu, 0xc400cc63u, 0x6277c7d7u, 0x539fdd4au, 0xf5e8d6feu, - 0x647e3ad9u, 0xc209316du, 0xf3e12bf0u, 0x55962044u, 0x90311ecau, 0x3646157eu, 0x07ae0fe3u, 0xa1d90457u, - 0x579174beu, 0xf1e67f0au, 0xc00e6597u, 0x66796e23u, 0xa3de50adu, 0x05a95b19u, 0x34414184u, 0x92364a30u -}; - -static const unsigned lodepng_crc32_table7[256] = { - 0x00000000u, 0xccaa009eu, 0x4225077du, 0x8e8f07e3u, 0x844a0efau, 0x48e00e64u, 0xc66f0987u, 0x0ac50919u, - 0xd3e51bb5u, 0x1f4f1b2bu, 0x91c01cc8u, 0x5d6a1c56u, 0x57af154fu, 0x9b0515d1u, 0x158a1232u, 0xd92012acu, - 0x7cbb312bu, 0xb01131b5u, 0x3e9e3656u, 0xf23436c8u, 0xf8f13fd1u, 0x345b3f4fu, 0xbad438acu, 0x767e3832u, - 0xaf5e2a9eu, 0x63f42a00u, 0xed7b2de3u, 0x21d12d7du, 0x2b142464u, 0xe7be24fau, 0x69312319u, 0xa59b2387u, - 0xf9766256u, 0x35dc62c8u, 0xbb53652bu, 0x77f965b5u, 0x7d3c6cacu, 0xb1966c32u, 0x3f196bd1u, 0xf3b36b4fu, - 0x2a9379e3u, 0xe639797du, 0x68b67e9eu, 0xa41c7e00u, 0xaed97719u, 0x62737787u, 0xecfc7064u, 0x205670fau, - 0x85cd537du, 0x496753e3u, 0xc7e85400u, 0x0b42549eu, 0x01875d87u, 0xcd2d5d19u, 0x43a25afau, 0x8f085a64u, - 0x562848c8u, 0x9a824856u, 0x140d4fb5u, 0xd8a74f2bu, 0xd2624632u, 0x1ec846acu, 0x9047414fu, 0x5ced41d1u, - 0x299dc2edu, 0xe537c273u, 0x6bb8c590u, 0xa712c50eu, 0xadd7cc17u, 0x617dcc89u, 0xeff2cb6au, 0x2358cbf4u, - 0xfa78d958u, 0x36d2d9c6u, 0xb85dde25u, 0x74f7debbu, 0x7e32d7a2u, 0xb298d73cu, 0x3c17d0dfu, 0xf0bdd041u, - 0x5526f3c6u, 0x998cf358u, 0x1703f4bbu, 0xdba9f425u, 0xd16cfd3cu, 0x1dc6fda2u, 0x9349fa41u, 0x5fe3fadfu, - 0x86c3e873u, 0x4a69e8edu, 0xc4e6ef0eu, 0x084cef90u, 0x0289e689u, 0xce23e617u, 0x40ace1f4u, 0x8c06e16au, - 0xd0eba0bbu, 0x1c41a025u, 0x92cea7c6u, 0x5e64a758u, 0x54a1ae41u, 0x980baedfu, 0x1684a93cu, 0xda2ea9a2u, - 0x030ebb0eu, 0xcfa4bb90u, 0x412bbc73u, 0x8d81bcedu, 0x8744b5f4u, 0x4beeb56au, 0xc561b289u, 0x09cbb217u, - 0xac509190u, 0x60fa910eu, 0xee7596edu, 0x22df9673u, 0x281a9f6au, 0xe4b09ff4u, 0x6a3f9817u, 0xa6959889u, - 0x7fb58a25u, 0xb31f8abbu, 0x3d908d58u, 0xf13a8dc6u, 0xfbff84dfu, 0x37558441u, 0xb9da83a2u, 0x7570833cu, - 0x533b85dau, 0x9f918544u, 0x111e82a7u, 0xddb48239u, 0xd7718b20u, 0x1bdb8bbeu, 0x95548c5du, 0x59fe8cc3u, - 0x80de9e6fu, 0x4c749ef1u, 0xc2fb9912u, 0x0e51998cu, 0x04949095u, 0xc83e900bu, 0x46b197e8u, 0x8a1b9776u, - 0x2f80b4f1u, 0xe32ab46fu, 0x6da5b38cu, 0xa10fb312u, 0xabcaba0bu, 0x6760ba95u, 0xe9efbd76u, 0x2545bde8u, - 0xfc65af44u, 0x30cfafdau, 0xbe40a839u, 0x72eaa8a7u, 0x782fa1beu, 0xb485a120u, 0x3a0aa6c3u, 0xf6a0a65du, - 0xaa4de78cu, 0x66e7e712u, 0xe868e0f1u, 0x24c2e06fu, 0x2e07e976u, 0xe2ade9e8u, 0x6c22ee0bu, 0xa088ee95u, - 0x79a8fc39u, 0xb502fca7u, 0x3b8dfb44u, 0xf727fbdau, 0xfde2f2c3u, 0x3148f25du, 0xbfc7f5beu, 0x736df520u, - 0xd6f6d6a7u, 0x1a5cd639u, 0x94d3d1dau, 0x5879d144u, 0x52bcd85du, 0x9e16d8c3u, 0x1099df20u, 0xdc33dfbeu, - 0x0513cd12u, 0xc9b9cd8cu, 0x4736ca6fu, 0x8b9ccaf1u, 0x8159c3e8u, 0x4df3c376u, 0xc37cc495u, 0x0fd6c40bu, - 0x7aa64737u, 0xb60c47a9u, 0x3883404au, 0xf42940d4u, 0xfeec49cdu, 0x32464953u, 0xbcc94eb0u, 0x70634e2eu, - 0xa9435c82u, 0x65e95c1cu, 0xeb665bffu, 0x27cc5b61u, 0x2d095278u, 0xe1a352e6u, 0x6f2c5505u, 0xa386559bu, - 0x061d761cu, 0xcab77682u, 0x44387161u, 0x889271ffu, 0x825778e6u, 0x4efd7878u, 0xc0727f9bu, 0x0cd87f05u, - 0xd5f86da9u, 0x19526d37u, 0x97dd6ad4u, 0x5b776a4au, 0x51b26353u, 0x9d1863cdu, 0x1397642eu, 0xdf3d64b0u, - 0x83d02561u, 0x4f7a25ffu, 0xc1f5221cu, 0x0d5f2282u, 0x079a2b9bu, 0xcb302b05u, 0x45bf2ce6u, 0x89152c78u, - 0x50353ed4u, 0x9c9f3e4au, 0x121039a9u, 0xdeba3937u, 0xd47f302eu, 0x18d530b0u, 0x965a3753u, 0x5af037cdu, - 0xff6b144au, 0x33c114d4u, 0xbd4e1337u, 0x71e413a9u, 0x7b211ab0u, 0xb78b1a2eu, 0x39041dcdu, 0xf5ae1d53u, - 0x2c8e0fffu, 0xe0240f61u, 0x6eab0882u, 0xa201081cu, 0xa8c40105u, 0x646e019bu, 0xeae10678u, 0x264b06e6u -}; - -/* Computes the cyclic redundancy check as used by PNG chunks*/ -unsigned lodepng_crc32(const unsigned char* data, size_t length) { - /*Using the Slicing by Eight algorithm*/ - unsigned r = 0xffffffffu; - while(length >= 8) { - r = lodepng_crc32_table7[(data[0] ^ (r & 0xffu))] ^ - lodepng_crc32_table6[(data[1] ^ ((r >> 8) & 0xffu))] ^ - lodepng_crc32_table5[(data[2] ^ ((r >> 16) & 0xffu))] ^ - lodepng_crc32_table4[(data[3] ^ ((r >> 24) & 0xffu))] ^ - lodepng_crc32_table3[data[4]] ^ - lodepng_crc32_table2[data[5]] ^ - lodepng_crc32_table1[data[6]] ^ - lodepng_crc32_table0[data[7]]; - data += 8; - length -= 8; - } - while(length--) { - r = lodepng_crc32_table0[(r ^ *data++) & 0xffu] ^ (r >> 8); - } - return r ^ 0xffffffffu; -} -#else /* LODEPNG_COMPILE_CRC */ -/*in this case, the function is only declared here, and must be defined externally -so that it will be linked in. - -Example implementation that uses a much smaller lookup table for memory constrained cases: - -unsigned lodepng_crc32(const unsigned char* data, size_t length) { - unsigned r = 0xffffffffu; - static const unsigned table[16] = { - 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, - 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c - }; - while(length--) { - r = table[(r ^ *data) & 0xf] ^ (r >> 4); - r = table[(r ^ (*data >> 4)) & 0xf] ^ (r >> 4); - data++; - } - return r ^ 0xffffffffu; -} -*/ -unsigned lodepng_crc32(const unsigned char* data, size_t length); -#endif /* LODEPNG_COMPILE_CRC */ - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / Reading and writing PNG color channel bits / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -/* The color channel bits of less-than-8-bit pixels are read with the MSB of bytes first, -so LodePNGBitWriter and LodePNGBitReader can't be used for those. */ - -static unsigned char readBitFromReversedStream(size_t* bitpointer, const unsigned char* bitstream) { - unsigned char result = (unsigned char)((bitstream[(*bitpointer) >> 3] >> (7 - ((*bitpointer) & 0x7))) & 1); - ++(*bitpointer); - return result; -} - -/* TODO: make this faster */ -static unsigned readBitsFromReversedStream(size_t* bitpointer, const unsigned char* bitstream, size_t nbits) { - unsigned result = 0; - size_t i; - for(i = 0 ; i < nbits; ++i) { - result <<= 1u; - result |= (unsigned)readBitFromReversedStream(bitpointer, bitstream); - } - return result; -} - -static void setBitOfReversedStream(size_t* bitpointer, unsigned char* bitstream, unsigned char bit) { - /*the current bit in bitstream may be 0 or 1 for this to work*/ - if(bit == 0) bitstream[(*bitpointer) >> 3u] &= (unsigned char)(~(1u << (7u - ((*bitpointer) & 7u)))); - else bitstream[(*bitpointer) >> 3u] |= (1u << (7u - ((*bitpointer) & 7u))); - ++(*bitpointer); -} - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / PNG chunks / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -unsigned lodepng_chunk_length(const unsigned char* chunk) { - return lodepng_read32bitInt(chunk); -} - -void lodepng_chunk_type(char type[5], const unsigned char* chunk) { - unsigned i; - for(i = 0; i != 4; ++i) type[i] = (char)chunk[4 + i]; - type[4] = 0; /*null termination char*/ -} - -unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, const char* type) { - if(lodepng_strlen(type) != 4) return 0; - return (chunk[4] == type[0] && chunk[5] == type[1] && chunk[6] == type[2] && chunk[7] == type[3]); -} - -/* chunk type name must exist only out of alphabetic characters a-z or A-Z */ -static unsigned char lodepng_chunk_type_name_valid(const unsigned char* chunk) { - unsigned i; - for(i = 0; i != 4; ++i) { - char c = (char)chunk[4 + i]; - if(!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))) { - return 0; /* not valid */ - } - } - return 1; /* valid */ -} - -unsigned char lodepng_chunk_ancillary(const unsigned char* chunk) { - return((chunk[4] & 32) != 0); -} - -unsigned char lodepng_chunk_private(const unsigned char* chunk) { - return((chunk[5] & 32) != 0); -} - -/* this is an error if it is reserved: the third character must be uppercase in the PNG standard, -lowercasing this character is reserved for possible future extension by the spec*/ -static unsigned char lodepng_chunk_reserved(const unsigned char* chunk) { - return((chunk[6] & 32) != 0); -} - -unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk) { - return((chunk[7] & 32) != 0); -} - -unsigned char* lodepng_chunk_data(unsigned char* chunk) { - return &chunk[8]; -} - -const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk) { - return &chunk[8]; -} - -unsigned lodepng_chunk_check_crc(const unsigned char* chunk) { - unsigned length = lodepng_chunk_length(chunk); - unsigned crc = lodepng_read32bitInt(&chunk[length + 8]); - /*the CRC is taken of the data and the 4 chunk type letters, not the length*/ - unsigned checksum = lodepng_crc32(&chunk[4], length + 4); - if(crc != checksum) return 1; - else return 0; -} - -void lodepng_chunk_generate_crc(unsigned char* chunk) { - unsigned length = lodepng_chunk_length(chunk); - unsigned crc = lodepng_crc32(&chunk[4], length + 4); - lodepng_set32bitInt(chunk + 8 + length, crc); -} - -unsigned char* lodepng_chunk_next(unsigned char* chunk, unsigned char* end) { - size_t available_size = (size_t)(end - chunk); - if(chunk >= end || available_size < 12) return end; /*too small to contain a chunk*/ - if(chunk[0] == 0x89 && chunk[1] == 0x50 && chunk[2] == 0x4e && chunk[3] == 0x47 - && chunk[4] == 0x0d && chunk[5] == 0x0a && chunk[6] == 0x1a && chunk[7] == 0x0a) { - /* Is PNG magic header at start of PNG file. Jump to first actual chunk. */ - return chunk + 8; - } else { - size_t total_chunk_length; - if(lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) return end; - if(total_chunk_length > available_size) return end; /*outside of range*/ - return chunk + total_chunk_length; - } -} - -const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk, const unsigned char* end) { - size_t available_size = (size_t)(end - chunk); - if(chunk >= end || available_size < 12) return end; /*too small to contain a chunk*/ - if(chunk[0] == 0x89 && chunk[1] == 0x50 && chunk[2] == 0x4e && chunk[3] == 0x47 - && chunk[4] == 0x0d && chunk[5] == 0x0a && chunk[6] == 0x1a && chunk[7] == 0x0a) { - /* Is PNG magic header at start of PNG file. Jump to first actual chunk. */ - return chunk + 8; - } else { - size_t total_chunk_length; - if(lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) return end; - if(total_chunk_length > available_size) return end; /*outside of range*/ - return chunk + total_chunk_length; - } -} - -unsigned char* lodepng_chunk_find(unsigned char* chunk, unsigned char* end, const char type[5]) { - for(;;) { - if(chunk >= end || end - chunk < 12) return 0; /* past file end: chunk + 12 > end */ - if(lodepng_chunk_type_equals(chunk, type)) return chunk; - chunk = lodepng_chunk_next(chunk, end); - } -} - -const unsigned char* lodepng_chunk_find_const(const unsigned char* chunk, const unsigned char* end, const char type[5]) { - for(;;) { - if(chunk >= end || end - chunk < 12) return 0; /* past file end: chunk + 12 > end */ - if(lodepng_chunk_type_equals(chunk, type)) return chunk; - chunk = lodepng_chunk_next_const(chunk, end); - } -} - -unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsigned char* chunk) { - unsigned i; - size_t total_chunk_length, new_length; - unsigned char *chunk_start, *new_buffer; - - if(!lodepng_chunk_type_name_valid(chunk)) { - return 121; /* invalid chunk type name */ - } - if(lodepng_chunk_reserved(chunk)) { - return 122; /* invalid third lowercase character */ - } - - if(lodepng_addofl(lodepng_chunk_length(chunk), 12, &total_chunk_length)) return 77; - if(lodepng_addofl(*outsize, total_chunk_length, &new_length)) return 77; - - new_buffer = (unsigned char*)lodepng_realloc(*out, new_length); - if(!new_buffer) return 83; /*alloc fail*/ - (*out) = new_buffer; - (*outsize) = new_length; - chunk_start = &(*out)[new_length - total_chunk_length]; - - for(i = 0; i != total_chunk_length; ++i) chunk_start[i] = chunk[i]; - - return 0; -} - -/*Sets length and name and allocates the space for data and crc but does not -set data or crc yet. Returns the start of the chunk in chunk. The start of -the data is at chunk + 8. To finalize chunk, add the data, then use -lodepng_chunk_generate_crc */ -static unsigned lodepng_chunk_init(unsigned char** chunk, - ucvector* out, - size_t length, const char* type) { - size_t new_length = out->size; - if(lodepng_addofl(new_length, length, &new_length)) return 77; - if(lodepng_addofl(new_length, 12, &new_length)) return 77; - if(!ucvector_resize(out, new_length)) return 83; /*alloc fail*/ - *chunk = out->data + new_length - length - 12u; - - /*1: length*/ - lodepng_set32bitInt(*chunk, (unsigned)length); - - /*2: chunk name (4 letters)*/ - lodepng_memcpy(*chunk + 4, type, 4); - - return 0; -} - -/* like lodepng_chunk_create but with custom allocsize */ -static unsigned lodepng_chunk_createv(ucvector* out, - size_t length, const char* type, const unsigned char* data) { - unsigned char* chunk; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, length, type)); - - /*3: the data*/ - lodepng_memcpy(chunk + 8, data, length); - - /*4: CRC (of the chunkname characters and the data)*/ - lodepng_chunk_generate_crc(chunk); - - return 0; -} - -unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, - size_t length, const char* type, const unsigned char* data) { - ucvector v = ucvector_init(*out, *outsize); - unsigned error = lodepng_chunk_createv(&v, length, type, data); - *out = v.data; - *outsize = v.size; - return error; -} - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / Color types, channels, bits / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -/*checks if the colortype is valid and the bitdepth bd is allowed for this colortype. -Return value is a LodePNG error code.*/ -static unsigned checkColorValidity(LodePNGColorType colortype, unsigned bd) { - switch(colortype) { - case LCT_GREY: if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 || bd == 16)) return 37; break; - case LCT_RGB: if(!( bd == 8 || bd == 16)) return 37; break; - case LCT_PALETTE: if(!(bd == 1 || bd == 2 || bd == 4 || bd == 8 )) return 37; break; - case LCT_GREY_ALPHA: if(!( bd == 8 || bd == 16)) return 37; break; - case LCT_RGBA: if(!( bd == 8 || bd == 16)) return 37; break; - case LCT_MAX_OCTET_VALUE: return 31; /* invalid color type */ - default: return 31; /* invalid color type */ - } - return 0; /*allowed color type / bits combination*/ -} - -static unsigned getNumColorChannels(LodePNGColorType colortype) { - switch(colortype) { - case LCT_GREY: return 1; - case LCT_RGB: return 3; - case LCT_PALETTE: return 1; - case LCT_GREY_ALPHA: return 2; - case LCT_RGBA: return 4; - case LCT_MAX_OCTET_VALUE: return 0; /* invalid color type */ - default: return 0; /*invalid color type*/ - } -} - -static unsigned lodepng_get_bpp_lct(LodePNGColorType colortype, unsigned bitdepth) { - /*bits per pixel is amount of channels * bits per channel*/ - return getNumColorChannels(colortype) * bitdepth; -} - -/* ////////////////////////////////////////////////////////////////////////// */ - -void lodepng_color_mode_init(LodePNGColorMode* info) { - info->key_defined = 0; - info->key_r = info->key_g = info->key_b = 0; - info->colortype = LCT_RGBA; - info->bitdepth = 8; - info->palette = 0; - info->palettesize = 0; -} - -/*allocates palette memory if needed, and initializes all colors to black*/ -static void lodepng_color_mode_alloc_palette(LodePNGColorMode* info) { - size_t i; - /*if the palette is already allocated, it will have size 1024 so no reallocation needed in that case*/ - /*the palette must have room for up to 256 colors with 4 bytes each.*/ - if(!info->palette) info->palette = (unsigned char*)lodepng_malloc(1024); - if(!info->palette) return; /*alloc fail*/ - for(i = 0; i != 256; ++i) { - /*Initialize all unused colors with black, the value used for invalid palette indices. - This is an error according to the PNG spec, but common PNG decoders make it black instead. - That makes color conversion slightly faster due to no error handling needed.*/ - info->palette[i * 4 + 0] = 0; - info->palette[i * 4 + 1] = 0; - info->palette[i * 4 + 2] = 0; - info->palette[i * 4 + 3] = 255; - } -} - -void lodepng_color_mode_cleanup(LodePNGColorMode* info) { - lodepng_palette_clear(info); -} - -unsigned lodepng_color_mode_copy(LodePNGColorMode* dest, const LodePNGColorMode* source) { - lodepng_color_mode_cleanup(dest); - lodepng_memcpy(dest, source, sizeof(LodePNGColorMode)); - if(source->palette) { - dest->palette = (unsigned char*)lodepng_malloc(1024); - if(!dest->palette && source->palettesize) return 83; /*alloc fail*/ - lodepng_memcpy(dest->palette, source->palette, source->palettesize * 4); - } - return 0; -} - -LodePNGColorMode lodepng_color_mode_make(LodePNGColorType colortype, unsigned bitdepth) { - LodePNGColorMode result; - lodepng_color_mode_init(&result); - result.colortype = colortype; - result.bitdepth = bitdepth; - return result; -} - -static int lodepng_color_mode_equal(const LodePNGColorMode* a, const LodePNGColorMode* b) { - size_t i; - if(a->colortype != b->colortype) return 0; - if(a->bitdepth != b->bitdepth) return 0; - if(a->key_defined != b->key_defined) return 0; - if(a->key_defined) { - if(a->key_r != b->key_r) return 0; - if(a->key_g != b->key_g) return 0; - if(a->key_b != b->key_b) return 0; - } - if(a->palettesize != b->palettesize) return 0; - for(i = 0; i != a->palettesize * 4; ++i) { - if(a->palette[i] != b->palette[i]) return 0; - } - return 1; -} - -void lodepng_palette_clear(LodePNGColorMode* info) { - if(info->palette) lodepng_free(info->palette); - info->palette = 0; - info->palettesize = 0; -} - -unsigned lodepng_palette_add(LodePNGColorMode* info, - unsigned char r, unsigned char g, unsigned char b, unsigned char a) { - if(!info->palette) /*allocate palette if empty*/ { - lodepng_color_mode_alloc_palette(info); - if(!info->palette) return 83; /*alloc fail*/ - } - if(info->palettesize >= 256) { - return 108; /*too many palette values*/ - } - info->palette[4 * info->palettesize + 0] = r; - info->palette[4 * info->palettesize + 1] = g; - info->palette[4 * info->palettesize + 2] = b; - info->palette[4 * info->palettesize + 3] = a; - ++info->palettesize; - return 0; -} - -/*calculate bits per pixel out of colortype and bitdepth*/ -unsigned lodepng_get_bpp(const LodePNGColorMode* info) { - return lodepng_get_bpp_lct(info->colortype, info->bitdepth); -} - -unsigned lodepng_get_channels(const LodePNGColorMode* info) { - return getNumColorChannels(info->colortype); -} - -unsigned lodepng_is_greyscale_type(const LodePNGColorMode* info) { - return info->colortype == LCT_GREY || info->colortype == LCT_GREY_ALPHA; -} - -unsigned lodepng_is_alpha_type(const LodePNGColorMode* info) { - return (info->colortype & 4) != 0; /*4 or 6*/ -} - -unsigned lodepng_is_palette_type(const LodePNGColorMode* info) { - return info->colortype == LCT_PALETTE; -} - -unsigned lodepng_has_palette_alpha(const LodePNGColorMode* info) { - size_t i; - for(i = 0; i != info->palettesize; ++i) { - if(info->palette[i * 4 + 3] < 255) return 1; - } - return 0; -} - -unsigned lodepng_can_have_alpha(const LodePNGColorMode* info) { - return info->key_defined - || lodepng_is_alpha_type(info) - || lodepng_has_palette_alpha(info); -} - -static size_t lodepng_get_raw_size_lct(unsigned w, unsigned h, LodePNGColorType colortype, unsigned bitdepth) { - size_t bpp = lodepng_get_bpp_lct(colortype, bitdepth); - size_t n = (size_t)w * (size_t)h; - return ((n / 8u) * bpp) + ((n & 7u) * bpp + 7u) / 8u; -} - -size_t lodepng_get_raw_size(unsigned w, unsigned h, const LodePNGColorMode* color) { - return lodepng_get_raw_size_lct(w, h, color->colortype, color->bitdepth); -} - - -#ifdef LODEPNG_COMPILE_PNG - -/*in an idat chunk, each scanline is a multiple of 8 bits, unlike the lodepng output buffer, -and in addition has one extra byte per line: the filter byte. So this gives a larger -result than lodepng_get_raw_size. Set h to 1 to get the size of 1 row including filter byte. */ -static size_t lodepng_get_raw_size_idat(unsigned w, unsigned h, unsigned bpp) { - /* + 1 for the filter byte, and possibly plus padding bits per line. */ - /* Ignoring casts, the expression is equal to (w * bpp + 7) / 8 + 1, but avoids overflow of w * bpp */ - size_t line = ((size_t)(w / 8u) * bpp) + 1u + ((w & 7u) * bpp + 7u) / 8u; - return (size_t)h * line; -} - -#ifdef LODEPNG_COMPILE_DECODER -/*Safely checks whether size_t overflow can be caused due to amount of pixels. -This check is overcautious rather than precise. If this check indicates no overflow, -you can safely compute in a size_t (but not an unsigned): --(size_t)w * (size_t)h * 8 --amount of bytes in IDAT (including filter, padding and Adam7 bytes) --amount of bytes in raw color model -Returns 1 if overflow possible, 0 if not. -*/ -static int lodepng_pixel_overflow(unsigned w, unsigned h, - const LodePNGColorMode* pngcolor, const LodePNGColorMode* rawcolor) { - size_t bpp = LODEPNG_MAX(lodepng_get_bpp(pngcolor), lodepng_get_bpp(rawcolor)); - size_t numpixels, total; - size_t line; /* bytes per line in worst case */ - - if(lodepng_mulofl((size_t)w, (size_t)h, &numpixels)) return 1; - if(lodepng_mulofl(numpixels, 8, &total)) return 1; /* bit pointer with 8-bit color, or 8 bytes per channel color */ - - /* Bytes per scanline with the expression "(w / 8u) * bpp) + ((w & 7u) * bpp + 7u) / 8u" */ - if(lodepng_mulofl((size_t)(w / 8u), bpp, &line)) return 1; - if(lodepng_addofl(line, ((w & 7u) * bpp + 7u) / 8u, &line)) return 1; - - if(lodepng_addofl(line, 5, &line)) return 1; /* 5 bytes overhead per line: 1 filterbyte, 4 for Adam7 worst case */ - if(lodepng_mulofl(line, h, &total)) return 1; /* Total bytes in worst case */ - - return 0; /* no overflow */ -} -#endif /*LODEPNG_COMPILE_DECODER*/ -#endif /*LODEPNG_COMPILE_PNG*/ - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - -static void LodePNGUnknownChunks_init(LodePNGInfo* info) { - unsigned i; - for(i = 0; i != 3; ++i) info->unknown_chunks_data[i] = 0; - for(i = 0; i != 3; ++i) info->unknown_chunks_size[i] = 0; -} - -static void LodePNGUnknownChunks_cleanup(LodePNGInfo* info) { - unsigned i; - for(i = 0; i != 3; ++i) lodepng_free(info->unknown_chunks_data[i]); -} - -static unsigned LodePNGUnknownChunks_copy(LodePNGInfo* dest, const LodePNGInfo* src) { - unsigned i; - - LodePNGUnknownChunks_cleanup(dest); - - for(i = 0; i != 3; ++i) { - size_t j; - dest->unknown_chunks_size[i] = src->unknown_chunks_size[i]; - dest->unknown_chunks_data[i] = (unsigned char*)lodepng_malloc(src->unknown_chunks_size[i]); - if(!dest->unknown_chunks_data[i] && dest->unknown_chunks_size[i]) return 83; /*alloc fail*/ - for(j = 0; j < src->unknown_chunks_size[i]; ++j) { - dest->unknown_chunks_data[i][j] = src->unknown_chunks_data[i][j]; - } - } - - return 0; -} - -/******************************************************************************/ - -static void LodePNGText_init(LodePNGInfo* info) { - info->text_num = 0; - info->text_keys = NULL; - info->text_strings = NULL; -} - -static void LodePNGText_cleanup(LodePNGInfo* info) { - size_t i; - for(i = 0; i != info->text_num; ++i) { - lodepng_free(info->text_keys[i]); - lodepng_free(info->text_strings[i]); - } - lodepng_free(info->text_keys); - lodepng_free(info->text_strings); -} - -static unsigned LodePNGText_copy(LodePNGInfo* dest, const LodePNGInfo* source) { - size_t i = 0; - dest->text_keys = NULL; - dest->text_strings = NULL; - dest->text_num = 0; - for(i = 0; i != source->text_num; ++i) { - CERROR_TRY_RETURN(lodepng_add_text(dest, source->text_keys[i], source->text_strings[i])); - } - return 0; -} - -static unsigned lodepng_add_text_sized(LodePNGInfo* info, const char* key, const char* str, size_t size) { - char** new_keys = (char**)(lodepng_realloc(info->text_keys, sizeof(char*) * (info->text_num + 1))); - char** new_strings = (char**)(lodepng_realloc(info->text_strings, sizeof(char*) * (info->text_num + 1))); - - if(new_keys) info->text_keys = new_keys; - if(new_strings) info->text_strings = new_strings; - - if(!new_keys || !new_strings) return 83; /*alloc fail*/ - - ++info->text_num; - info->text_keys[info->text_num - 1] = alloc_string(key); - info->text_strings[info->text_num - 1] = alloc_string_sized(str, size); - if(!info->text_keys[info->text_num - 1] || !info->text_strings[info->text_num - 1]) return 83; /*alloc fail*/ - - return 0; -} - -unsigned lodepng_add_text(LodePNGInfo* info, const char* key, const char* str) { - return lodepng_add_text_sized(info, key, str, lodepng_strlen(str)); -} - -void lodepng_clear_text(LodePNGInfo* info) { - LodePNGText_cleanup(info); - /*cleanup only deconstructs, need to init again to set appropriate pointers to NULL*/ - LodePNGText_init(info); -} - -/******************************************************************************/ - -static void LodePNGIText_init(LodePNGInfo* info) { - info->itext_num = 0; - info->itext_keys = NULL; - info->itext_langtags = NULL; - info->itext_transkeys = NULL; - info->itext_strings = NULL; -} - -static void LodePNGIText_cleanup(LodePNGInfo* info) { - size_t i; - for(i = 0; i != info->itext_num; ++i) { - lodepng_free(info->itext_keys[i]); - lodepng_free(info->itext_langtags[i]); - lodepng_free(info->itext_transkeys[i]); - lodepng_free(info->itext_strings[i]); - } - lodepng_free(info->itext_keys); - lodepng_free(info->itext_langtags); - lodepng_free(info->itext_transkeys); - lodepng_free(info->itext_strings); -} - -static unsigned LodePNGIText_copy(LodePNGInfo* dest, const LodePNGInfo* source) { - size_t i = 0; - dest->itext_keys = NULL; - dest->itext_langtags = NULL; - dest->itext_transkeys = NULL; - dest->itext_strings = NULL; - dest->itext_num = 0; - for(i = 0; i != source->itext_num; ++i) { - CERROR_TRY_RETURN(lodepng_add_itext(dest, source->itext_keys[i], source->itext_langtags[i], - source->itext_transkeys[i], source->itext_strings[i])); - } - return 0; -} - -void lodepng_clear_itext(LodePNGInfo* info) { - LodePNGIText_cleanup(info); - /*cleanup only deconstructs, need to init again to set appropriate pointers to NULL*/ - LodePNGIText_init(info); -} - -static unsigned lodepng_add_itext_sized(LodePNGInfo* info, const char* key, const char* langtag, - const char* transkey, const char* str, size_t size) { - char** new_keys = (char**)(lodepng_realloc(info->itext_keys, sizeof(char*) * (info->itext_num + 1))); - char** new_langtags = (char**)(lodepng_realloc(info->itext_langtags, sizeof(char*) * (info->itext_num + 1))); - char** new_transkeys = (char**)(lodepng_realloc(info->itext_transkeys, sizeof(char*) * (info->itext_num + 1))); - char** new_strings = (char**)(lodepng_realloc(info->itext_strings, sizeof(char*) * (info->itext_num + 1))); - - if(new_keys) info->itext_keys = new_keys; - if(new_langtags) info->itext_langtags = new_langtags; - if(new_transkeys) info->itext_transkeys = new_transkeys; - if(new_strings) info->itext_strings = new_strings; - - if(!new_keys || !new_langtags || !new_transkeys || !new_strings) return 83; /*alloc fail*/ - - ++info->itext_num; - - info->itext_keys[info->itext_num - 1] = alloc_string(key); - info->itext_langtags[info->itext_num - 1] = alloc_string(langtag); - info->itext_transkeys[info->itext_num - 1] = alloc_string(transkey); - info->itext_strings[info->itext_num - 1] = alloc_string_sized(str, size); - - return 0; -} - -unsigned lodepng_add_itext(LodePNGInfo* info, const char* key, const char* langtag, - const char* transkey, const char* str) { - return lodepng_add_itext_sized(info, key, langtag, transkey, str, lodepng_strlen(str)); -} - -unsigned lodepng_set_icc(LodePNGInfo* info, const char* name, const unsigned char* profile, unsigned profile_size) { - if(info->iccp_defined) lodepng_clear_icc(info); - - if(profile_size == 0) return 123; /*invalid ICC profile size*/ - - info->iccp_name = alloc_string(name); - if(!info->iccp_name) return 83; /*alloc fail*/ - - info->iccp_profile = (unsigned char*)lodepng_malloc(profile_size); - if(!info->iccp_profile) { - lodepng_free(info->iccp_name); - return 83; /*alloc fail*/ - } - - lodepng_memcpy(info->iccp_profile, profile, profile_size); - info->iccp_profile_size = profile_size; - info->iccp_defined = 1; - - return 0; /*ok*/ -} - -static void lodepng_init_icc(LodePNGInfo* info) { - info->iccp_defined = 0; - info->iccp_name = NULL; - info->iccp_profile = NULL; - info->iccp_profile_size = 0; -} - -void lodepng_clear_icc(LodePNGInfo* info) { - lodepng_free(info->iccp_name); - lodepng_free(info->iccp_profile); - lodepng_init_icc(info); -} - -unsigned lodepng_set_exif(LodePNGInfo* info, const unsigned char* exif, unsigned exif_size) { - if(info->exif_defined) lodepng_clear_exif(info); - info->exif = (unsigned char*)lodepng_malloc(exif_size); - - if(!info->exif) return 83; /*alloc fail*/ - - lodepng_memcpy(info->exif, exif, exif_size); - info->exif_size = exif_size; - info->exif_defined = 1; - - return 0; /*ok*/ -} - -static void lodepng_init_exif(LodePNGInfo* info) { - info->exif_defined = 0; - info->exif = NULL; - info->exif_size = 0; -} - -void lodepng_clear_exif(LodePNGInfo* info) { - lodepng_free(info->exif); - lodepng_init_exif(info); -} -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - -void lodepng_info_init(LodePNGInfo* info) { - lodepng_color_mode_init(&info->color); - info->interlace_method = 0; - info->compression_method = 0; - info->filter_method = 0; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - info->background_defined = 0; - info->background_r = info->background_g = info->background_b = 0; - - LodePNGText_init(info); - LodePNGIText_init(info); - lodepng_init_icc(info); - lodepng_init_exif(info); - - info->time_defined = 0; - info->phys_defined = 0; - - info->gama_defined = 0; - info->chrm_defined = 0; - info->srgb_defined = 0; - info->cicp_defined = 0; - info->cicp_color_primaries = 0; - info->cicp_transfer_function = 0; - info->cicp_matrix_coefficients = 0; - info->cicp_video_full_range_flag = 0; - info->mdcv_defined = 0; - info->mdcv_red_x = 0; - info->mdcv_red_y = 0; - info->mdcv_green_x = 0; - info->mdcv_green_y = 0; - info->mdcv_blue_x = 0; - info->mdcv_blue_y = 0; - info->mdcv_white_x = 0; - info->mdcv_white_y = 0; - info->mdcv_max_luminance = 0; - info->mdcv_min_luminance = 0; - info->clli_defined = 0; - info->clli_max_cll = 0; - info->clli_max_fall = 0; - - info->sbit_defined = 0; - info->sbit_r = info->sbit_g = info->sbit_b = info->sbit_a = 0; - - LodePNGUnknownChunks_init(info); -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ -} - -void lodepng_info_cleanup(LodePNGInfo* info) { - lodepng_color_mode_cleanup(&info->color); -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - LodePNGText_cleanup(info); - LodePNGIText_cleanup(info); - - lodepng_clear_icc(info); - lodepng_clear_exif(info); - - LodePNGUnknownChunks_cleanup(info); -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ -} - -unsigned lodepng_info_copy(LodePNGInfo* dest, const LodePNGInfo* source) { - lodepng_info_cleanup(dest); - lodepng_memcpy(dest, source, sizeof(LodePNGInfo)); - - /*ensure to initialize all fields pointing to allocated data to NULL first*/ - lodepng_color_mode_init(&dest->color); -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - LodePNGText_init(dest); - LodePNGIText_init(dest); - lodepng_init_icc(dest); - lodepng_init_exif(dest); - LodePNGUnknownChunks_init(dest); -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - - CERROR_TRY_RETURN(lodepng_color_mode_copy(&dest->color, &source->color)); -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - CERROR_TRY_RETURN(LodePNGText_copy(dest, source)); - CERROR_TRY_RETURN(LodePNGIText_copy(dest, source)); - if(source->iccp_defined) { - CERROR_TRY_RETURN(lodepng_set_icc(dest, source->iccp_name, source->iccp_profile, source->iccp_profile_size)); - } - if(source->exif_defined) { - CERROR_TRY_RETURN(lodepng_set_exif(dest, source->exif, source->exif_size)); - } - CERROR_TRY_RETURN(LodePNGUnknownChunks_copy(dest, source)); -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - - return 0; -} - -/* ////////////////////////////////////////////////////////////////////////// */ - -/*index: bitgroup index, bits: bitgroup size(1, 2 or 4), in: bitgroup value, out: octet array to add bits to*/ -static void addColorBits(unsigned char* out, size_t index, unsigned bits, unsigned in) { - unsigned m = bits == 1 ? 7 : bits == 2 ? 3 : 1; /*8 / bits - 1*/ - /*p = the partial index in the byte, e.g. with 4 palettebits it is 0 for first half or 1 for second half*/ - unsigned p = index & m; - in &= (1u << bits) - 1u; /*filter out any other bits of the input value*/ - in = in << (bits * (m - p)); - if(p == 0) out[index * bits / 8u] = in; - else out[index * bits / 8u] |= in; -} - -typedef struct ColorTree ColorTree; - -/* -One node of a color tree -This is the data structure used to count the number of unique colors and to get a palette -index for a color. It's like an octree, but because the alpha channel is used too, each -node has 16 instead of 8 children. -*/ -struct ColorTree { - ColorTree* children[16]; /*up to 16 pointers to ColorTree of next level*/ - int index; /*the payload. Only has a meaningful value if this is in the last level*/ -}; - -static void color_tree_init(ColorTree* tree) { - lodepng_memset(tree->children, 0, 16 * sizeof(*tree->children)); - tree->index = -1; -} - -static void color_tree_cleanup(ColorTree* tree) { - int i; - for(i = 0; i != 16; ++i) { - if(tree->children[i]) { - color_tree_cleanup(tree->children[i]); - lodepng_free(tree->children[i]); - } - } -} - -/*returns -1 if color not present, its index otherwise*/ -static int color_tree_get(ColorTree* tree, unsigned char r, unsigned char g, unsigned char b, unsigned char a) { - int bit = 0; - for(bit = 0; bit < 8; ++bit) { - int i = 8 * ((r >> bit) & 1) + 4 * ((g >> bit) & 1) + 2 * ((b >> bit) & 1) + 1 * ((a >> bit) & 1); - if(!tree->children[i]) return -1; - else tree = tree->children[i]; - } - return tree ? tree->index : -1; -} - -#ifdef LODEPNG_COMPILE_ENCODER -static int color_tree_has(ColorTree* tree, unsigned char r, unsigned char g, unsigned char b, unsigned char a) { - return color_tree_get(tree, r, g, b, a) >= 0; -} -#endif /*LODEPNG_COMPILE_ENCODER*/ - -/*color is not allowed to already exist. -Index should be >= 0 (it's signed to be compatible with using -1 for "doesn't exist") -Returns error code, or 0 if ok*/ -static unsigned color_tree_add(ColorTree* tree, - unsigned char r, unsigned char g, unsigned char b, unsigned char a, unsigned index) { - int bit; - for(bit = 0; bit < 8; ++bit) { - int i = 8 * ((r >> bit) & 1) + 4 * ((g >> bit) & 1) + 2 * ((b >> bit) & 1) + 1 * ((a >> bit) & 1); - if(!tree->children[i]) { - tree->children[i] = (ColorTree*)lodepng_malloc(sizeof(ColorTree)); - if(!tree->children[i]) return 83; /*alloc fail*/ - color_tree_init(tree->children[i]); - } - tree = tree->children[i]; - } - tree->index = (int)index; - return 0; -} - -/*put a pixel, given its RGBA color, into image of any color type*/ -static unsigned rgba8ToPixel(unsigned char* out, size_t i, - const LodePNGColorMode* mode, ColorTree* tree /*for palette*/, - unsigned char r, unsigned char g, unsigned char b, unsigned char a) { - if(mode->colortype == LCT_GREY) { - unsigned char gray = r; /*((unsigned short)r + g + b) / 3u;*/ - if(mode->bitdepth == 8) out[i] = gray; - else if(mode->bitdepth == 16) out[i * 2 + 0] = out[i * 2 + 1] = gray; - else { - /*take the most significant bits of gray*/ - gray = ((unsigned)gray >> (8u - mode->bitdepth)) & ((1u << mode->bitdepth) - 1u); - addColorBits(out, i, mode->bitdepth, gray); - } - } else if(mode->colortype == LCT_RGB) { - if(mode->bitdepth == 8) { - out[i * 3 + 0] = r; - out[i * 3 + 1] = g; - out[i * 3 + 2] = b; - } else { - out[i * 6 + 0] = out[i * 6 + 1] = r; - out[i * 6 + 2] = out[i * 6 + 3] = g; - out[i * 6 + 4] = out[i * 6 + 5] = b; - } - } else if(mode->colortype == LCT_PALETTE) { - int index = color_tree_get(tree, r, g, b, a); - if(index < 0) return 82; /*color not in palette*/ - if(mode->bitdepth == 8) out[i] = index; - else addColorBits(out, i, mode->bitdepth, (unsigned)index); - } else if(mode->colortype == LCT_GREY_ALPHA) { - unsigned char gray = r; /*((unsigned short)r + g + b) / 3u;*/ - if(mode->bitdepth == 8) { - out[i * 2 + 0] = gray; - out[i * 2 + 1] = a; - } else if(mode->bitdepth == 16) { - out[i * 4 + 0] = out[i * 4 + 1] = gray; - out[i * 4 + 2] = out[i * 4 + 3] = a; - } - } else if(mode->colortype == LCT_RGBA) { - if(mode->bitdepth == 8) { - out[i * 4 + 0] = r; - out[i * 4 + 1] = g; - out[i * 4 + 2] = b; - out[i * 4 + 3] = a; - } else { - out[i * 8 + 0] = out[i * 8 + 1] = r; - out[i * 8 + 2] = out[i * 8 + 3] = g; - out[i * 8 + 4] = out[i * 8 + 5] = b; - out[i * 8 + 6] = out[i * 8 + 7] = a; - } - } - - return 0; /*no error*/ -} - -/*put a pixel, given its RGBA16 color, into image of any color 16-bitdepth type*/ -static void rgba16ToPixel(unsigned char* out, size_t i, - const LodePNGColorMode* mode, - unsigned short r, unsigned short g, unsigned short b, unsigned short a) { - if(mode->colortype == LCT_GREY) { - unsigned short gray = r; /*((unsigned)r + g + b) / 3u;*/ - out[i * 2 + 0] = (gray >> 8) & 255; - out[i * 2 + 1] = gray & 255; - } else if(mode->colortype == LCT_RGB) { - out[i * 6 + 0] = (r >> 8) & 255; - out[i * 6 + 1] = r & 255; - out[i * 6 + 2] = (g >> 8) & 255; - out[i * 6 + 3] = g & 255; - out[i * 6 + 4] = (b >> 8) & 255; - out[i * 6 + 5] = b & 255; - } else if(mode->colortype == LCT_GREY_ALPHA) { - unsigned short gray = r; /*((unsigned)r + g + b) / 3u;*/ - out[i * 4 + 0] = (gray >> 8) & 255; - out[i * 4 + 1] = gray & 255; - out[i * 4 + 2] = (a >> 8) & 255; - out[i * 4 + 3] = a & 255; - } else if(mode->colortype == LCT_RGBA) { - out[i * 8 + 0] = (r >> 8) & 255; - out[i * 8 + 1] = r & 255; - out[i * 8 + 2] = (g >> 8) & 255; - out[i * 8 + 3] = g & 255; - out[i * 8 + 4] = (b >> 8) & 255; - out[i * 8 + 5] = b & 255; - out[i * 8 + 6] = (a >> 8) & 255; - out[i * 8 + 7] = a & 255; - } -} - -/*Get RGBA8 color of pixel with index i (y * width + x) from the raw image with given color type.*/ -static void getPixelColorRGBA8(unsigned char* r, unsigned char* g, - unsigned char* b, unsigned char* a, - const unsigned char* in, size_t i, - const LodePNGColorMode* mode) { - if(mode->colortype == LCT_GREY) { - if(mode->bitdepth == 8) { - *r = *g = *b = in[i]; - if(mode->key_defined && *r == mode->key_r) *a = 0; - else *a = 255; - } else if(mode->bitdepth == 16) { - *r = *g = *b = in[i * 2 + 0]; - if(mode->key_defined && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r) *a = 0; - else *a = 255; - } else { - unsigned highest = ((1U << mode->bitdepth) - 1U); /*highest possible value for this bit depth*/ - size_t j = i * mode->bitdepth; - unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); - *r = *g = *b = (value * 255) / highest; - if(mode->key_defined && value == mode->key_r) *a = 0; - else *a = 255; - } - } else if(mode->colortype == LCT_RGB) { - if(mode->bitdepth == 8) { - *r = in[i * 3 + 0]; *g = in[i * 3 + 1]; *b = in[i * 3 + 2]; - if(mode->key_defined && *r == mode->key_r && *g == mode->key_g && *b == mode->key_b) *a = 0; - else *a = 255; - } else { - *r = in[i * 6 + 0]; - *g = in[i * 6 + 2]; - *b = in[i * 6 + 4]; - if(mode->key_defined && 256U * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r - && 256U * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g - && 256U * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b) *a = 0; - else *a = 255; - } - } else if(mode->colortype == LCT_PALETTE) { - unsigned index; - if(mode->bitdepth == 8) index = in[i]; - else { - size_t j = i * mode->bitdepth; - index = readBitsFromReversedStream(&j, in, mode->bitdepth); - } - /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ - *r = mode->palette[index * 4 + 0]; - *g = mode->palette[index * 4 + 1]; - *b = mode->palette[index * 4 + 2]; - *a = mode->palette[index * 4 + 3]; - } else if(mode->colortype == LCT_GREY_ALPHA) { - if(mode->bitdepth == 8) { - *r = *g = *b = in[i * 2 + 0]; - *a = in[i * 2 + 1]; - } else { - *r = *g = *b = in[i * 4 + 0]; - *a = in[i * 4 + 2]; - } - } else if(mode->colortype == LCT_RGBA) { - if(mode->bitdepth == 8) { - *r = in[i * 4 + 0]; - *g = in[i * 4 + 1]; - *b = in[i * 4 + 2]; - *a = in[i * 4 + 3]; - } else { - *r = in[i * 8 + 0]; - *g = in[i * 8 + 2]; - *b = in[i * 8 + 4]; - *a = in[i * 8 + 6]; - } - } -} - -/*Similar to getPixelColorRGBA8, but with all the for loops inside of the color -mode test cases, optimized to convert the colors much faster, when converting -to the common case of RGBA with 8 bit per channel. buffer must be RGBA with -enough memory.*/ -static void getPixelColorsRGBA8(unsigned char* LODEPNG_RESTRICT buffer, size_t numpixels, - const unsigned char* LODEPNG_RESTRICT in, - const LodePNGColorMode* mode) { - unsigned num_channels = 4; - size_t i; - if(mode->colortype == LCT_GREY) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i]; - buffer[3] = 255; - } - if(mode->key_defined) { - buffer -= numpixels * num_channels; - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - if(buffer[0] == mode->key_r) buffer[3] = 0; - } - } - } else if(mode->bitdepth == 16) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i * 2]; - buffer[3] = mode->key_defined && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r ? 0 : 255; - } - } else { - unsigned highest = ((1U << mode->bitdepth) - 1U); /*highest possible value for this bit depth*/ - size_t j = 0; - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); - buffer[0] = buffer[1] = buffer[2] = (value * 255) / highest; - buffer[3] = mode->key_defined && value == mode->key_r ? 0 : 255; - } - } - } else if(mode->colortype == LCT_RGB) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - lodepng_memcpy(buffer, &in[i * 3], 3); - buffer[3] = 255; - } - if(mode->key_defined) { - buffer -= numpixels * num_channels; - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - if(buffer[0] == mode->key_r && buffer[1]== mode->key_g && buffer[2] == mode->key_b) buffer[3] = 0; - } - } - } else { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = in[i * 6 + 0]; - buffer[1] = in[i * 6 + 2]; - buffer[2] = in[i * 6 + 4]; - buffer[3] = mode->key_defined - && 256U * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r - && 256U * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g - && 256U * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b ? 0 : 255; - } - } - } else if(mode->colortype == LCT_PALETTE) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - unsigned index = in[i]; - /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ - lodepng_memcpy(buffer, &mode->palette[index * 4], 4); - } - } else { - size_t j = 0; - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - unsigned index = readBitsFromReversedStream(&j, in, mode->bitdepth); - /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ - lodepng_memcpy(buffer, &mode->palette[index * 4], 4); - } - } - } else if(mode->colortype == LCT_GREY_ALPHA) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i * 2 + 0]; - buffer[3] = in[i * 2 + 1]; - } - } else { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i * 4 + 0]; - buffer[3] = in[i * 4 + 2]; - } - } - } else if(mode->colortype == LCT_RGBA) { - if(mode->bitdepth == 8) { - lodepng_memcpy(buffer, in, numpixels * 4); - } else { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = in[i * 8 + 0]; - buffer[1] = in[i * 8 + 2]; - buffer[2] = in[i * 8 + 4]; - buffer[3] = in[i * 8 + 6]; - } - } - } -} - -/*Similar to getPixelColorsRGBA8, but with 3-channel RGB output.*/ -static void getPixelColorsRGB8(unsigned char* LODEPNG_RESTRICT buffer, size_t numpixels, - const unsigned char* LODEPNG_RESTRICT in, - const LodePNGColorMode* mode) { - const unsigned num_channels = 3; - size_t i; - if(mode->colortype == LCT_GREY) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i]; - } - } else if(mode->bitdepth == 16) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i * 2]; - } - } else { - unsigned highest = ((1U << mode->bitdepth) - 1U); /*highest possible value for this bit depth*/ - size_t j = 0; - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - unsigned value = readBitsFromReversedStream(&j, in, mode->bitdepth); - buffer[0] = buffer[1] = buffer[2] = (value * 255) / highest; - } - } - } else if(mode->colortype == LCT_RGB) { - if(mode->bitdepth == 8) { - lodepng_memcpy(buffer, in, numpixels * 3); - } else { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = in[i * 6 + 0]; - buffer[1] = in[i * 6 + 2]; - buffer[2] = in[i * 6 + 4]; - } - } - } else if(mode->colortype == LCT_PALETTE) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - unsigned index = in[i]; - /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ - lodepng_memcpy(buffer, &mode->palette[index * 4], 3); - } - } else { - size_t j = 0; - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - unsigned index = readBitsFromReversedStream(&j, in, mode->bitdepth); - /*out of bounds of palette not checked: see lodepng_color_mode_alloc_palette.*/ - lodepng_memcpy(buffer, &mode->palette[index * 4], 3); - } - } - } else if(mode->colortype == LCT_GREY_ALPHA) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i * 2 + 0]; - } - } else { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = buffer[1] = buffer[2] = in[i * 4 + 0]; - } - } - } else if(mode->colortype == LCT_RGBA) { - if(mode->bitdepth == 8) { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - lodepng_memcpy(buffer, &in[i * 4], 3); - } - } else { - for(i = 0; i != numpixels; ++i, buffer += num_channels) { - buffer[0] = in[i * 8 + 0]; - buffer[1] = in[i * 8 + 2]; - buffer[2] = in[i * 8 + 4]; - } - } - } -} - -/*Get RGBA16 color of pixel with index i (y * width + x) from the raw image with -given color type, but the given color type must be 16-bit itself.*/ -static void getPixelColorRGBA16(unsigned short* r, unsigned short* g, unsigned short* b, unsigned short* a, - const unsigned char* in, size_t i, const LodePNGColorMode* mode) { - if(mode->colortype == LCT_GREY) { - *r = *g = *b = 256 * in[i * 2 + 0] + in[i * 2 + 1]; - if(mode->key_defined && 256U * in[i * 2 + 0] + in[i * 2 + 1] == mode->key_r) *a = 0; - else *a = 65535; - } else if(mode->colortype == LCT_RGB) { - *r = 256u * in[i * 6 + 0] + in[i * 6 + 1]; - *g = 256u * in[i * 6 + 2] + in[i * 6 + 3]; - *b = 256u * in[i * 6 + 4] + in[i * 6 + 5]; - if(mode->key_defined - && 256u * in[i * 6 + 0] + in[i * 6 + 1] == mode->key_r - && 256u * in[i * 6 + 2] + in[i * 6 + 3] == mode->key_g - && 256u * in[i * 6 + 4] + in[i * 6 + 5] == mode->key_b) *a = 0; - else *a = 65535; - } else if(mode->colortype == LCT_GREY_ALPHA) { - *r = *g = *b = 256u * in[i * 4 + 0] + in[i * 4 + 1]; - *a = 256u * in[i * 4 + 2] + in[i * 4 + 3]; - } else if(mode->colortype == LCT_RGBA) { - *r = 256u * in[i * 8 + 0] + in[i * 8 + 1]; - *g = 256u * in[i * 8 + 2] + in[i * 8 + 3]; - *b = 256u * in[i * 8 + 4] + in[i * 8 + 5]; - *a = 256u * in[i * 8 + 6] + in[i * 8 + 7]; - } -} - -unsigned lodepng_convert(unsigned char* out, const unsigned char* in, - const LodePNGColorMode* mode_out, const LodePNGColorMode* mode_in, - unsigned w, unsigned h) { - size_t i; - ColorTree tree; - size_t numpixels = (size_t)w * (size_t)h; - unsigned error = 0; - - if(mode_in->colortype == LCT_PALETTE && !mode_in->palette) { - return 107; /* error: must provide palette if input mode is palette */ - } - - if(lodepng_color_mode_equal(mode_out, mode_in)) { - size_t numbytes = lodepng_get_raw_size(w, h, mode_in); - lodepng_memcpy(out, in, numbytes); - return 0; - } - - if(mode_out->colortype == LCT_PALETTE) { - size_t palettesize = mode_out->palettesize; - const unsigned char* palette = mode_out->palette; - size_t palsize = (size_t)1u << mode_out->bitdepth; - /*if the user specified output palette but did not give the values, assume - they want the values of the input color type (assuming that one is palette). - Note that we never create a new palette ourselves.*/ - if(palettesize == 0) { - palettesize = mode_in->palettesize; - palette = mode_in->palette; - /*if the input was also palette with same bitdepth, then the color types are also - equal, so copy literally. This to preserve the exact indices that were in the PNG - even in case there are duplicate colors in the palette.*/ - if(mode_in->colortype == LCT_PALETTE && mode_in->bitdepth == mode_out->bitdepth) { - size_t numbytes = lodepng_get_raw_size(w, h, mode_in); - lodepng_memcpy(out, in, numbytes); - return 0; - } - } - if(palettesize < palsize) palsize = palettesize; - color_tree_init(&tree); - for(i = 0; i != palsize; ++i) { - const unsigned char* p = &palette[i * 4]; - error = color_tree_add(&tree, p[0], p[1], p[2], p[3], (unsigned)i); - if(error) break; - } - } - - if(!error) { - if(mode_in->bitdepth == 16 && mode_out->bitdepth == 16) { - for(i = 0; i != numpixels; ++i) { - unsigned short r = 0, g = 0, b = 0, a = 0; - getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); - rgba16ToPixel(out, i, mode_out, r, g, b, a); - } - } else if(mode_out->bitdepth == 8 && mode_out->colortype == LCT_RGBA) { - getPixelColorsRGBA8(out, numpixels, in, mode_in); - } else if(mode_out->bitdepth == 8 && mode_out->colortype == LCT_RGB) { - getPixelColorsRGB8(out, numpixels, in, mode_in); - } else { - unsigned char r = 0, g = 0, b = 0, a = 0; - for(i = 0; i != numpixels; ++i) { - getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); - error = rgba8ToPixel(out, i, mode_out, &tree, r, g, b, a); - if(error) break; - } - } - } - - if(mode_out->colortype == LCT_PALETTE) { - color_tree_cleanup(&tree); - } - - return error; -} - - -/* Converts a single rgb color without alpha from one type to another, color bits truncated to -their bitdepth. In case of single channel (gray or palette), only the r channel is used. Slow -function, do not use to process all pixels of an image. Alpha channel not supported on purpose: -this is for bKGD, supporting alpha may prevent it from finding a color in the palette, from the -specification it looks like bKGD should ignore the alpha values of the palette since it can use -any palette index but doesn't have an alpha channel. Idem with ignoring color key. */ -unsigned lodepng_convert_rgb( - unsigned* r_out, unsigned* g_out, unsigned* b_out, - unsigned r_in, unsigned g_in, unsigned b_in, - const LodePNGColorMode* mode_out, const LodePNGColorMode* mode_in) { - unsigned r = 0, g = 0, b = 0; - unsigned mul = 65535 / ((1u << mode_in->bitdepth) - 1u); /*65535, 21845, 4369, 257, 1*/ - unsigned shift = 16 - mode_out->bitdepth; - - if(mode_in->colortype == LCT_GREY || mode_in->colortype == LCT_GREY_ALPHA) { - r = g = b = r_in * mul; - } else if(mode_in->colortype == LCT_RGB || mode_in->colortype == LCT_RGBA) { - r = r_in * mul; - g = g_in * mul; - b = b_in * mul; - } else if(mode_in->colortype == LCT_PALETTE) { - if(r_in >= mode_in->palettesize) return 82; - r = mode_in->palette[r_in * 4 + 0] * 257u; - g = mode_in->palette[r_in * 4 + 1] * 257u; - b = mode_in->palette[r_in * 4 + 2] * 257u; - } else { - return 31; - } - - /* now convert to output format */ - if(mode_out->colortype == LCT_GREY || mode_out->colortype == LCT_GREY_ALPHA) { - *r_out = r >> shift ; - } else if(mode_out->colortype == LCT_RGB || mode_out->colortype == LCT_RGBA) { - *r_out = r >> shift ; - *g_out = g >> shift ; - *b_out = b >> shift ; - } else if(mode_out->colortype == LCT_PALETTE) { - unsigned i; - /* a 16-bit color cannot be in the palette */ - if((r >> 8) != (r & 255) || (g >> 8) != (g & 255) || (b >> 8) != (b & 255)) return 82; - for(i = 0; i < mode_out->palettesize; i++) { - unsigned j = i * 4; - if((r >> 8) == mode_out->palette[j + 0] && (g >> 8) == mode_out->palette[j + 1] && - (b >> 8) == mode_out->palette[j + 2]) { - *r_out = i; - return 0; - } - } - return 82; - } else { - return 31; - } - - return 0; -} - -#ifdef LODEPNG_COMPILE_ENCODER - -void lodepng_color_stats_init(LodePNGColorStats* stats) { - /*stats*/ - stats->colored = 0; - stats->key = 0; - stats->key_r = stats->key_g = stats->key_b = 0; - stats->alpha = 0; - stats->numcolors = 0; - stats->bits = 1; - stats->numpixels = 0; - /*settings*/ - stats->allow_palette = 1; - stats->allow_greyscale = 1; -} - -/*function used for debug purposes with C++*/ -/*void printColorStats(LodePNGColorStats* p) { - std::cout << "colored: " << (int)p->colored << ", "; - std::cout << "key: " << (int)p->key << ", "; - std::cout << "key_r: " << (int)p->key_r << ", "; - std::cout << "key_g: " << (int)p->key_g << ", "; - std::cout << "key_b: " << (int)p->key_b << ", "; - std::cout << "alpha: " << (int)p->alpha << ", "; - std::cout << "numcolors: " << (int)p->numcolors << ", "; - std::cout << "bits: " << (int)p->bits << std::endl; -}*/ - -/*Returns how many bits needed to represent given value (max 8 bit)*/ -static unsigned getValueRequiredBits(unsigned char value) { - if(value == 0 || value == 255) return 1; - /*The scaling of 2-bit and 4-bit values uses multiples of 85 and 17*/ - if(value % 17 == 0) return value % 85 == 0 ? 2 : 4; - return 8; -} - -/*stats must already have been inited. */ -unsigned lodepng_compute_color_stats(LodePNGColorStats* stats, - const unsigned char* in, unsigned w, unsigned h, - const LodePNGColorMode* mode_in) { - size_t i; - ColorTree tree; - size_t numpixels = (size_t)w * (size_t)h; - unsigned error = 0; - - /* mark things as done already if it would be impossible to have a more expensive case */ - unsigned colored_done = lodepng_is_greyscale_type(mode_in) ? 1 : 0; - unsigned alpha_done = lodepng_can_have_alpha(mode_in) ? 0 : 1; - unsigned numcolors_done = 0; - unsigned bpp = lodepng_get_bpp(mode_in); - unsigned bits_done = (stats->bits == 1 && bpp == 1) ? 1 : 0; - unsigned sixteen = 0; /* whether the input image is 16 bit */ - unsigned maxnumcolors = 257; - if(bpp <= 8) maxnumcolors = LODEPNG_MIN(257, stats->numcolors + (1u << bpp)); - - stats->numpixels += numpixels; - - /*if palette not allowed, no need to compute numcolors*/ - if(!stats->allow_palette) numcolors_done = 1; - - color_tree_init(&tree); - - /*If the stats was already filled in from previous data, fill its palette in tree - and mark things as done already if we know they are the most expensive case already*/ - if(stats->alpha) alpha_done = 1; - if(stats->colored) colored_done = 1; - if(stats->bits == 16) numcolors_done = 1; - if(stats->bits >= bpp) bits_done = 1; - if(stats->numcolors >= maxnumcolors) numcolors_done = 1; - - if(!numcolors_done) { - for(i = 0; i < stats->numcolors; i++) { - const unsigned char* color = &stats->palette[i * 4]; - error = color_tree_add(&tree, color[0], color[1], color[2], color[3], (unsigned)i); - if(error) goto cleanup; - } - } - - /*Check if the 16-bit input is truly 16-bit*/ - if(mode_in->bitdepth == 16 && !sixteen) { - unsigned short r = 0, g = 0, b = 0, a = 0; - for(i = 0; i != numpixels; ++i) { - getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); - if((r & 255) != ((r >> 8) & 255) || (g & 255) != ((g >> 8) & 255) || - (b & 255) != ((b >> 8) & 255) || (a & 255) != ((a >> 8) & 255)) /*first and second byte differ*/ { - stats->bits = 16; - sixteen = 1; - bits_done = 1; - numcolors_done = 1; /*counting colors no longer useful, palette doesn't support 16-bit*/ - break; - } - } - } - - if(sixteen) { - unsigned short r = 0, g = 0, b = 0, a = 0; - - for(i = 0; i != numpixels; ++i) { - getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); - - if(!colored_done && (r != g || r != b)) { - stats->colored = 1; - colored_done = 1; - } - - if(!alpha_done) { - unsigned matchkey = (r == stats->key_r && g == stats->key_g && b == stats->key_b); - if(a != 65535 && (a != 0 || (stats->key && !matchkey))) { - stats->alpha = 1; - stats->key = 0; - alpha_done = 1; - } else if(a == 0 && !stats->alpha && !stats->key) { - stats->key = 1; - stats->key_r = r; - stats->key_g = g; - stats->key_b = b; - } else if(a == 65535 && stats->key && matchkey) { - /* Color key cannot be used if an opaque pixel also has that RGB color. */ - stats->alpha = 1; - stats->key = 0; - alpha_done = 1; - } - } - if(alpha_done && numcolors_done && colored_done && bits_done) break; - } - - if(stats->key && !stats->alpha) { - for(i = 0; i != numpixels; ++i) { - getPixelColorRGBA16(&r, &g, &b, &a, in, i, mode_in); - if(a != 0 && r == stats->key_r && g == stats->key_g && b == stats->key_b) { - /* Color key cannot be used if an opaque pixel also has that RGB color. */ - stats->alpha = 1; - stats->key = 0; - alpha_done = 1; - } - } - } - } else /* < 16-bit */ { - unsigned char r = 0, g = 0, b = 0, a = 0; - unsigned char pr = 0, pg = 0, pb = 0, pa = 0; - for(i = 0; i != numpixels; ++i) { - getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); - - /*skip if color same as before, this speeds up large non-photographic - images with many same colors by avoiding 'color_tree_has' below */ - if(i != 0 && r == pr && g == pg && b == pb && a == pa) continue; - pr = r; - pg = g; - pb = b; - pa = a; - - if(!bits_done && stats->bits < 8) { - /*only r is checked, < 8 bits is only relevant for grayscale*/ - unsigned bits = getValueRequiredBits(r); - if(bits > stats->bits) stats->bits = bits; - } - bits_done = (stats->bits >= bpp); - - if(!colored_done && (r != g || r != b)) { - stats->colored = 1; - colored_done = 1; - if(stats->bits < 8) stats->bits = 8; /*PNG has no colored modes with less than 8-bit per channel*/ - } - - if(!alpha_done) { - unsigned matchkey = (r == stats->key_r && g == stats->key_g && b == stats->key_b); - if(a != 255 && (a != 0 || (stats->key && !matchkey))) { - stats->alpha = 1; - stats->key = 0; - alpha_done = 1; - if(stats->bits < 8) stats->bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ - } else if(a == 0 && !stats->alpha && !stats->key) { - stats->key = 1; - stats->key_r = r; - stats->key_g = g; - stats->key_b = b; - } else if(a == 255 && stats->key && matchkey) { - /* Color key cannot be used if an opaque pixel also has that RGB color. */ - stats->alpha = 1; - stats->key = 0; - alpha_done = 1; - if(stats->bits < 8) stats->bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ - } - } - - if(!numcolors_done) { - if(!color_tree_has(&tree, r, g, b, a)) { - error = color_tree_add(&tree, r, g, b, a, stats->numcolors); - if(error) goto cleanup; - if(stats->numcolors < 256) { - unsigned char* p = stats->palette; - unsigned n = stats->numcolors; - p[n * 4 + 0] = r; - p[n * 4 + 1] = g; - p[n * 4 + 2] = b; - p[n * 4 + 3] = a; - } - ++stats->numcolors; - numcolors_done = stats->numcolors >= maxnumcolors; - } - } - - if(alpha_done && numcolors_done && colored_done && bits_done) break; - } - - if(stats->key && !stats->alpha) { - for(i = 0; i != numpixels; ++i) { - getPixelColorRGBA8(&r, &g, &b, &a, in, i, mode_in); - if(a != 0 && r == stats->key_r && g == stats->key_g && b == stats->key_b) { - /* Color key cannot be used if an opaque pixel also has that RGB color. */ - stats->alpha = 1; - stats->key = 0; - alpha_done = 1; - if(stats->bits < 8) stats->bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ - } - } - } - - /*make the stats's key always 16-bit for consistency - repeat each byte twice*/ - stats->key_r += (stats->key_r << 8); - stats->key_g += (stats->key_g << 8); - stats->key_b += (stats->key_b << 8); - } - -cleanup: - color_tree_cleanup(&tree); - return error; -} - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS -/*Adds a single color to the color stats. The stats must already have been inited. The color must be given as 16-bit -(with 2 bytes repeating for 8-bit and 65535 for opaque alpha channel). This function is expensive, do not call it for -all pixels of an image but only for a few additional values. */ -static unsigned lodepng_color_stats_add(LodePNGColorStats* stats, - unsigned r, unsigned g, unsigned b, unsigned a) { - unsigned error = 0; - unsigned char image[8]; - LodePNGColorMode mode; - lodepng_color_mode_init(&mode); - image[0] = r >> 8; image[1] = r; image[2] = g >> 8; image[3] = g; - image[4] = b >> 8; image[5] = b; image[6] = a >> 8; image[7] = a; - mode.bitdepth = 16; - mode.colortype = LCT_RGBA; - error = lodepng_compute_color_stats(stats, image, 1, 1, &mode); - lodepng_color_mode_cleanup(&mode); - return error; -} -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - -/*Computes a minimal PNG color model that can contain all colors as indicated by the stats. -The stats should be computed with lodepng_compute_color_stats. -mode_in is raw color profile of the image the stats were computed on, to copy palette order from when relevant. -Minimal PNG color model means the color type and bit depth that gives smallest amount of bits in the output image, -e.g. gray if only grayscale pixels, palette if less than 256 colors, color key if only single transparent color, ... -This is used if auto_convert is enabled (it is by default). -*/ -static unsigned auto_choose_color(LodePNGColorMode* mode_out, - const LodePNGColorMode* mode_in, - const LodePNGColorStats* stats) { - unsigned error = 0; - unsigned palettebits; - size_t i, n; - size_t numpixels = stats->numpixels; - unsigned palette_ok, gray_ok; - - unsigned alpha = stats->alpha; - unsigned key = stats->key; - unsigned bits = stats->bits; - - mode_out->key_defined = 0; - - if(key && numpixels <= 16) { - alpha = 1; /*too few pixels to justify tRNS chunk overhead*/ - key = 0; - if(bits < 8) bits = 8; /*PNG has no alphachannel modes with less than 8-bit per channel*/ - } - - gray_ok = !stats->colored; - if(!stats->allow_greyscale) gray_ok = 0; - if(!gray_ok && bits < 8) bits = 8; - - n = stats->numcolors; - palettebits = n <= 2 ? 1 : (n <= 4 ? 2 : (n <= 16 ? 4 : 8)); - palette_ok = n <= 256 && bits <= 8 && n != 0; /*n==0 means likely numcolors wasn't computed*/ - if(numpixels < n * 2) palette_ok = 0; /*don't add palette overhead if image has only a few pixels*/ - if(gray_ok && !alpha && bits <= palettebits) palette_ok = 0; /*gray is less overhead*/ - if(!stats->allow_palette) palette_ok = 0; - - if(palette_ok) { - const unsigned char* p = stats->palette; - lodepng_palette_clear(mode_out); /*remove potential earlier palette*/ - for(i = 0; i != stats->numcolors; ++i) { - error = lodepng_palette_add(mode_out, p[i * 4 + 0], p[i * 4 + 1], p[i * 4 + 2], p[i * 4 + 3]); - if(error) break; - } - - mode_out->colortype = LCT_PALETTE; - mode_out->bitdepth = palettebits; - - if(mode_in->colortype == LCT_PALETTE && mode_in->palettesize >= mode_out->palettesize - && mode_in->bitdepth == mode_out->bitdepth) { - /*If input should have same palette colors, keep original to preserve its order and prevent conversion*/ - lodepng_color_mode_cleanup(mode_out); /*clears palette, keeps the above set colortype and bitdepth fields as-is*/ - lodepng_color_mode_copy(mode_out, mode_in); - } - } else /*8-bit or 16-bit per channel*/ { - mode_out->bitdepth = bits; - mode_out->colortype = alpha ? (gray_ok ? LCT_GREY_ALPHA : LCT_RGBA) - : (gray_ok ? LCT_GREY : LCT_RGB); - if(key) { - unsigned mask = (1u << mode_out->bitdepth) - 1u; /*stats always uses 16-bit, mask converts it*/ - mode_out->key_r = stats->key_r & mask; - mode_out->key_g = stats->key_g & mask; - mode_out->key_b = stats->key_b & mask; - mode_out->key_defined = 1; - } - } - - return error; -} - -#endif /* #ifdef LODEPNG_COMPILE_ENCODER */ - -/*Paeth predictor, used by PNG filter type 4*/ -static unsigned char paethPredictor(unsigned char a, unsigned char b, unsigned char c) { - /* the subtractions of unsigned char cast it to a signed type. - With gcc, short is faster than int, with clang int is as fast (as of april 2023)*/ - short pa = (b - c) < 0 ? -(b - c) : (b - c); - short pb = (a - c) < 0 ? -(a - c) : (a - c); - /* writing it out like this compiles to something faster than introducing a temp variable*/ - short pc = (a + b - c - c) < 0 ? -(a + b - c - c) : (a + b - c - c); - /* return input value associated with smallest of pa, pb, pc (with certain priority if equal) */ - if(pb < pa) { a = b; pa = pb; } - return (pc < pa) ? c : a; -} - -/*shared values used by multiple Adam7 related functions*/ - -static const unsigned ADAM7_IX[7] = { 0, 4, 0, 2, 0, 1, 0 }; /*x start values*/ -static const unsigned ADAM7_IY[7] = { 0, 0, 4, 0, 2, 0, 1 }; /*y start values*/ -static const unsigned ADAM7_DX[7] = { 8, 8, 4, 4, 2, 2, 1 }; /*x delta values*/ -static const unsigned ADAM7_DY[7] = { 8, 8, 8, 4, 4, 2, 2 }; /*y delta values*/ - -/* -Outputs various dimensions and positions in the image related to the Adam7 reduced images. -passw: output containing the width of the 7 passes -passh: output containing the height of the 7 passes -filter_passstart: output containing the index of the start and end of each - reduced image with filter bytes -padded_passstart output containing the index of the start and end of each - reduced image when without filter bytes but with padded scanlines -passstart: output containing the index of the start and end of each reduced - image without padding between scanlines, but still padding between the images -w, h: width and height of non-interlaced image -bpp: bits per pixel -"padded" is only relevant if bpp is less than 8 and a scanline or image does not - end at a full byte -*/ -static void Adam7_getpassvalues(unsigned passw[7], unsigned passh[7], size_t filter_passstart[8], - size_t padded_passstart[8], size_t passstart[8], unsigned w, unsigned h, unsigned bpp) { - /*the passstart values have 8 values: the 8th one indicates the byte after the end of the 7th (= last) pass*/ - unsigned i; - - /*calculate width and height in pixels of each pass*/ - for(i = 0; i != 7; ++i) { - passw[i] = (w + ADAM7_DX[i] - ADAM7_IX[i] - 1) / ADAM7_DX[i]; - passh[i] = (h + ADAM7_DY[i] - ADAM7_IY[i] - 1) / ADAM7_DY[i]; - if(passw[i] == 0) passh[i] = 0; - if(passh[i] == 0) passw[i] = 0; - } - - filter_passstart[0] = padded_passstart[0] = passstart[0] = 0; - for(i = 0; i != 7; ++i) { - /*if passw[i] is 0, it's 0 bytes, not 1 (no filtertype-byte)*/ - filter_passstart[i + 1] = filter_passstart[i] - + ((passw[i] && passh[i]) ? passh[i] * (1u + (passw[i] * bpp + 7u) / 8u) : 0); - /*bits padded if needed to fill full byte at end of each scanline*/ - padded_passstart[i + 1] = padded_passstart[i] + passh[i] * ((passw[i] * bpp + 7u) / 8u); - /*only padded at end of reduced image*/ - passstart[i + 1] = passstart[i] + (passh[i] * passw[i] * bpp + 7u) / 8u; - } -} - -#ifdef LODEPNG_COMPILE_DECODER - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / PNG Decoder / */ -/* ////////////////////////////////////////////////////////////////////////// */ - -/*read the information from the header and store it in the LodePNGInfo. return value is error*/ -unsigned lodepng_inspect(unsigned* w, unsigned* h, LodePNGState* state, - const unsigned char* in, size_t insize) { - unsigned width, height; - LodePNGInfo* info = &state->info_png; - if(insize == 0 || in == 0) { - CERROR_RETURN_ERROR(state->error, 48); /*error: the given data is empty*/ - } - if(insize < 33) { - CERROR_RETURN_ERROR(state->error, 27); /*error: the data length is smaller than the length of a PNG header*/ - } - - /*when decoding a new PNG image, make sure all parameters created after previous decoding are reset*/ - /* TODO: remove this. One should use a new LodePNGState for new sessions */ - lodepng_info_cleanup(info); - lodepng_info_init(info); - - if(in[0] != 137 || in[1] != 80 || in[2] != 78 || in[3] != 71 - || in[4] != 13 || in[5] != 10 || in[6] != 26 || in[7] != 10) { - CERROR_RETURN_ERROR(state->error, 28); /*error: the first 8 bytes are not the correct PNG signature*/ - } - if(lodepng_chunk_length(in + 8) != 13) { - CERROR_RETURN_ERROR(state->error, 94); /*error: header size must be 13 bytes*/ - } - if(!lodepng_chunk_type_equals(in + 8, "IHDR")) { - CERROR_RETURN_ERROR(state->error, 29); /*error: it doesn't start with a IHDR chunk!*/ - } - - /*read the values given in the header*/ - width = lodepng_read32bitInt(&in[16]); - height = lodepng_read32bitInt(&in[20]); - /*TODO: remove the undocumented feature that allows to give null pointers to width or height*/ - if(w) *w = width; - if(h) *h = height; - info->color.bitdepth = in[24]; - info->color.colortype = (LodePNGColorType)in[25]; - info->compression_method = in[26]; - info->filter_method = in[27]; - info->interlace_method = in[28]; - - /*errors returned only after the parsing so other values are still output*/ - - /*error: invalid image size*/ - if(width == 0 || height == 0) CERROR_RETURN_ERROR(state->error, 93); - /*error: invalid colortype or bitdepth combination*/ - state->error = checkColorValidity(info->color.colortype, info->color.bitdepth); - if(state->error) return state->error; - /*error: only compression method 0 is allowed in the specification*/ - if(info->compression_method != 0) CERROR_RETURN_ERROR(state->error, 32); - /*error: only filter method 0 is allowed in the specification*/ - if(info->filter_method != 0) CERROR_RETURN_ERROR(state->error, 33); - /*error: only interlace methods 0 and 1 exist in the specification*/ - if(info->interlace_method > 1) CERROR_RETURN_ERROR(state->error, 34); - - if(!state->decoder.ignore_crc) { - unsigned crc = lodepng_read32bitInt(&in[29]); - unsigned checksum = lodepng_crc32(&in[12], 17); - if(crc != checksum) { - CERROR_RETURN_ERROR(state->error, 57); /*invalid CRC*/ - } - } - - return state->error; -} - -static unsigned unfilterScanline(unsigned char* recon, const unsigned char* scanline, const unsigned char* precon, - size_t bytewidth, unsigned char filterType, size_t length) { - /* - For PNG filter method 0 - unfilter a PNG image scanline by scanline. when the pixels are smaller than 1 byte, - the filter works byte per byte (bytewidth = 1) - precon is the previous unfiltered scanline, recon the result, scanline the current one - the incoming scanlines do NOT include the filtertype byte, that one is given in the parameter filterType instead - recon and scanline MAY be the same memory address! precon must be disjoint. - */ - - size_t i; - switch(filterType) { - case 0: - for(i = 0; i != length; ++i) recon[i] = scanline[i]; - break; - case 1: { - size_t j = 0; - for(i = 0; i != bytewidth; ++i) recon[i] = scanline[i]; - for(i = bytewidth; i != length; ++i, ++j) recon[i] = scanline[i] + recon[j]; - break; - } - case 2: - if(precon) { - for(i = 0; i != length; ++i) recon[i] = scanline[i] + precon[i]; - } else { - for(i = 0; i != length; ++i) recon[i] = scanline[i]; - } - break; - case 3: - if(precon) { - size_t j = 0; - for(i = 0; i != bytewidth; ++i) recon[i] = scanline[i] + (precon[i] >> 1u); - /* Unroll independent paths of this predictor. A 6x and 8x version is also possible but that adds - too much code. Whether this speeds up anything depends on compiler and settings. */ - if(bytewidth >= 4) { - for(; i + 3 < length; i += 4, j += 4) { - unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1], s2 = scanline[i + 2], s3 = scanline[i + 3]; - unsigned char r0 = recon[j + 0], r1 = recon[j + 1], r2 = recon[j + 2], r3 = recon[j + 3]; - unsigned char p0 = precon[i + 0], p1 = precon[i + 1], p2 = precon[i + 2], p3 = precon[i + 3]; - recon[i + 0] = s0 + ((r0 + p0) >> 1u); - recon[i + 1] = s1 + ((r1 + p1) >> 1u); - recon[i + 2] = s2 + ((r2 + p2) >> 1u); - recon[i + 3] = s3 + ((r3 + p3) >> 1u); - } - } else if(bytewidth >= 3) { - for(; i + 2 < length; i += 3, j += 3) { - unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1], s2 = scanline[i + 2]; - unsigned char r0 = recon[j + 0], r1 = recon[j + 1], r2 = recon[j + 2]; - unsigned char p0 = precon[i + 0], p1 = precon[i + 1], p2 = precon[i + 2]; - recon[i + 0] = s0 + ((r0 + p0) >> 1u); - recon[i + 1] = s1 + ((r1 + p1) >> 1u); - recon[i + 2] = s2 + ((r2 + p2) >> 1u); - } - } else if(bytewidth >= 2) { - for(; i + 1 < length; i += 2, j += 2) { - unsigned char s0 = scanline[i + 0], s1 = scanline[i + 1]; - unsigned char r0 = recon[j + 0], r1 = recon[j + 1]; - unsigned char p0 = precon[i + 0], p1 = precon[i + 1]; - recon[i + 0] = s0 + ((r0 + p0) >> 1u); - recon[i + 1] = s1 + ((r1 + p1) >> 1u); - } - } - for(; i != length; ++i, ++j) recon[i] = scanline[i] + ((recon[j] + precon[i]) >> 1u); - } else { - size_t j = 0; - for(i = 0; i != bytewidth; ++i) recon[i] = scanline[i]; - for(i = bytewidth; i != length; ++i, ++j) recon[i] = scanline[i] + (recon[j] >> 1u); - } - break; - case 4: - if(precon) { - /* Unroll independent paths of this predictor. Whether this speeds up - anything depends on compiler and settings. */ - if(bytewidth == 8) { - unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; - unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; - unsigned char a4, b4 = 0, c4, d4 = 0, a5, b5 = 0, c5, d5 = 0; - unsigned char a6, b6 = 0, c6, d6 = 0, a7, b7 = 0, c7, d7 = 0; - for(i = 0; i + 7 < length; i += 8) { - c0 = b0; c1 = b1; c2 = b2; c3 = b3; - c4 = b4; c5 = b5; c6 = b6; c7 = b7; - b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; b3 = precon[i + 3]; - b4 = precon[i + 4]; b5 = precon[i + 5]; b6 = precon[i + 6]; b7 = precon[i + 7]; - a0 = d0; a1 = d1; a2 = d2; a3 = d3; - a4 = d4; a5 = d5; a6 = d6; a7 = d7; - d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); - d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); - d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); - d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); - d4 = scanline[i + 4] + paethPredictor(a4, b4, c4); - d5 = scanline[i + 5] + paethPredictor(a5, b5, c5); - d6 = scanline[i + 6] + paethPredictor(a6, b6, c6); - d7 = scanline[i + 7] + paethPredictor(a7, b7, c7); - recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; recon[i + 3] = d3; - recon[i + 4] = d4; recon[i + 5] = d5; recon[i + 6] = d6; recon[i + 7] = d7; - } - } else if(bytewidth == 6) { - unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; - unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; - unsigned char a4, b4 = 0, c4, d4 = 0, a5, b5 = 0, c5, d5 = 0; - for(i = 0; i + 5 < length; i += 6) { - c0 = b0; c1 = b1; c2 = b2; - c3 = b3; c4 = b4; c5 = b5; - b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; - b3 = precon[i + 3]; b4 = precon[i + 4]; b5 = precon[i + 5]; - a0 = d0; a1 = d1; a2 = d2; - a3 = d3; a4 = d4; a5 = d5; - d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); - d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); - d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); - d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); - d4 = scanline[i + 4] + paethPredictor(a4, b4, c4); - d5 = scanline[i + 5] + paethPredictor(a5, b5, c5); - recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; - recon[i + 3] = d3; recon[i + 4] = d4; recon[i + 5] = d5; - } - } else if(bytewidth == 4) { - unsigned char a0, b0 = 0, c0, d0 = 0, a1, b1 = 0, c1, d1 = 0; - unsigned char a2, b2 = 0, c2, d2 = 0, a3, b3 = 0, c3, d3 = 0; - for(i = 0; i + 3 < length; i += 4) { - c0 = b0; c1 = b1; c2 = b2; c3 = b3; - b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; b3 = precon[i + 3]; - a0 = d0; a1 = d1; a2 = d2; a3 = d3; - d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); - d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); - d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); - d3 = scanline[i + 3] + paethPredictor(a3, b3, c3); - recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; recon[i + 3] = d3; - } - } else if(bytewidth == 3) { - unsigned char a0, b0 = 0, c0, d0 = 0; - unsigned char a1, b1 = 0, c1, d1 = 0; - unsigned char a2, b2 = 0, c2, d2 = 0; - for(i = 0; i + 2 < length; i += 3) { - c0 = b0; c1 = b1; c2 = b2; - b0 = precon[i + 0]; b1 = precon[i + 1]; b2 = precon[i + 2]; - a0 = d0; a1 = d1; a2 = d2; - d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); - d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); - d2 = scanline[i + 2] + paethPredictor(a2, b2, c2); - recon[i + 0] = d0; recon[i + 1] = d1; recon[i + 2] = d2; - } - } else if(bytewidth == 2) { - unsigned char a0, b0 = 0, c0, d0 = 0; - unsigned char a1, b1 = 0, c1, d1 = 0; - for(i = 0; i + 1 < length; i += 2) { - c0 = b0; c1 = b1; - b0 = precon[i + 0]; - b1 = precon[i + 1]; - a0 = d0; a1 = d1; - d0 = scanline[i + 0] + paethPredictor(a0, b0, c0); - d1 = scanline[i + 1] + paethPredictor(a1, b1, c1); - recon[i + 0] = d0; - recon[i + 1] = d1; - } - } else if(bytewidth == 1) { - unsigned char a, b = 0, c, d = 0; - for(i = 0; i != length; ++i) { - c = b; - b = precon[i]; - a = d; - d = scanline[i] + paethPredictor(a, b, c); - recon[i] = d; - } - } else { - /* Normally not a possible case, but this would handle it correctly */ - for(i = 0; i != bytewidth; ++i) { - recon[i] = (scanline[i] + precon[i]); /*paethPredictor(0, precon[i], 0) is always precon[i]*/ - } - } - /* finish any remaining bytes */ - for(; i != length; ++i) { - recon[i] = (scanline[i] + paethPredictor(recon[i - bytewidth], precon[i], precon[i - bytewidth])); - } - } else { - size_t j = 0; - for(i = 0; i != bytewidth; ++i) { - recon[i] = scanline[i]; - } - for(i = bytewidth; i != length; ++i, ++j) { - /*paethPredictor(recon[i - bytewidth], 0, 0) is always recon[i - bytewidth]*/ - recon[i] = (scanline[i] + recon[j]); - } - } - break; - default: return 36; /*error: invalid filter type given*/ - } - return 0; -} - -static unsigned unfilter(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, unsigned bpp) { - /* - For PNG filter method 0 - this function unfilters a single image (e.g. without interlacing this is called once, with Adam7 seven times) - out must have enough bytes allocated already, in must have the scanlines + 1 filtertype byte per scanline - w and h are image dimensions or dimensions of reduced image, bpp is bits per pixel - in and out are allowed to be the same memory address (but aren't the same size since in has the extra filter bytes) - */ - - unsigned y; - unsigned char* prevline = 0; - - /*bytewidth is used for filtering, is 1 when bpp < 8, number of bytes per pixel otherwise*/ - size_t bytewidth = (bpp + 7u) / 8u; - /*the width of a scanline in bytes, not including the filter type*/ - size_t linebytes = lodepng_get_raw_size_idat(w, 1, bpp) - 1u; - - for(y = 0; y < h; ++y) { - size_t outindex = linebytes * y; - size_t inindex = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ - unsigned char filterType = in[inindex]; - - CERROR_TRY_RETURN(unfilterScanline(&out[outindex], &in[inindex + 1], prevline, bytewidth, filterType, linebytes)); - - prevline = &out[outindex]; - } - - return 0; -} - -/* -in: Adam7 interlaced image, with no padding bits between scanlines, but between - reduced images so that each reduced image starts at a byte. -out: the same pixels, but re-ordered so that they're now a non-interlaced image with size w*h -bpp: bits per pixel -out has the following size in bits: w * h * bpp. -in is possibly bigger due to padding bits between reduced images. -out must be big enough AND must be 0 everywhere if bpp < 8 in the current implementation -(because that's likely a little bit faster) -NOTE: comments about padding bits are only relevant if bpp < 8 -*/ -static void Adam7_deinterlace(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, unsigned bpp) { - unsigned passw[7], passh[7]; - size_t filter_passstart[8], padded_passstart[8], passstart[8]; - unsigned i; - - Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); - - if(bpp >= 8) { - for(i = 0; i != 7; ++i) { - unsigned x, y, b; - size_t bytewidth = bpp / 8u; - for(y = 0; y < passh[i]; ++y) - for(x = 0; x < passw[i]; ++x) { - size_t pixelinstart = passstart[i] + (y * passw[i] + x) * bytewidth; - size_t pixeloutstart = ((ADAM7_IY[i] + (size_t)y * ADAM7_DY[i]) * (size_t)w - + ADAM7_IX[i] + (size_t)x * ADAM7_DX[i]) * bytewidth; - for(b = 0; b < bytewidth; ++b) { - out[pixeloutstart + b] = in[pixelinstart + b]; - } - } - } - } else /*bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit pointers*/ { - for(i = 0; i != 7; ++i) { - unsigned x, y, b; - unsigned ilinebits = bpp * passw[i]; - unsigned olinebits = bpp * w; - size_t obp, ibp; /*bit pointers (for out and in buffer)*/ - for(y = 0; y < passh[i]; ++y) - for(x = 0; x < passw[i]; ++x) { - ibp = (8 * passstart[i]) + (y * ilinebits + x * bpp); - obp = (ADAM7_IY[i] + (size_t)y * ADAM7_DY[i]) * olinebits + (ADAM7_IX[i] + (size_t)x * ADAM7_DX[i]) * bpp; - for(b = 0; b < bpp; ++b) { - unsigned char bit = readBitFromReversedStream(&ibp, in); - setBitOfReversedStream(&obp, out, bit); - } - } - } - } -} - -static void removePaddingBits(unsigned char* out, const unsigned char* in, - size_t olinebits, size_t ilinebits, unsigned h) { - /* - After filtering there are still padding bits if scanlines have non multiple of 8 bit amounts. They need - to be removed (except at last scanline of (Adam7-reduced) image) before working with pure image buffers - for the Adam7 code, the color convert code and the output to the user. - in and out are allowed to be the same buffer, in may also be higher but still overlapping; in must - have >= ilinebits*h bits, out must have >= olinebits*h bits, olinebits must be <= ilinebits - also used to move bits after earlier such operations happened, e.g. in a sequence of reduced images from Adam7 - only useful if (ilinebits - olinebits) is a value in the range 1..7 - */ - unsigned y; - size_t diff = ilinebits - olinebits; - size_t ibp = 0, obp = 0; /*input and output bit pointers*/ - for(y = 0; y < h; ++y) { - size_t x; - for(x = 0; x < olinebits; ++x) { - unsigned char bit = readBitFromReversedStream(&ibp, in); - setBitOfReversedStream(&obp, out, bit); - } - ibp += diff; - } -} - -/*out must be buffer big enough to contain full image, and in must contain the full decompressed data from -the IDAT chunks (with filter index bytes and possible padding bits) -return value is error*/ -static unsigned postProcessScanlines(unsigned char* out, unsigned char* in, - unsigned w, unsigned h, const LodePNGInfo* info_png) { - /* - This function converts the filtered-padded-interlaced data into pure 2D image buffer with the PNG's colortype. - Steps: - *) if no Adam7: 1) unfilter 2) remove padding bits (= possible extra bits per scanline if bpp < 8) - *) if adam7: 1) 7x unfilter 2) 7x remove padding bits 3) Adam7_deinterlace - NOTE: the in buffer will be overwritten with intermediate data! - */ - unsigned bpp = lodepng_get_bpp(&info_png->color); - if(bpp == 0) return 31; /*error: invalid colortype*/ - - if(info_png->interlace_method == 0) { - if(bpp < 8 && w * bpp != ((w * bpp + 7u) / 8u) * 8u) { - CERROR_TRY_RETURN(unfilter(in, in, w, h, bpp)); - removePaddingBits(out, in, w * bpp, ((w * bpp + 7u) / 8u) * 8u, h); - } - /*we can immediately filter into the out buffer, no other steps needed*/ - else CERROR_TRY_RETURN(unfilter(out, in, w, h, bpp)); - } else /*interlace_method is 1 (Adam7)*/ { - unsigned passw[7], passh[7]; size_t filter_passstart[8], padded_passstart[8], passstart[8]; - unsigned i; - - Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); - - for(i = 0; i != 7; ++i) { - CERROR_TRY_RETURN(unfilter(&in[padded_passstart[i]], &in[filter_passstart[i]], passw[i], passh[i], bpp)); - /*TODO: possible efficiency improvement: if in this reduced image the bits fit nicely in 1 scanline, - move bytes instead of bits or move not at all*/ - if(bpp < 8) { - /*remove padding bits in scanlines; after this there still may be padding - bits between the different reduced images: each reduced image still starts nicely at a byte*/ - removePaddingBits(&in[passstart[i]], &in[padded_passstart[i]], passw[i] * bpp, - ((passw[i] * bpp + 7u) / 8u) * 8u, passh[i]); - } - } - - Adam7_deinterlace(out, in, w, h, bpp); - } - - return 0; -} - -static unsigned readChunk_PLTE(LodePNGColorMode* color, const unsigned char* data, size_t chunkLength) { - unsigned pos = 0, i; - color->palettesize = chunkLength / 3u; - if(color->palettesize == 0 || color->palettesize > 256) return 38; /*error: palette too small or big*/ - lodepng_color_mode_alloc_palette(color); - if(!color->palette && color->palettesize) { - color->palettesize = 0; - return 83; /*alloc fail*/ - } - - for(i = 0; i != color->palettesize; ++i) { - color->palette[4 * i + 0] = data[pos++]; /*R*/ - color->palette[4 * i + 1] = data[pos++]; /*G*/ - color->palette[4 * i + 2] = data[pos++]; /*B*/ - color->palette[4 * i + 3] = 255; /*alpha*/ - } - - return 0; /* OK */ -} - -static unsigned readChunk_tRNS(LodePNGColorMode* color, const unsigned char* data, size_t chunkLength) { - unsigned i; - if(color->colortype == LCT_PALETTE) { - /*error: more alpha values given than there are palette entries*/ - if(chunkLength > color->palettesize) return 39; - - for(i = 0; i != chunkLength; ++i) color->palette[4 * i + 3] = data[i]; - } else if(color->colortype == LCT_GREY) { - /*error: this chunk must be 2 bytes for grayscale image*/ - if(chunkLength != 2) return 30; - - color->key_defined = 1; - color->key_r = color->key_g = color->key_b = 256u * data[0] + data[1]; - } else if(color->colortype == LCT_RGB) { - /*error: this chunk must be 6 bytes for RGB image*/ - if(chunkLength != 6) return 41; - - color->key_defined = 1; - color->key_r = 256u * data[0] + data[1]; - color->key_g = 256u * data[2] + data[3]; - color->key_b = 256u * data[4] + data[5]; - } - else return 42; /*error: tRNS chunk not allowed for other color models*/ - - return 0; /* OK */ -} - - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS -/*background color chunk (bKGD)*/ -static unsigned readChunk_bKGD(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(info->color.colortype == LCT_PALETTE) { - /*error: this chunk must be 1 byte for indexed color image*/ - if(chunkLength != 1) return 43; - - /*error: invalid palette index, or maybe this chunk appeared before PLTE*/ - if(data[0] >= info->color.palettesize) return 103; - - info->background_defined = 1; - info->background_r = info->background_g = info->background_b = data[0]; - } else if(info->color.colortype == LCT_GREY || info->color.colortype == LCT_GREY_ALPHA) { - /*error: this chunk must be 2 bytes for grayscale image*/ - if(chunkLength != 2) return 44; - - /*the values are truncated to bitdepth in the PNG file*/ - info->background_defined = 1; - info->background_r = info->background_g = info->background_b = 256u * data[0] + data[1]; - } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_RGBA) { - /*error: this chunk must be 6 bytes for grayscale image*/ - if(chunkLength != 6) return 45; - - /*the values are truncated to bitdepth in the PNG file*/ - info->background_defined = 1; - info->background_r = 256u * data[0] + data[1]; - info->background_g = 256u * data[2] + data[3]; - info->background_b = 256u * data[4] + data[5]; - } - - return 0; /* OK */ -} - -/*text chunk (tEXt)*/ -static unsigned readChunk_tEXt(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - unsigned error = 0; - char *key = 0, *str = 0; - - while(!error) /*not really a while loop, only used to break on error*/ { - unsigned length, string2_begin; - - length = 0; - while(length < chunkLength && data[length] != 0) ++length; - /*even though it's not allowed by the standard, no error is thrown if - there's no null termination char, if the text is empty*/ - if(length < 1 || length > 79) CERROR_BREAK(error, 89); /*keyword too short or long*/ - - key = (char*)lodepng_malloc(length + 1); - if(!key) CERROR_BREAK(error, 83); /*alloc fail*/ - - lodepng_memcpy(key, data, length); - key[length] = 0; - - string2_begin = length + 1; /*skip keyword null terminator*/ - - length = (unsigned)(chunkLength < string2_begin ? 0 : chunkLength - string2_begin); - str = (char*)lodepng_malloc(length + 1); - if(!str) CERROR_BREAK(error, 83); /*alloc fail*/ - - lodepng_memcpy(str, data + string2_begin, length); - str[length] = 0; - - error = lodepng_add_text(info, key, str); - - break; - } - - lodepng_free(key); - lodepng_free(str); - - return error; -} - -/*compressed text chunk (zTXt)*/ -static unsigned readChunk_zTXt(LodePNGInfo* info, const LodePNGDecoderSettings* decoder, - const unsigned char* data, size_t chunkLength) { - unsigned error = 0; - - /*copy the object to change parameters in it*/ - LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; - - unsigned length, string2_begin; - char *key = 0; - unsigned char* str = 0; - size_t size = 0; - - while(!error) /*not really a while loop, only used to break on error*/ { - for(length = 0; length < chunkLength && data[length] != 0; ++length) ; - if(length + 2 >= chunkLength) CERROR_BREAK(error, 75); /*no null termination, corrupt?*/ - if(length < 1 || length > 79) CERROR_BREAK(error, 89); /*keyword too short or long*/ - - key = (char*)lodepng_malloc(length + 1); - if(!key) CERROR_BREAK(error, 83); /*alloc fail*/ - - lodepng_memcpy(key, data, length); - key[length] = 0; - - if(data[length + 1] != 0) CERROR_BREAK(error, 72); /*the 0 byte indicating compression must be 0*/ - - string2_begin = length + 2; - if(string2_begin > chunkLength) CERROR_BREAK(error, 75); /*no null termination, corrupt?*/ - - length = (unsigned)chunkLength - string2_begin; - zlibsettings.max_output_size = decoder->max_text_size; - /*will fail if zlib error, e.g. if length is too small*/ - error = zlib_decompress(&str, &size, 0, &data[string2_begin], - length, &zlibsettings); - /*error: compressed text larger than decoder->max_text_size*/ - if(error && size > zlibsettings.max_output_size) error = 112; - if(error) break; - error = lodepng_add_text_sized(info, key, (char*)str, size); - break; - } - - lodepng_free(key); - lodepng_free(str); - - return error; -} - -/*international text chunk (iTXt)*/ -static unsigned readChunk_iTXt(LodePNGInfo* info, const LodePNGDecoderSettings* decoder, - const unsigned char* data, size_t chunkLength) { - unsigned error = 0; - unsigned i; - - /*copy the object to change parameters in it*/ - LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; - - unsigned length, begin, compressed; - char *key = 0, *langtag = 0, *transkey = 0; - - while(!error) /*not really a while loop, only used to break on error*/ { - /*Quick check if the chunk length isn't too small. Even without check - it'd still fail with other error checks below if it's too short. This just gives a different error code.*/ - if(chunkLength < 5) CERROR_BREAK(error, 30); /*iTXt chunk too short*/ - - /*read the key*/ - for(length = 0; length < chunkLength && data[length] != 0; ++length) ; - if(length + 3 >= chunkLength) CERROR_BREAK(error, 75); /*no null termination char, corrupt?*/ - if(length < 1 || length > 79) CERROR_BREAK(error, 89); /*keyword too short or long*/ - - key = (char*)lodepng_malloc(length + 1); - if(!key) CERROR_BREAK(error, 83); /*alloc fail*/ - - lodepng_memcpy(key, data, length); - key[length] = 0; - - /*read the compression method*/ - compressed = data[length + 1]; - if(data[length + 2] != 0) CERROR_BREAK(error, 72); /*the 0 byte indicating compression must be 0*/ - - /*even though it's not allowed by the standard, no error is thrown if - there's no null termination char, if the text is empty for the next 3 texts*/ - - /*read the langtag*/ - begin = length + 3; - length = 0; - for(i = begin; i < chunkLength && data[i] != 0; ++i) ++length; - - langtag = (char*)lodepng_malloc(length + 1); - if(!langtag) CERROR_BREAK(error, 83); /*alloc fail*/ - - lodepng_memcpy(langtag, data + begin, length); - langtag[length] = 0; - - /*read the transkey*/ - begin += length + 1; - length = 0; - for(i = begin; i < chunkLength && data[i] != 0; ++i) ++length; - - transkey = (char*)lodepng_malloc(length + 1); - if(!transkey) CERROR_BREAK(error, 83); /*alloc fail*/ - - lodepng_memcpy(transkey, data + begin, length); - transkey[length] = 0; - - /*read the actual text*/ - begin += length + 1; - - length = (unsigned)chunkLength < begin ? 0 : (unsigned)chunkLength - begin; - - if(compressed) { - unsigned char* str = 0; - size_t size = 0; - zlibsettings.max_output_size = decoder->max_text_size; - /*will fail if zlib error, e.g. if length is too small*/ - error = zlib_decompress(&str, &size, 0, &data[begin], - length, &zlibsettings); - /*error: compressed text larger than decoder->max_text_size*/ - if(error && size > zlibsettings.max_output_size) error = 112; - if(!error) error = lodepng_add_itext_sized(info, key, langtag, transkey, (char*)str, size); - lodepng_free(str); - } else { - error = lodepng_add_itext_sized(info, key, langtag, transkey, (const char*)(data + begin), length); - } - - break; - } - - lodepng_free(key); - lodepng_free(langtag); - lodepng_free(transkey); - - return error; -} - -static unsigned readChunk_tIME(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 7) return 73; /*invalid tIME chunk size*/ - - info->time_defined = 1; - info->time.year = 256u * data[0] + data[1]; - info->time.month = data[2]; - info->time.day = data[3]; - info->time.hour = data[4]; - info->time.minute = data[5]; - info->time.second = data[6]; - - return 0; /* OK */ -} - -static unsigned readChunk_pHYs(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 9) return 74; /*invalid pHYs chunk size*/ - - info->phys_defined = 1; - info->phys_x = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; - info->phys_y = 16777216u * data[4] + 65536u * data[5] + 256u * data[6] + data[7]; - info->phys_unit = data[8]; - - return 0; /* OK */ -} - -static unsigned readChunk_gAMA(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 4) return 96; /*invalid gAMA chunk size*/ - - info->gama_defined = 1; - info->gama_gamma = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; - - return 0; /* OK */ -} - -static unsigned readChunk_cHRM(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 32) return 97; /*invalid cHRM chunk size*/ - - info->chrm_defined = 1; - info->chrm_white_x = 16777216u * data[ 0] + 65536u * data[ 1] + 256u * data[ 2] + data[ 3]; - info->chrm_white_y = 16777216u * data[ 4] + 65536u * data[ 5] + 256u * data[ 6] + data[ 7]; - info->chrm_red_x = 16777216u * data[ 8] + 65536u * data[ 9] + 256u * data[10] + data[11]; - info->chrm_red_y = 16777216u * data[12] + 65536u * data[13] + 256u * data[14] + data[15]; - info->chrm_green_x = 16777216u * data[16] + 65536u * data[17] + 256u * data[18] + data[19]; - info->chrm_green_y = 16777216u * data[20] + 65536u * data[21] + 256u * data[22] + data[23]; - info->chrm_blue_x = 16777216u * data[24] + 65536u * data[25] + 256u * data[26] + data[27]; - info->chrm_blue_y = 16777216u * data[28] + 65536u * data[29] + 256u * data[30] + data[31]; - - return 0; /* OK */ -} - -static unsigned readChunk_sRGB(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 1) return 98; /*invalid sRGB chunk size (this one is never ignored)*/ - - info->srgb_defined = 1; - info->srgb_intent = data[0]; - - return 0; /* OK */ -} - -static unsigned readChunk_iCCP(LodePNGInfo* info, const LodePNGDecoderSettings* decoder, - const unsigned char* data, size_t chunkLength) { - unsigned error = 0; - unsigned i; - size_t size = 0; - /*copy the object to change parameters in it*/ - LodePNGDecompressSettings zlibsettings = decoder->zlibsettings; - - unsigned length, string2_begin; - - if(info->iccp_defined) lodepng_clear_icc(info); - - for(length = 0; length < chunkLength && data[length] != 0; ++length) ; - if(length + 2 >= chunkLength) return 75; /*no null termination, corrupt?*/ - if(length < 1 || length > 79) return 89; /*keyword too short or long*/ - - info->iccp_name = (char*)lodepng_malloc(length + 1); - if(!info->iccp_name) return 83; /*alloc fail*/ - - info->iccp_name[length] = 0; - for(i = 0; i != length; ++i) info->iccp_name[i] = (char)data[i]; - - if(data[length + 1] != 0) return 72; /*the 0 byte indicating compression must be 0*/ - - string2_begin = length + 2; - if(string2_begin > chunkLength) return 75; /*no null termination, corrupt?*/ - - length = (unsigned)chunkLength - string2_begin; - zlibsettings.max_output_size = decoder->max_icc_size; - error = zlib_decompress(&info->iccp_profile, &size, 0, - &data[string2_begin], - length, &zlibsettings); - /*error: ICC profile larger than decoder->max_icc_size*/ - if(error && size > zlibsettings.max_output_size) error = 113; - info->iccp_profile_size = (unsigned)size; - if(!error && !info->iccp_profile_size) error = 123; /*invalid ICC profile size*/ - - if(!error) info->iccp_defined = 1; - return error; -} - -static unsigned readChunk_cICP(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 4) return 117; /*invalid cICP chunk size*/ - - info->cicp_defined = 1; - /* No error checking for value ranges is done here, that is up to a CICP - handling library, not the PNG decoding. Just pass on the metadata. */ - info->cicp_color_primaries = data[0]; - info->cicp_transfer_function = data[1]; - info->cicp_matrix_coefficients = data[2]; - info->cicp_video_full_range_flag = data[3]; - - return 0; /* OK */ -} - -static unsigned readChunk_mDCV(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 24) return 119; /*invalid mDCV chunk size*/ - - info->mdcv_defined = 1; - info->mdcv_red_x = 256u * data[0] + data[1]; - info->mdcv_red_y = 256u * data[2] + data[3]; - info->mdcv_green_x = 256u * data[4] + data[5]; - info->mdcv_green_y = 256u * data[6] + data[7]; - info->mdcv_blue_x = 256u * data[8] + data[9]; - info->mdcv_blue_y = 256u * data[10] + data[11]; - info->mdcv_white_x = 256u * data[12] + data[13]; - info->mdcv_white_y = 256u * data[14] + data[15]; - info->mdcv_max_luminance = 16777216u * data[16] + 65536u * data[17] + 256u * data[18] + data[19]; - info->mdcv_min_luminance = 16777216u * data[20] + 65536u * data[21] + 256u * data[22] + data[23]; - - return 0; /* OK */ -} - -static unsigned readChunk_cLLI(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - if(chunkLength != 8) return 120; /*invalid cLLI chunk size*/ - - info->clli_defined = 1; - info->clli_max_cll = 16777216u * data[0] + 65536u * data[1] + 256u * data[2] + data[3]; - info->clli_max_fall = 16777216u * data[4] + 65536u * data[5] + 256u * data[6] + data[7]; - - return 0; /* OK */ -} - -static unsigned readChunk_eXIf(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - return lodepng_set_exif(info, data, (unsigned)chunkLength); -} - -/*significant bits chunk (sBIT)*/ -static unsigned readChunk_sBIT(LodePNGInfo* info, const unsigned char* data, size_t chunkLength) { - unsigned bitdepth = (info->color.colortype == LCT_PALETTE) ? 8 : info->color.bitdepth; - if(info->color.colortype == LCT_GREY) { - /*error: this chunk must be 1 bytes for grayscale image*/ - if(chunkLength != 1) return 114; - if(data[0] == 0 || data[0] > bitdepth) return 115; - info->sbit_defined = 1; - info->sbit_r = info->sbit_g = info->sbit_b = data[0]; /*setting g and b is not required, but sensible*/ - } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_PALETTE) { - /*error: this chunk must be 3 bytes for RGB and palette image*/ - if(chunkLength != 3) return 114; - if(data[0] == 0 || data[1] == 0 || data[2] == 0) return 115; - if(data[0] > bitdepth || data[1] > bitdepth || data[2] > bitdepth) return 115; - info->sbit_defined = 1; - info->sbit_r = data[0]; - info->sbit_g = data[1]; - info->sbit_b = data[2]; - } else if(info->color.colortype == LCT_GREY_ALPHA) { - /*error: this chunk must be 2 byte for grayscale with alpha image*/ - if(chunkLength != 2) return 114; - if(data[0] == 0 || data[1] == 0) return 115; - if(data[0] > bitdepth || data[1] > bitdepth) return 115; - info->sbit_defined = 1; - info->sbit_r = info->sbit_g = info->sbit_b = data[0]; /*setting g and b is not required, but sensible*/ - info->sbit_a = data[1]; - } else if(info->color.colortype == LCT_RGBA) { - /*error: this chunk must be 4 bytes for grayscale image*/ - if(chunkLength != 4) return 114; - if(data[0] == 0 || data[1] == 0 || data[2] == 0 || data[3] == 0) return 115; - if(data[0] > bitdepth || data[1] > bitdepth || data[2] > bitdepth || data[3] > bitdepth) return 115; - info->sbit_defined = 1; - info->sbit_r = data[0]; - info->sbit_g = data[1]; - info->sbit_b = data[2]; - info->sbit_a = data[3]; - } - - return 0; /* OK */ -} -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - -unsigned lodepng_inspect_chunk(LodePNGState* state, size_t pos, - const unsigned char* in, size_t insize) { - const unsigned char* chunk = in + pos; - unsigned chunkLength; - const unsigned char* data; - unsigned unhandled = 0; - unsigned error = 0; - - if(pos + 4 > insize) return 30; - chunkLength = lodepng_chunk_length(chunk); - if(chunkLength > 2147483647) return 63; - data = lodepng_chunk_data_const(chunk); - if(chunkLength + 12 > insize - pos) return 30; - - if(lodepng_chunk_type_equals(chunk, "PLTE")) { - error = readChunk_PLTE(&state->info_png.color, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "tRNS")) { - error = readChunk_tRNS(&state->info_png.color, data, chunkLength); -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - } else if(lodepng_chunk_type_equals(chunk, "bKGD")) { - error = readChunk_bKGD(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "tEXt")) { - error = readChunk_tEXt(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "zTXt")) { - error = readChunk_zTXt(&state->info_png, &state->decoder, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "iTXt")) { - error = readChunk_iTXt(&state->info_png, &state->decoder, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "tIME")) { - error = readChunk_tIME(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "pHYs")) { - error = readChunk_pHYs(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "gAMA")) { - error = readChunk_gAMA(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "cHRM")) { - error = readChunk_cHRM(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "sRGB")) { - error = readChunk_sRGB(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "iCCP")) { - error = readChunk_iCCP(&state->info_png, &state->decoder, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "cICP")) { - error = readChunk_cICP(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "mDCV")) { - error = readChunk_mDCV(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "cLLI")) { - error = readChunk_cLLI(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "eXIf")) { - error = readChunk_eXIf(&state->info_png, data, chunkLength); - } else if(lodepng_chunk_type_equals(chunk, "sBIT")) { - error = readChunk_sBIT(&state->info_png, data, chunkLength); -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - } else { - /* unhandled chunk is ok (is not an error) */ - unhandled = 1; - } - - if(!error && !unhandled && !state->decoder.ignore_crc) { - if(lodepng_chunk_check_crc(chunk)) return 57; /*invalid CRC*/ - } - - return error; -} - -/*read a PNG, the result will be in the same color type as the PNG (hence "generic")*/ -static void decodeGeneric(unsigned char** out, unsigned* w, unsigned* h, - LodePNGState* state, - const unsigned char* in, size_t insize) { - unsigned char IEND = 0; - const unsigned char* chunk; /*points to beginning of next chunk*/ - unsigned char* idat; /*the data from idat chunks, zlib compressed*/ - size_t idatsize = 0; - unsigned char* scanlines = 0; - size_t scanlines_size = 0, expected_size = 0; - size_t outsize = 0; - - /*for unknown chunk order*/ - unsigned unknown = 0; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - unsigned critical_pos = 1; /*1 = after IHDR, 2 = after PLTE, 3 = after IDAT*/ -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - - - /* safe output values in case error happens */ - *out = 0; - *w = *h = 0; - - state->error = lodepng_inspect(w, h, state, in, insize); /*reads header and resets other parameters in state->info_png*/ - if(state->error) return; - - if(lodepng_pixel_overflow(*w, *h, &state->info_png.color, &state->info_raw)) { - CERROR_RETURN(state->error, 92); /*overflow possible due to amount of pixels*/ - } - - /*the input filesize is a safe upper bound for the sum of idat chunks size*/ - idat = (unsigned char*)lodepng_malloc(insize); - if(!idat) CERROR_RETURN(state->error, 83); /*alloc fail*/ - - chunk = &in[33]; /*first byte of the first chunk after the header*/ - - /*loop through the chunks, ignoring unknown chunks and stopping at IEND chunk. - IDAT data is put at the start of the in buffer*/ - while(!IEND && !state->error) { - unsigned chunkLength; - const unsigned char* data; /*the data in the chunk*/ - size_t pos = (size_t)(chunk - in); - - /*error: next chunk out of bounds of the in buffer*/ - if(chunk < in || pos + 12 > insize) { - if(state->decoder.ignore_end) break; /*other errors may still happen though*/ - CERROR_BREAK(state->error, 30); - } - - /*length of the data of the chunk, excluding the 12 bytes for length, chunk type and CRC*/ - chunkLength = lodepng_chunk_length(chunk); - /*error: chunk length larger than the max PNG chunk size*/ - if(chunkLength > 2147483647) { - if(state->decoder.ignore_end) break; /*other errors may still happen though*/ - CERROR_BREAK(state->error, 63); - } - - if(pos + (size_t)chunkLength + 12 > insize || pos + (size_t)chunkLength + 12 < pos) { - CERROR_BREAK(state->error, 64); /*error: size of the in buffer too small to contain next chunk (or int overflow)*/ - } - - data = lodepng_chunk_data_const(chunk); - - unknown = 0; - - /*IDAT chunk, containing compressed image data*/ - if(lodepng_chunk_type_equals(chunk, "IDAT")) { - size_t newsize; - if(lodepng_addofl(idatsize, chunkLength, &newsize)) CERROR_BREAK(state->error, 95); - if(newsize > insize) CERROR_BREAK(state->error, 95); - lodepng_memcpy(idat + idatsize, data, chunkLength); - idatsize += chunkLength; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - critical_pos = 3; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - } else if(lodepng_chunk_type_equals(chunk, "IEND")) { - /*IEND chunk*/ - IEND = 1; - } else if(lodepng_chunk_type_equals(chunk, "PLTE")) { - /*palette chunk (PLTE)*/ - state->error = readChunk_PLTE(&state->info_png.color, data, chunkLength); - if(state->error) break; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - critical_pos = 2; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - } else if(lodepng_chunk_type_equals(chunk, "tRNS")) { - /*palette transparency chunk (tRNS). Even though this one is an ancillary chunk , it is still compiled - in without 'LODEPNG_COMPILE_ANCILLARY_CHUNKS' because it contains essential color information that - affects the alpha channel of pixels. */ - state->error = readChunk_tRNS(&state->info_png.color, data, chunkLength); - if(state->error) break; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /*background color chunk (bKGD)*/ - } else if(lodepng_chunk_type_equals(chunk, "bKGD")) { - state->error = readChunk_bKGD(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "tEXt")) { - /*text chunk (tEXt)*/ - if(state->decoder.read_text_chunks) { - state->error = readChunk_tEXt(&state->info_png, data, chunkLength); - if(state->error) break; - } - } else if(lodepng_chunk_type_equals(chunk, "zTXt")) { - /*compressed text chunk (zTXt)*/ - if(state->decoder.read_text_chunks) { - state->error = readChunk_zTXt(&state->info_png, &state->decoder, data, chunkLength); - if(state->error) break; - } - } else if(lodepng_chunk_type_equals(chunk, "iTXt")) { - /*international text chunk (iTXt)*/ - if(state->decoder.read_text_chunks) { - state->error = readChunk_iTXt(&state->info_png, &state->decoder, data, chunkLength); - if(state->error) break; - } - } else if(lodepng_chunk_type_equals(chunk, "tIME")) { - state->error = readChunk_tIME(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "pHYs")) { - state->error = readChunk_pHYs(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "gAMA")) { - state->error = readChunk_gAMA(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "cHRM")) { - state->error = readChunk_cHRM(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "sRGB")) { - state->error = readChunk_sRGB(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "iCCP")) { - state->error = readChunk_iCCP(&state->info_png, &state->decoder, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "cICP")) { - state->error = readChunk_cICP(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "mDCV")) { - state->error = readChunk_mDCV(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "cLLI")) { - state->error = readChunk_cLLI(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "eXIf")) { - state->error = readChunk_eXIf(&state->info_png, data, chunkLength); - if(state->error) break; - } else if(lodepng_chunk_type_equals(chunk, "sBIT")) { - state->error = readChunk_sBIT(&state->info_png, data, chunkLength); - if(state->error) break; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - } else /*it's not an implemented chunk type, so ignore it: skip over the data*/ { - if(!lodepng_chunk_type_name_valid(chunk)) { - CERROR_BREAK(state->error, 121); /* invalid chunk type name */ - } - if(lodepng_chunk_reserved(chunk)) { - CERROR_BREAK(state->error, 122); /* invalid third lowercase character */ - } - - /*error: unknown critical chunk (5th bit of first byte of chunk type is 0)*/ - if(!state->decoder.ignore_critical && !lodepng_chunk_ancillary(chunk)) { - CERROR_BREAK(state->error, 69); - } - - unknown = 1; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - if(state->decoder.remember_unknown_chunks) { - state->error = lodepng_chunk_append(&state->info_png.unknown_chunks_data[critical_pos - 1], - &state->info_png.unknown_chunks_size[critical_pos - 1], chunk); - if(state->error) break; - } -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - } - - if(!state->decoder.ignore_crc && !unknown) /*check CRC if wanted, only on known chunk types*/ { - if(lodepng_chunk_check_crc(chunk)) CERROR_BREAK(state->error, 57); /*invalid CRC*/ - } - - if(!IEND) chunk = lodepng_chunk_next_const(chunk, in + insize); - } - - if(!state->error && state->info_png.color.colortype == LCT_PALETTE && !state->info_png.color.palette) { - state->error = 106; /* error: PNG file must have PLTE chunk if color type is palette */ - } - - if(!state->error) { - /*predict output size, to allocate exact size for output buffer to avoid more dynamic allocation. - If the decompressed size does not match the prediction, the image must be corrupt.*/ - if(state->info_png.interlace_method == 0) { - unsigned bpp = lodepng_get_bpp(&state->info_png.color); - expected_size = lodepng_get_raw_size_idat(*w, *h, bpp); - } else { - unsigned bpp = lodepng_get_bpp(&state->info_png.color); - /*Adam-7 interlaced: expected size is the sum of the 7 sub-images sizes*/ - expected_size = 0; - expected_size += lodepng_get_raw_size_idat((*w + 7) >> 3, (*h + 7) >> 3, bpp); - if(*w > 4) expected_size += lodepng_get_raw_size_idat((*w + 3) >> 3, (*h + 7) >> 3, bpp); - expected_size += lodepng_get_raw_size_idat((*w + 3) >> 2, (*h + 3) >> 3, bpp); - if(*w > 2) expected_size += lodepng_get_raw_size_idat((*w + 1) >> 2, (*h + 3) >> 2, bpp); - expected_size += lodepng_get_raw_size_idat((*w + 1) >> 1, (*h + 1) >> 2, bpp); - if(*w > 1) expected_size += lodepng_get_raw_size_idat((*w + 0) >> 1, (*h + 1) >> 1, bpp); - expected_size += lodepng_get_raw_size_idat((*w + 0), (*h + 0) >> 1, bpp); - } - - state->error = zlib_decompress(&scanlines, &scanlines_size, expected_size, idat, idatsize, &state->decoder.zlibsettings); - } - if(!state->error && scanlines_size != expected_size) state->error = 91; /*decompressed size doesn't match prediction*/ - lodepng_free(idat); - - if(!state->error) { - outsize = lodepng_get_raw_size(*w, *h, &state->info_png.color); - *out = (unsigned char*)lodepng_malloc(outsize); - if(!*out) state->error = 83; /*alloc fail*/ - } - if(!state->error) { - lodepng_memset(*out, 0, outsize); - state->error = postProcessScanlines(*out, scanlines, *w, *h, &state->info_png); - } - lodepng_free(scanlines); -} - -unsigned lodepng_decode(unsigned char** out, unsigned* w, unsigned* h, - LodePNGState* state, - const unsigned char* in, size_t insize) { - *out = 0; - decodeGeneric(out, w, h, state, in, insize); - if(state->error) return state->error; - if(!state->decoder.color_convert || lodepng_color_mode_equal(&state->info_raw, &state->info_png.color)) { - /*same color type, no copying or converting of data needed*/ - /*store the info_png color settings on the info_raw so that the info_raw still reflects what colortype - the raw image has to the end user*/ - if(!state->decoder.color_convert) { - state->error = lodepng_color_mode_copy(&state->info_raw, &state->info_png.color); - if(state->error) return state->error; - } - } else { /*color conversion needed*/ - unsigned char* data = *out; - size_t outsize; - - /*TODO: check if this works according to the statement in the documentation: "The converter can convert - from grayscale input color type, to 8-bit grayscale or grayscale with alpha"*/ - if(!(state->info_raw.colortype == LCT_RGB || state->info_raw.colortype == LCT_RGBA) - && !(state->info_raw.bitdepth == 8)) { - return 56; /*unsupported color mode conversion*/ - } - - outsize = lodepng_get_raw_size(*w, *h, &state->info_raw); - *out = (unsigned char*)lodepng_malloc(outsize); - if(!(*out)) { - state->error = 83; /*alloc fail*/ - } - else state->error = lodepng_convert(*out, data, &state->info_raw, - &state->info_png.color, *w, *h); - lodepng_free(data); - } - return state->error; -} - -unsigned lodepng_decode_memory(unsigned char** out, unsigned* w, unsigned* h, const unsigned char* in, - size_t insize, LodePNGColorType colortype, unsigned bitdepth) { - unsigned error; - LodePNGState state; - lodepng_state_init(&state); - state.info_raw.colortype = colortype; - state.info_raw.bitdepth = bitdepth; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /*disable reading things that this function doesn't output*/ - state.decoder.read_text_chunks = 0; - state.decoder.remember_unknown_chunks = 0; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - error = lodepng_decode(out, w, h, &state, in, insize); - lodepng_state_cleanup(&state); - return error; -} - -unsigned lodepng_decode32(unsigned char** out, unsigned* w, unsigned* h, const unsigned char* in, size_t insize) { - return lodepng_decode_memory(out, w, h, in, insize, LCT_RGBA, 8); -} - -unsigned lodepng_decode24(unsigned char** out, unsigned* w, unsigned* h, const unsigned char* in, size_t insize) { - return lodepng_decode_memory(out, w, h, in, insize, LCT_RGB, 8); -} - -#ifdef LODEPNG_COMPILE_DISK -unsigned lodepng_decode_file(unsigned char** out, unsigned* w, unsigned* h, const char* filename, - LodePNGColorType colortype, unsigned bitdepth) { - unsigned char* buffer = 0; - size_t buffersize; - unsigned error; - /* safe output values in case error happens */ - *out = 0; - *w = *h = 0; - error = lodepng_load_file(&buffer, &buffersize, filename); - if(!error) error = lodepng_decode_memory(out, w, h, buffer, buffersize, colortype, bitdepth); - lodepng_free(buffer); - return error; -} - -unsigned lodepng_decode32_file(unsigned char** out, unsigned* w, unsigned* h, const char* filename) { - return lodepng_decode_file(out, w, h, filename, LCT_RGBA, 8); -} - -unsigned lodepng_decode24_file(unsigned char** out, unsigned* w, unsigned* h, const char* filename) { - return lodepng_decode_file(out, w, h, filename, LCT_RGB, 8); -} -#endif /*LODEPNG_COMPILE_DISK*/ - -void lodepng_decoder_settings_init(LodePNGDecoderSettings* settings) { - settings->color_convert = 1; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - settings->read_text_chunks = 1; - settings->remember_unknown_chunks = 0; - settings->max_text_size = 16777216; - settings->max_icc_size = 16777216; /* 16MB is much more than enough for any reasonable ICC profile */ -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - settings->ignore_crc = 0; - settings->ignore_critical = 0; - settings->ignore_end = 0; - lodepng_decompress_settings_init(&settings->zlibsettings); -} - -#endif /*LODEPNG_COMPILE_DECODER*/ - -#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) - -void lodepng_state_init(LodePNGState* state) { -#ifdef LODEPNG_COMPILE_DECODER - lodepng_decoder_settings_init(&state->decoder); -#endif /*LODEPNG_COMPILE_DECODER*/ -#ifdef LODEPNG_COMPILE_ENCODER - lodepng_encoder_settings_init(&state->encoder); -#endif /*LODEPNG_COMPILE_ENCODER*/ - lodepng_color_mode_init(&state->info_raw); - lodepng_info_init(&state->info_png); - state->error = 1; -} - -void lodepng_state_cleanup(LodePNGState* state) { - lodepng_color_mode_cleanup(&state->info_raw); - lodepng_info_cleanup(&state->info_png); -} - -unsigned lodepng_state_copy(LodePNGState* dest, const LodePNGState* source) { - lodepng_state_cleanup(dest); - *dest = *source; - lodepng_color_mode_init(&dest->info_raw); - lodepng_info_init(&dest->info_png); - dest->error = lodepng_color_mode_copy(&dest->info_raw, &source->info_raw); - if(dest->error) return dest->error; - dest->error = lodepng_info_copy(&dest->info_png, &source->info_png); - return dest->error; -} - -#endif /* defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) */ - -#ifdef LODEPNG_COMPILE_ENCODER - -/* ////////////////////////////////////////////////////////////////////////// */ -/* / PNG Encoder / */ -/* ////////////////////////////////////////////////////////////////////////// */ - - -static unsigned writeSignature(ucvector* out) { - size_t pos = out->size; - const unsigned char signature[] = {137, 80, 78, 71, 13, 10, 26, 10}; - /*8 bytes PNG signature, aka the magic bytes*/ - if(!ucvector_resize(out, out->size + 8)) return 83; /*alloc fail*/ - lodepng_memcpy(out->data + pos, signature, 8); - return 0; -} - -static unsigned addChunk_IHDR(ucvector* out, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth, unsigned interlace_method) { - unsigned char *chunk, *data; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 13, "IHDR")); - data = chunk + 8; - - lodepng_set32bitInt(data + 0, w); /*width*/ - lodepng_set32bitInt(data + 4, h); /*height*/ - data[8] = (unsigned char)bitdepth; /*bit depth*/ - data[9] = (unsigned char)colortype; /*color type*/ - data[10] = 0; /*compression method*/ - data[11] = 0; /*filter method*/ - data[12] = interlace_method; /*interlace method*/ - - lodepng_chunk_generate_crc(chunk); - return 0; -} - -/* only adds the chunk if needed (there is a key or palette with alpha) */ -static unsigned addChunk_PLTE(ucvector* out, const LodePNGColorMode* info) { - unsigned char* chunk; - size_t i, j = 8; - - if(info->palettesize == 0 || info->palettesize > 256) { - return 68; /*invalid palette size, it is only allowed to be 1-256*/ - } - - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, info->palettesize * 3, "PLTE")); - - for(i = 0; i != info->palettesize; ++i) { - /*add all channels except alpha channel*/ - chunk[j++] = info->palette[i * 4 + 0]; - chunk[j++] = info->palette[i * 4 + 1]; - chunk[j++] = info->palette[i * 4 + 2]; - } - - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_tRNS(ucvector* out, const LodePNGColorMode* info) { - unsigned char* chunk = 0; - - if(info->colortype == LCT_PALETTE) { - size_t i, amount = info->palettesize; - /*the tail of palette values that all have 255 as alpha, does not have to be encoded*/ - for(i = info->palettesize; i != 0; --i) { - if(info->palette[4 * (i - 1) + 3] != 255) break; - --amount; - } - if(amount) { - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, amount, "tRNS")); - /*add the alpha channel values from the palette*/ - for(i = 0; i != amount; ++i) chunk[8 + i] = info->palette[4 * i + 3]; - } - } else if(info->colortype == LCT_GREY) { - if(info->key_defined) { - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "tRNS")); - chunk[8] = (unsigned char)(info->key_r >> 8); - chunk[9] = (unsigned char)(info->key_r & 255); - } - } else if(info->colortype == LCT_RGB) { - if(info->key_defined) { - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 6, "tRNS")); - chunk[8] = (unsigned char)(info->key_r >> 8); - chunk[9] = (unsigned char)(info->key_r & 255); - chunk[10] = (unsigned char)(info->key_g >> 8); - chunk[11] = (unsigned char)(info->key_g & 255); - chunk[12] = (unsigned char)(info->key_b >> 8); - chunk[13] = (unsigned char)(info->key_b & 255); - } - } - - if(chunk) lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_IDAT(ucvector* out, const unsigned char* data, size_t datasize, - const LodePNGCompressSettings* zlibsettings) { - unsigned error = 0; - unsigned char* zlib = 0; - size_t pos = 0; - size_t zlibsize = 0; - /* max chunk length allowed by the specification is 2147483647 bytes */ - const size_t max_chunk_length = 2147483647u; - - error = zlib_compress(&zlib, &zlibsize, data, datasize, zlibsettings); - while(!error) { - if(zlibsize - pos > max_chunk_length) { - error = lodepng_chunk_createv(out, max_chunk_length, "IDAT", zlib + pos); - pos += max_chunk_length; - } else { - error = lodepng_chunk_createv(out, zlibsize - pos, "IDAT", zlib + pos); - break; - } - } - lodepng_free(zlib); - return error; -} - -static unsigned addChunk_IEND(ucvector* out) { - return lodepng_chunk_createv(out, 0, "IEND", 0); -} - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - -static unsigned addChunk_tEXt(ucvector* out, const char* keyword, const char* textstring) { - unsigned char* chunk = 0; - size_t keysize = lodepng_strlen(keyword), textsize = lodepng_strlen(textstring); - size_t size = keysize + 1 + textsize; - if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, size, "tEXt")); - lodepng_memcpy(chunk + 8, keyword, keysize); - chunk[8 + keysize] = 0; /*null termination char*/ - lodepng_memcpy(chunk + 9 + keysize, textstring, textsize); - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_zTXt(ucvector* out, const char* keyword, const char* textstring, - const LodePNGCompressSettings* zlibsettings) { - unsigned error = 0; - unsigned char* chunk = 0; - unsigned char* compressed = 0; - size_t compressedsize = 0; - size_t textsize = lodepng_strlen(textstring); - size_t keysize = lodepng_strlen(keyword); - if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ - - error = zlib_compress(&compressed, &compressedsize, - (const unsigned char*)textstring, textsize, zlibsettings); - if(!error) { - size_t size = keysize + 2 + compressedsize; - error = lodepng_chunk_init(&chunk, out, size, "zTXt"); - } - if(!error) { - lodepng_memcpy(chunk + 8, keyword, keysize); - chunk[8 + keysize] = 0; /*null termination char*/ - chunk[9 + keysize] = 0; /*compression method: 0*/ - lodepng_memcpy(chunk + 10 + keysize, compressed, compressedsize); - lodepng_chunk_generate_crc(chunk); - } - - lodepng_free(compressed); - return error; -} - -static unsigned addChunk_iTXt(ucvector* out, unsigned compress, const char* keyword, const char* langtag, - const char* transkey, const char* textstring, const LodePNGCompressSettings* zlibsettings) { - unsigned error = 0; - unsigned char* chunk = 0; - unsigned char* compressed = 0; - size_t compressedsize = 0; - size_t textsize = lodepng_strlen(textstring); - size_t keysize = lodepng_strlen(keyword), langsize = lodepng_strlen(langtag), transsize = lodepng_strlen(transkey); - - if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ - - if(compress) { - error = zlib_compress(&compressed, &compressedsize, - (const unsigned char*)textstring, textsize, zlibsettings); - } - if(!error) { - size_t size = keysize + 3 + langsize + 1 + transsize + 1 + (compress ? compressedsize : textsize); - error = lodepng_chunk_init(&chunk, out, size, "iTXt"); - } - if(!error) { - size_t pos = 8; - lodepng_memcpy(chunk + pos, keyword, keysize); - pos += keysize; - chunk[pos++] = 0; /*null termination char*/ - chunk[pos++] = (compress ? 1 : 0); /*compression flag*/ - chunk[pos++] = 0; /*compression method: 0*/ - lodepng_memcpy(chunk + pos, langtag, langsize); - pos += langsize; - chunk[pos++] = 0; /*null termination char*/ - lodepng_memcpy(chunk + pos, transkey, transsize); - pos += transsize; - chunk[pos++] = 0; /*null termination char*/ - if(compress) { - lodepng_memcpy(chunk + pos, compressed, compressedsize); - } else { - lodepng_memcpy(chunk + pos, textstring, textsize); - } - lodepng_chunk_generate_crc(chunk); - } - - lodepng_free(compressed); - return error; -} - -static unsigned addChunk_bKGD(ucvector* out, const LodePNGInfo* info) { - unsigned char* chunk = 0; - if(info->color.colortype == LCT_GREY || info->color.colortype == LCT_GREY_ALPHA) { - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "bKGD")); - chunk[8] = (unsigned char)(info->background_r >> 8); - chunk[9] = (unsigned char)(info->background_r & 255); - } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_RGBA) { - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 6, "bKGD")); - chunk[8] = (unsigned char)(info->background_r >> 8); - chunk[9] = (unsigned char)(info->background_r & 255); - chunk[10] = (unsigned char)(info->background_g >> 8); - chunk[11] = (unsigned char)(info->background_g & 255); - chunk[12] = (unsigned char)(info->background_b >> 8); - chunk[13] = (unsigned char)(info->background_b & 255); - } else if(info->color.colortype == LCT_PALETTE) { - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 1, "bKGD")); - chunk[8] = (unsigned char)(info->background_r & 255); /*palette index*/ - } - if(chunk) lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_tIME(ucvector* out, const LodePNGTime* time) { - unsigned char* chunk; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 7, "tIME")); - chunk[8] = (unsigned char)(time->year >> 8); - chunk[9] = (unsigned char)(time->year & 255); - chunk[10] = (unsigned char)time->month; - chunk[11] = (unsigned char)time->day; - chunk[12] = (unsigned char)time->hour; - chunk[13] = (unsigned char)time->minute; - chunk[14] = (unsigned char)time->second; - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_pHYs(ucvector* out, const LodePNGInfo* info) { - unsigned char* chunk; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 9, "pHYs")); - lodepng_set32bitInt(chunk + 8, info->phys_x); - lodepng_set32bitInt(chunk + 12, info->phys_y); - chunk[16] = info->phys_unit; - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_gAMA(ucvector* out, const LodePNGInfo* info) { - unsigned char* chunk; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "gAMA")); - lodepng_set32bitInt(chunk + 8, info->gama_gamma); - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_cHRM(ucvector* out, const LodePNGInfo* info) { - unsigned char* chunk; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 32, "cHRM")); - lodepng_set32bitInt(chunk + 8, info->chrm_white_x); - lodepng_set32bitInt(chunk + 12, info->chrm_white_y); - lodepng_set32bitInt(chunk + 16, info->chrm_red_x); - lodepng_set32bitInt(chunk + 20, info->chrm_red_y); - lodepng_set32bitInt(chunk + 24, info->chrm_green_x); - lodepng_set32bitInt(chunk + 28, info->chrm_green_y); - lodepng_set32bitInt(chunk + 32, info->chrm_blue_x); - lodepng_set32bitInt(chunk + 36, info->chrm_blue_y); - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_sRGB(ucvector* out, const LodePNGInfo* info) { - unsigned char data = info->srgb_intent; - return lodepng_chunk_createv(out, 1, "sRGB", &data); -} - -static unsigned addChunk_iCCP(ucvector* out, const LodePNGInfo* info, const LodePNGCompressSettings* zlibsettings) { - unsigned error = 0; - unsigned char* chunk = 0; - unsigned char* compressed = 0; - size_t compressedsize = 0; - size_t keysize = lodepng_strlen(info->iccp_name); - - if(keysize < 1 || keysize > 79) return 89; /*error: invalid keyword size*/ - error = zlib_compress(&compressed, &compressedsize, - info->iccp_profile, info->iccp_profile_size, zlibsettings); - if(!error) { - size_t size = keysize + 2 + compressedsize; - error = lodepng_chunk_init(&chunk, out, size, "iCCP"); - } - if(!error) { - lodepng_memcpy(chunk + 8, info->iccp_name, keysize); - chunk[8 + keysize] = 0; /*null termination char*/ - chunk[9 + keysize] = 0; /*compression method: 0*/ - lodepng_memcpy(chunk + 10 + keysize, compressed, compressedsize); - lodepng_chunk_generate_crc(chunk); - } - - lodepng_free(compressed); - return error; -} - -static unsigned addChunk_cICP(ucvector* out, const LodePNGInfo* info) { - unsigned char* chunk; - /* Allow up to 255 since they are bytes. The ITU-R-BT.709 spec has a more - restricted set of valid values for each field, but that's up to the error - handling of a CICP library, not the PNG encoding/decoding, to manage. */ - if(info->cicp_color_primaries > 255) return 116; - if(info->cicp_transfer_function > 255) return 116; - if(info->cicp_matrix_coefficients > 255) return 116; - if(info->cicp_video_full_range_flag > 255) return 116; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "cICP")); - chunk[8 + 0] = (unsigned char)info->cicp_color_primaries; - chunk[8 + 1] = (unsigned char)info->cicp_transfer_function; - chunk[8 + 2] = (unsigned char)info->cicp_matrix_coefficients; - chunk[8 + 3] = (unsigned char)info->cicp_video_full_range_flag; - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_mDCV(ucvector* out, const LodePNGInfo* info) { - unsigned char* chunk; - /* Allow up to 65535 since they are 16-bit ints. */ - if(info->mdcv_red_x > 65535) return 118; - if(info->mdcv_red_y > 65535) return 118; - if(info->mdcv_green_x > 65535) return 118; - if(info->mdcv_green_y > 65535) return 118; - if(info->mdcv_blue_x > 65535) return 118; - if(info->mdcv_blue_y > 65535) return 118; - if(info->mdcv_white_x > 65535) return 118; - if(info->mdcv_white_y > 65535) return 118; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 24, "mDCV")); - chunk[8 + 0] = (unsigned char)((info->mdcv_red_x) >> 8u); - chunk[8 + 1] = (unsigned char)(info->mdcv_red_x); - chunk[8 + 2] = (unsigned char)((info->mdcv_red_y) >> 8u); - chunk[8 + 3] = (unsigned char)(info->mdcv_red_y); - chunk[8 + 4] = (unsigned char)((info->mdcv_green_x) >> 8u); - chunk[8 + 5] = (unsigned char)(info->mdcv_green_x); - chunk[8 + 6] = (unsigned char)((info->mdcv_green_y) >> 8u); - chunk[8 + 7] = (unsigned char)(info->mdcv_green_y); - chunk[8 + 8] = (unsigned char)((info->mdcv_blue_x) >> 8u); - chunk[8 + 9] = (unsigned char)(info->mdcv_blue_x); - chunk[8 + 10] = (unsigned char)((info->mdcv_blue_y) >> 8u); - chunk[8 + 11] = (unsigned char)(info->mdcv_blue_y); - chunk[8 + 12] = (unsigned char)((info->mdcv_white_x) >> 8u); - chunk[8 + 13] = (unsigned char)(info->mdcv_white_x); - chunk[8 + 14] = (unsigned char)((info->mdcv_white_y) >> 8u); - chunk[8 + 15] = (unsigned char)(info->mdcv_white_y); - lodepng_set32bitInt(chunk + 8 + 16, info->mdcv_max_luminance); - lodepng_set32bitInt(chunk + 8 + 20, info->mdcv_min_luminance); - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_cLLI(ucvector* out, const LodePNGInfo* info) { - unsigned char* chunk; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 8, "cLLI")); - lodepng_set32bitInt(chunk + 8 + 0, info->clli_max_cll); - lodepng_set32bitInt(chunk + 8 + 4, info->clli_max_fall); - lodepng_chunk_generate_crc(chunk); - return 0; -} - -static unsigned addChunk_eXIf(ucvector* out, const LodePNGInfo* info) { - return lodepng_chunk_createv(out, info->exif_size, "eXIf", info->exif); -} - -static unsigned addChunk_sBIT(ucvector* out, const LodePNGInfo* info) { - unsigned bitdepth = (info->color.colortype == LCT_PALETTE) ? 8 : info->color.bitdepth; - unsigned char* chunk = 0; - if(info->color.colortype == LCT_GREY) { - if(info->sbit_r == 0 || info->sbit_r > bitdepth) return 115; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 1, "sBIT")); - chunk[8] = info->sbit_r; - } else if(info->color.colortype == LCT_RGB || info->color.colortype == LCT_PALETTE) { - if(info->sbit_r == 0 || info->sbit_g == 0 || info->sbit_b == 0) return 115; - if(info->sbit_r > bitdepth || info->sbit_g > bitdepth || info->sbit_b > bitdepth) return 115; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 3, "sBIT")); - chunk[8] = info->sbit_r; - chunk[9] = info->sbit_g; - chunk[10] = info->sbit_b; - } else if(info->color.colortype == LCT_GREY_ALPHA) { - if(info->sbit_r == 0 || info->sbit_a == 0) return 115; - if(info->sbit_r > bitdepth || info->sbit_a > bitdepth) return 115; - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 2, "sBIT")); - chunk[8] = info->sbit_r; - chunk[9] = info->sbit_a; - } else if(info->color.colortype == LCT_RGBA) { - if(info->sbit_r == 0 || info->sbit_g == 0 || info->sbit_b == 0 || info->sbit_a == 0 || - info->sbit_r > bitdepth || info->sbit_g > bitdepth || - info->sbit_b > bitdepth || info->sbit_a > bitdepth) { - return 115; - } - CERROR_TRY_RETURN(lodepng_chunk_init(&chunk, out, 4, "sBIT")); - chunk[8] = info->sbit_r; - chunk[9] = info->sbit_g; - chunk[10] = info->sbit_b; - chunk[11] = info->sbit_a; - } - if(chunk) lodepng_chunk_generate_crc(chunk); - return 0; -} - -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - -static void filterScanline(unsigned char* out, const unsigned char* scanline, const unsigned char* prevline, - size_t length, size_t bytewidth, unsigned char filterType) { - size_t i; - switch(filterType) { - case 0: /*None*/ - for(i = 0; i != length; ++i) out[i] = scanline[i]; - break; - case 1: /*Sub*/ - for(i = 0; i != bytewidth; ++i) out[i] = scanline[i]; - for(i = bytewidth; i < length; ++i) out[i] = scanline[i] - scanline[i - bytewidth]; - break; - case 2: /*Up*/ - if(prevline) { - for(i = 0; i != length; ++i) out[i] = scanline[i] - prevline[i]; - } else { - for(i = 0; i != length; ++i) out[i] = scanline[i]; - } - break; - case 3: /*Average*/ - if(prevline) { - for(i = 0; i != bytewidth; ++i) out[i] = scanline[i] - (prevline[i] >> 1); - for(i = bytewidth; i < length; ++i) out[i] = scanline[i] - ((scanline[i - bytewidth] + prevline[i]) >> 1); - } else { - for(i = 0; i != bytewidth; ++i) out[i] = scanline[i]; - for(i = bytewidth; i < length; ++i) out[i] = scanline[i] - (scanline[i - bytewidth] >> 1); - } - break; - case 4: /*Paeth*/ - if(prevline) { - /*paethPredictor(0, prevline[i], 0) is always prevline[i]*/ - for(i = 0; i != bytewidth; ++i) out[i] = (scanline[i] - prevline[i]); - for(i = bytewidth; i < length; ++i) { - out[i] = (scanline[i] - paethPredictor(scanline[i - bytewidth], prevline[i], prevline[i - bytewidth])); - } - } else { - for(i = 0; i != bytewidth; ++i) out[i] = scanline[i]; - /*paethPredictor(scanline[i - bytewidth], 0, 0) is always scanline[i - bytewidth]*/ - for(i = bytewidth; i < length; ++i) out[i] = (scanline[i] - scanline[i - bytewidth]); - } - break; - default: return; /*invalid filter type given*/ - } -} - -/* integer binary logarithm, max return value is 31 */ -static size_t ilog2(size_t i) { - size_t result = 0; - if(i >= 65536) { result += 16; i >>= 16; } - if(i >= 256) { result += 8; i >>= 8; } - if(i >= 16) { result += 4; i >>= 4; } - if(i >= 4) { result += 2; i >>= 2; } - if(i >= 2) { result += 1; /*i >>= 1;*/ } - return result; -} - -/* integer approximation for i * log2(i), helper function for LFS_ENTROPY */ -static size_t ilog2i(size_t i) { - size_t l; - if(i == 0) return 0; - l = ilog2(i); - /* approximate i*log2(i): l is integer logarithm, ((i - (1u << l)) << 1u) - linearly approximates the missing fractional part multiplied by i */ - return i * l + ((i - (((size_t)1) << l)) << 1u); -} - -static unsigned filter(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, - const LodePNGColorMode* color, const LodePNGEncoderSettings* settings) { - /* - For PNG filter method 0 - out must be a buffer with as size: h + (w * h * bpp + 7u) / 8u, because there are - the scanlines with 1 extra byte per scanline - */ - - unsigned bpp = lodepng_get_bpp(color); - /*the width of a scanline in bytes, not including the filter type*/ - size_t linebytes = lodepng_get_raw_size_idat(w, 1, bpp) - 1u; - - /*bytewidth is used for filtering, is 1 when bpp < 8, number of bytes per pixel otherwise*/ - size_t bytewidth = (bpp + 7u) / 8u; - const unsigned char* prevline = 0; - unsigned x, y; - unsigned error = 0; - LodePNGFilterStrategy strategy = settings->filter_strategy; - - if(settings->filter_palette_zero && (color->colortype == LCT_PALETTE || color->bitdepth < 8)) { - /*if the filter_palette_zero setting is enabled, override the filter strategy with - zero for all scanlines for palette and less-than-8-bitdepth images*/ - strategy = LFS_ZERO; - } - - if(bpp == 0) return 31; /*error: invalid color type*/ - - if(strategy >= LFS_ZERO && strategy <= LFS_FOUR) { - unsigned char type = (unsigned char)strategy; - for(y = 0; y != h; ++y) { - size_t outindex = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ - size_t inindex = linebytes * y; - out[outindex] = type; /*filter type byte*/ - filterScanline(&out[outindex + 1], &in[inindex], prevline, linebytes, bytewidth, type); - prevline = &in[inindex]; - } - } else if(strategy == LFS_MINSUM) { - /*adaptive filtering: independently for each row, try all five filter types and select the one that produces the - smallest sum of absolute values per row.*/ - unsigned char* attempt[5]; /*five filtering attempts, one for each filter type*/ - size_t smallest = 0; - unsigned char type, bestType = 0; - - for(type = 0; type != 5; ++type) { - attempt[type] = (unsigned char*)lodepng_malloc(linebytes); - if(!attempt[type]) error = 83; /*alloc fail*/ - } - - if(!error) { - for(y = 0; y != h; ++y) { - /*try the 5 filter types*/ - for(type = 0; type != 5; ++type) { - size_t sum = 0; - filterScanline(attempt[type], &in[y * linebytes], prevline, linebytes, bytewidth, type); - - /*calculate the sum of the result*/ - if(type == 0) { - for(x = 0; x != linebytes; ++x) sum += (unsigned char)(attempt[type][x]); - } else { - for(x = 0; x != linebytes; ++x) { - /*For differences, each byte should be treated as signed, values above 127 are negative - (converted to signed char). Filtertype 0 isn't a difference though, so use unsigned there. - This means filtertype 0 is almost never chosen, but that is justified.*/ - unsigned char s = attempt[type][x]; - sum += s < 128 ? s : (255U - s); - } - } - - /*check if this is smallest sum (or if type == 0 it's the first case so always store the values)*/ - if(type == 0 || sum < smallest) { - bestType = type; - smallest = sum; - } - } - - prevline = &in[y * linebytes]; - - /*now fill the out values*/ - out[y * (linebytes + 1)] = bestType; /*the first byte of a scanline will be the filter type*/ - for(x = 0; x != linebytes; ++x) out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; - } - } - - for(type = 0; type != 5; ++type) lodepng_free(attempt[type]); - } else if(strategy == LFS_ENTROPY) { - unsigned char* attempt[5]; /*five filtering attempts, one for each filter type*/ - size_t bestSum = 0; - unsigned type, bestType = 0; - unsigned count[256]; - - for(type = 0; type != 5; ++type) { - attempt[type] = (unsigned char*)lodepng_malloc(linebytes); - if(!attempt[type]) error = 83; /*alloc fail*/ - } - - if(!error) { - for(y = 0; y != h; ++y) { - /*try the 5 filter types*/ - for(type = 0; type != 5; ++type) { - size_t sum = 0; - filterScanline(attempt[type], &in[y * linebytes], prevline, linebytes, bytewidth, type); - lodepng_memset(count, 0, 256 * sizeof(*count)); - for(x = 0; x != linebytes; ++x) ++count[attempt[type][x]]; - ++count[type]; /*the filter type itself is part of the scanline*/ - for(x = 0; x != 256; ++x) { - sum += ilog2i(count[x]); - } - /*check if this is smallest sum (or if type == 0 it's the first case so always store the values)*/ - if(type == 0 || sum > bestSum) { - bestType = type; - bestSum = sum; - } - } - - prevline = &in[y * linebytes]; - - /*now fill the out values*/ - out[y * (linebytes + 1)] = bestType; /*the first byte of a scanline will be the filter type*/ - for(x = 0; x != linebytes; ++x) out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; - } - } - - for(type = 0; type != 5; ++type) lodepng_free(attempt[type]); - } else if(strategy == LFS_PREDEFINED) { - for(y = 0; y != h; ++y) { - size_t outindex = (1 + linebytes) * y; /*the extra filterbyte added to each row*/ - size_t inindex = linebytes * y; - unsigned char type = settings->predefined_filters[y]; - out[outindex] = type; /*filter type byte*/ - filterScanline(&out[outindex + 1], &in[inindex], prevline, linebytes, bytewidth, type); - prevline = &in[inindex]; - } - } else if(strategy == LFS_BRUTE_FORCE) { - /*brute force filter chooser. - deflate the scanline after every filter attempt to see which one deflates best. - This is very slow and gives only slightly smaller, sometimes even larger, result*/ - size_t size[5]; - unsigned char* attempt[5]; /*five filtering attempts, one for each filter type*/ - size_t smallest = 0; - unsigned type = 0, bestType = 0; - unsigned char* dummy; - LodePNGCompressSettings zlibsettings; - lodepng_memcpy(&zlibsettings, &settings->zlibsettings, sizeof(LodePNGCompressSettings)); - /*use fixed tree on the attempts so that the tree is not adapted to the filtertype on purpose, - to simulate the true case where the tree is the same for the whole image. Sometimes it gives - better result with dynamic tree anyway. Using the fixed tree sometimes gives worse, but in rare - cases better compression. It does make this a bit less slow, so it's worth doing this.*/ - zlibsettings.btype = 1; - /*a custom encoder likely doesn't read the btype setting and is optimized for complete PNG - images only, so disable it*/ - zlibsettings.custom_zlib = 0; - zlibsettings.custom_deflate = 0; - for(type = 0; type != 5; ++type) { - attempt[type] = (unsigned char*)lodepng_malloc(linebytes); - if(!attempt[type]) error = 83; /*alloc fail*/ - } - if(!error) { - for(y = 0; y != h; ++y) /*try the 5 filter types*/ { - for(type = 0; type != 5; ++type) { - unsigned testsize = (unsigned)linebytes; - /*if(testsize > 8) testsize /= 8;*/ /*it already works good enough by testing a part of the row*/ - - filterScanline(attempt[type], &in[y * linebytes], prevline, linebytes, bytewidth, type); - size[type] = 0; - dummy = 0; - zlib_compress(&dummy, &size[type], attempt[type], testsize, &zlibsettings); - lodepng_free(dummy); - /*check if this is smallest size (or if type == 0 it's the first case so always store the values)*/ - if(type == 0 || size[type] < smallest) { - bestType = type; - smallest = size[type]; - } - } - prevline = &in[y * linebytes]; - out[y * (linebytes + 1)] = bestType; /*the first byte of a scanline will be the filter type*/ - for(x = 0; x != linebytes; ++x) out[y * (linebytes + 1) + 1 + x] = attempt[bestType][x]; - } - } - for(type = 0; type != 5; ++type) lodepng_free(attempt[type]); - } - else return 88; /* unknown filter strategy */ - - return error; -} - -static void addPaddingBits(unsigned char* out, const unsigned char* in, - size_t olinebits, size_t ilinebits, unsigned h) { - /*The opposite of the removePaddingBits function - olinebits must be >= ilinebits*/ - unsigned y; - size_t diff = olinebits - ilinebits; - size_t obp = 0, ibp = 0; /*bit pointers*/ - for(y = 0; y != h; ++y) { - size_t x; - for(x = 0; x < ilinebits; ++x) { - unsigned char bit = readBitFromReversedStream(&ibp, in); - setBitOfReversedStream(&obp, out, bit); - } - /*obp += diff; --> no, fill in some value in the padding bits too, to avoid - "Use of uninitialised value of size ###" warning from valgrind*/ - for(x = 0; x != diff; ++x) setBitOfReversedStream(&obp, out, 0); - } -} - -/* -in: non-interlaced image with size w*h -out: the same pixels, but re-ordered according to PNG's Adam7 interlacing, with - no padding bits between scanlines, but between reduced images so that each - reduced image starts at a byte. -bpp: bits per pixel -there are no padding bits, not between scanlines, not between reduced images -in has the following size in bits: w * h * bpp. -out is possibly bigger due to padding bits between reduced images -NOTE: comments about padding bits are only relevant if bpp < 8 -*/ -static void Adam7_interlace(unsigned char* out, const unsigned char* in, unsigned w, unsigned h, unsigned bpp) { - unsigned passw[7], passh[7]; - size_t filter_passstart[8], padded_passstart[8], passstart[8]; - unsigned i; - - Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, bpp); - - if(bpp >= 8) { - for(i = 0; i != 7; ++i) { - unsigned x, y, b; - size_t bytewidth = bpp / 8u; - for(y = 0; y < passh[i]; ++y) - for(x = 0; x < passw[i]; ++x) { - size_t pixelinstart = ((ADAM7_IY[i] + y * ADAM7_DY[i]) * w + ADAM7_IX[i] + x * ADAM7_DX[i]) * bytewidth; - size_t pixeloutstart = passstart[i] + (y * passw[i] + x) * bytewidth; - for(b = 0; b < bytewidth; ++b) { - out[pixeloutstart + b] = in[pixelinstart + b]; - } - } - } - } else /*bpp < 8: Adam7 with pixels < 8 bit is a bit trickier: with bit pointers*/ { - for(i = 0; i != 7; ++i) { - unsigned x, y, b; - unsigned ilinebits = bpp * passw[i]; - unsigned olinebits = bpp * w; - size_t obp, ibp; /*bit pointers (for out and in buffer)*/ - for(y = 0; y < passh[i]; ++y) - for(x = 0; x < passw[i]; ++x) { - ibp = (ADAM7_IY[i] + y * ADAM7_DY[i]) * olinebits + (ADAM7_IX[i] + x * ADAM7_DX[i]) * bpp; - obp = (8 * passstart[i]) + (y * ilinebits + x * bpp); - for(b = 0; b < bpp; ++b) { - unsigned char bit = readBitFromReversedStream(&ibp, in); - setBitOfReversedStream(&obp, out, bit); - } - } - } - } -} - -/*out must be buffer big enough to contain uncompressed IDAT chunk data, and in must contain the full image. -return value is error**/ -static unsigned preProcessScanlines(unsigned char** out, size_t* outsize, const unsigned char* in, - unsigned w, unsigned h, - const LodePNGInfo* info_png, const LodePNGEncoderSettings* settings) { - /* - This function converts the pure 2D image with the PNG's colortype, into filtered-padded-interlaced data. Steps: - *) if no Adam7: 1) add padding bits (= possible extra bits per scanline if bpp < 8) 2) filter - *) if adam7: 1) Adam7_interlace 2) 7x add padding bits 3) 7x filter - */ - size_t bpp = lodepng_get_bpp(&info_png->color); - unsigned error = 0; - if(info_png->interlace_method == 0) { - /*image size plus an extra byte per scanline + possible padding bits*/ - *outsize = (size_t)h + ((size_t)h * (((size_t)w * bpp + 7u) / 8u)); - *out = (unsigned char*)lodepng_malloc(*outsize); - if(!(*out) && (*outsize)) error = 83; /*alloc fail*/ - - if(!error) { - /*non multiple of 8 bits per scanline, padding bits needed per scanline*/ - if(bpp < 8 && (size_t)w * bpp != (((size_t)w * bpp + 7u) / 8u) * 8u) { - unsigned char* padded = (unsigned char*)lodepng_malloc(h * ((w * bpp + 7u) / 8u)); - if(!padded) error = 83; /*alloc fail*/ - if(!error) { - addPaddingBits(padded, in, (((size_t)w * bpp + 7u) / 8u) * 8u, (size_t)w * bpp, h); - error = filter(*out, padded, w, h, &info_png->color, settings); - } - lodepng_free(padded); - } else { - /*we can immediately filter into the out buffer, no other steps needed*/ - error = filter(*out, in, w, h, &info_png->color, settings); - } - } - } else /*interlace_method is 1 (Adam7)*/ { - unsigned passw[7], passh[7]; - size_t filter_passstart[8], padded_passstart[8], passstart[8]; - unsigned char* adam7; - - Adam7_getpassvalues(passw, passh, filter_passstart, padded_passstart, passstart, w, h, (unsigned)bpp); - - *outsize = filter_passstart[7]; /*image size plus an extra byte per scanline + possible padding bits*/ - *out = (unsigned char*)lodepng_malloc(*outsize); - if(!(*out)) error = 83; /*alloc fail*/ - - adam7 = (unsigned char*)lodepng_malloc(passstart[7]); - if(!adam7 && passstart[7]) error = 83; /*alloc fail*/ - - if(!error) { - unsigned i; - - Adam7_interlace(adam7, in, w, h, (unsigned)bpp); - for(i = 0; i != 7; ++i) { - if(bpp < 8) { - unsigned char* padded = (unsigned char*)lodepng_malloc(padded_passstart[i + 1] - padded_passstart[i]); - if(!padded) ERROR_BREAK(83); /*alloc fail*/ - addPaddingBits(padded, &adam7[passstart[i]], - (((size_t)passw[i] * bpp + 7u) / 8u) * 8u, (size_t)passw[i] * bpp, passh[i]); - error = filter(&(*out)[filter_passstart[i]], padded, - passw[i], passh[i], &info_png->color, settings); - lodepng_free(padded); - } else { - error = filter(&(*out)[filter_passstart[i]], &adam7[padded_passstart[i]], - passw[i], passh[i], &info_png->color, settings); - } - - if(error) break; - } - } - - lodepng_free(adam7); - } - - return error; -} - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS -static unsigned addUnknownChunks(ucvector* out, unsigned char* data, size_t datasize) { - unsigned char* inchunk = data; - while((size_t)(inchunk - data) < datasize) { - CERROR_TRY_RETURN(lodepng_chunk_append(&out->data, &out->size, inchunk)); - out->allocsize = out->size; /*fix the allocsize again*/ - inchunk = lodepng_chunk_next(inchunk, data + datasize); - } - return 0; -} - -static unsigned isGrayICCProfile(const unsigned char* profile, unsigned size) { - /* - It is a gray profile if bytes 16-19 are "GRAY", rgb profile if bytes 16-19 - are "RGB ". We do not perform any full parsing of the ICC profile here, other - than check those 4 bytes to grayscale profile. Other than that, validity of - the profile is not checked. This is needed only because the PNG specification - requires using a non-gray color model if there is an ICC profile with "RGB " - (sadly limiting compression opportunities if the input data is grayscale RGB - data), and requires using a gray color model if it is "GRAY". - */ - if(size < 20) return 0; - return profile[16] == 'G' && profile[17] == 'R' && profile[18] == 'A' && profile[19] == 'Y'; -} - -static unsigned isRGBICCProfile(const unsigned char* profile, unsigned size) { - /* See comment in isGrayICCProfile*/ - if(size < 20) return 0; - return profile[16] == 'R' && profile[17] == 'G' && profile[18] == 'B' && profile[19] == ' '; -} -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - -unsigned lodepng_encode(unsigned char** out, size_t* outsize, - const unsigned char* image, unsigned w, unsigned h, - LodePNGState* state) { - unsigned char* data = 0; /*uncompressed version of the IDAT chunk data*/ - size_t datasize = 0; - ucvector outv = ucvector_init(NULL, 0); - LodePNGInfo info; - const LodePNGInfo* info_png = &state->info_png; - LodePNGColorMode auto_color; - unsigned error = 0; - - lodepng_info_init(&info); - lodepng_color_mode_init(&auto_color); - - /*provide some proper output values if error will happen*/ - *out = 0; - *outsize = 0; - - /*check input values validity*/ - if((info_png->color.colortype == LCT_PALETTE || state->encoder.force_palette) - && (info_png->color.palettesize == 0 || info_png->color.palettesize > 256)) { - /*this error is returned even if auto_convert is enabled and thus encoder could - generate the palette by itself: while allowing this could be possible in theory, - it may complicate the code or edge cases, and always requiring to give a palette - when setting this color type is a simpler contract*/ - error = 68; /*invalid palette size, it is only allowed to be 1-256*/ - goto cleanup; - } - if(state->encoder.zlibsettings.btype > 2) { - error = 61; /*error: invalid btype*/ - goto cleanup; - } - if(info_png->interlace_method > 1) { - error = 71; /*error: invalid interlace mode*/ - goto cleanup; - } - error = checkColorValidity(info_png->color.colortype, info_png->color.bitdepth); - if(error) goto cleanup; /*error: invalid color type given*/ - error = checkColorValidity(state->info_raw.colortype, state->info_raw.bitdepth); - if(error) goto cleanup; /*error: invalid color type given*/ - - /* color convert and compute scanline filter types */ - CERROR_TRY_RETURN(lodepng_info_copy(&info, &state->info_png)); - if(state->encoder.auto_convert) { - LodePNGColorStats stats; - unsigned allow_convert = 1; - lodepng_color_stats_init(&stats); -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - if(info_png->iccp_defined && - isGrayICCProfile(info_png->iccp_profile, info_png->iccp_profile_size)) { - /*the PNG specification does not allow to use palette with a GRAY ICC profile, even - if the palette has only gray colors, so disallow it.*/ - stats.allow_palette = 0; - } - if(info_png->iccp_defined && - isRGBICCProfile(info_png->iccp_profile, info_png->iccp_profile_size)) { - /*the PNG specification does not allow to use grayscale color with RGB ICC profile, so disallow gray.*/ - stats.allow_greyscale = 0; - } -#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ - error = lodepng_compute_color_stats(&stats, image, w, h, &state->info_raw); - if(error) goto cleanup; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - if(info_png->background_defined) { - /*the background chunk's color must be taken into account as well*/ - unsigned r = 0, g = 0, b = 0; - LodePNGColorMode mode16 = lodepng_color_mode_make(LCT_RGB, 16); - lodepng_convert_rgb(&r, &g, &b, - info_png->background_r, info_png->background_g, info_png->background_b, &mode16, &info_png->color); - error = lodepng_color_stats_add(&stats, r, g, b, 65535); - if(error) goto cleanup; - } -#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ - error = auto_choose_color(&auto_color, &state->info_raw, &stats); - if(error) goto cleanup; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - if(info_png->sbit_defined) { - /*if sbit is defined, due to strict requirements of which sbit values can be present for which color modes, - auto_convert can't be done in many cases. However, do support a few cases here. - TODO: more conversions may be possible, and it may also be possible to get a more appropriate color type out of - auto_choose_color if knowledge about sbit is used beforehand - */ - unsigned sbit_max = LODEPNG_MAX(LODEPNG_MAX(LODEPNG_MAX(info_png->sbit_r, info_png->sbit_g), - info_png->sbit_b), info_png->sbit_a); - unsigned equal = (!info_png->sbit_g || info_png->sbit_g == info_png->sbit_r) - && (!info_png->sbit_b || info_png->sbit_b == info_png->sbit_r) - && (!info_png->sbit_a || info_png->sbit_a == info_png->sbit_r); - allow_convert = 0; - if(info.color.colortype == LCT_PALETTE && - auto_color.colortype == LCT_PALETTE) { - /* input and output are palette, and in this case it may happen that palette data is - expected to be copied from info_raw into the info_png */ - allow_convert = 1; - } - /*going from 8-bit RGB to palette (or 16-bit as long as sbit_max <= 8) is possible - since both are 8-bit RGB for sBIT's purposes*/ - if(info.color.colortype == LCT_RGB && - auto_color.colortype == LCT_PALETTE && sbit_max <= 8) { - allow_convert = 1; - } - /*going from 8-bit RGBA to palette is also ok but only if sbit_a is exactly 8*/ - if(info.color.colortype == LCT_RGBA && auto_color.colortype == LCT_PALETTE && - info_png->sbit_a == 8 && sbit_max <= 8) { - allow_convert = 1; - } - /*going from 16-bit RGB(A) to 8-bit RGB(A) is ok if all sbit values are <= 8*/ - if((info.color.colortype == LCT_RGB || info.color.colortype == LCT_RGBA) && info.color.bitdepth == 16 && - auto_color.colortype == info.color.colortype && auto_color.bitdepth == 8 && - sbit_max <= 8) { - allow_convert = 1; - } - /*going to less channels is ok if all bit values are equal (all possible values in sbit, - as well as the chosen bitdepth of the result). Due to how auto_convert works, - we already know that auto_color.colortype has less than or equal amount of channels than - info.colortype. Palette is not used here. This conversion is not allowed if - info_png->sbit_r < auto_color.bitdepth, because specifically for alpha, non-presence of - an sbit value heavily implies that alpha's bit depth is equal to the PNG bit depth (rather - than the bit depths set in the r, g and b sbit values, by how the PNG specification describes - handling tRNS chunk case with sBIT), so be conservative here about ignoring user input.*/ - if(info.color.colortype != LCT_PALETTE && auto_color.colortype != LCT_PALETTE && - equal && info_png->sbit_r == auto_color.bitdepth) { - allow_convert = 1; - } - } -#endif - if(state->encoder.force_palette) { - if(info.color.colortype != LCT_GREY && info.color.colortype != LCT_GREY_ALPHA && - (auto_color.colortype == LCT_GREY || auto_color.colortype == LCT_GREY_ALPHA)) { - /*user specifically forced a PLTE palette, so cannot convert to grayscale types because - the PNG specification only allows writing a suggested palette in PLTE for truecolor types*/ - allow_convert = 0; - } - } - if(allow_convert) { - lodepng_color_mode_copy(&info.color, &auto_color); -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /*also convert the background chunk*/ - if(info_png->background_defined) { - if(lodepng_convert_rgb(&info.background_r, &info.background_g, &info.background_b, - info_png->background_r, info_png->background_g, info_png->background_b, &info.color, &info_png->color)) { - error = 104; - goto cleanup; - } - } -#endif /* LODEPNG_COMPILE_ANCILLARY_CHUNKS */ - } - } -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - if(info_png->iccp_defined) { - unsigned gray_icc = isGrayICCProfile(info_png->iccp_profile, info_png->iccp_profile_size); - unsigned rgb_icc = isRGBICCProfile(info_png->iccp_profile, info_png->iccp_profile_size); - unsigned gray_png = info.color.colortype == LCT_GREY || info.color.colortype == LCT_GREY_ALPHA; - if(!gray_icc && !rgb_icc) { - error = 100; /* Disallowed profile color type for PNG */ - goto cleanup; - } - if(gray_icc != gray_png) { - /*Not allowed to use RGB/RGBA/palette with GRAY ICC profile or vice versa, - or in case of auto_convert, it wasn't possible to find appropriate model*/ - error = state->encoder.auto_convert ? 102 : 101; - goto cleanup; - } - } -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - if(!lodepng_color_mode_equal(&state->info_raw, &info.color)) { - unsigned char* converted; - size_t size = ((size_t)w * (size_t)h * (size_t)lodepng_get_bpp(&info.color) + 7u) / 8u; - - converted = (unsigned char*)lodepng_malloc(size); - if(!converted && size) error = 83; /*alloc fail*/ - if(!error) { - error = lodepng_convert(converted, image, &info.color, &state->info_raw, w, h); - } - if(!error) { - error = preProcessScanlines(&data, &datasize, converted, w, h, &info, &state->encoder); - } - lodepng_free(converted); - if(error) goto cleanup; - } else { - error = preProcessScanlines(&data, &datasize, image, w, h, &info, &state->encoder); - if(error) goto cleanup; - } - - /* output all PNG chunks */ { -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - size_t i; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - /*write signature and chunks*/ - error = writeSignature(&outv); - if(error) goto cleanup; - /*IHDR*/ - error = addChunk_IHDR(&outv, w, h, info.color.colortype, info.color.bitdepth, info.interlace_method); - if(error) goto cleanup; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /*unknown chunks between IHDR and PLTE*/ - if(info.unknown_chunks_data[0]) { - error = addUnknownChunks(&outv, info.unknown_chunks_data[0], info.unknown_chunks_size[0]); - if(error) goto cleanup; - } - /*color profile chunks must come before PLTE */ - if(info.cicp_defined) { - error = addChunk_cICP(&outv, &info); - if(error) goto cleanup; - } - if(info.mdcv_defined) { - error = addChunk_mDCV(&outv, &info); - if(error) goto cleanup; - } - if(info.clli_defined) { - error = addChunk_cLLI(&outv, &info); - if(error) goto cleanup; - } - if(info.iccp_defined) { - error = addChunk_iCCP(&outv, &info, &state->encoder.zlibsettings); - if(error) goto cleanup; - } - if(info.srgb_defined) { - error = addChunk_sRGB(&outv, &info); - if(error) goto cleanup; - } - if(info.gama_defined) { - error = addChunk_gAMA(&outv, &info); - if(error) goto cleanup; - } - if(info.chrm_defined) { - error = addChunk_cHRM(&outv, &info); - if(error) goto cleanup; - } - if(info_png->sbit_defined) { - error = addChunk_sBIT(&outv, &info); - if(error) goto cleanup; - } - if(info.exif_defined) { - error = addChunk_eXIf(&outv, &info); - if(error) goto cleanup; - } -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - /*PLTE*/ - if(info.color.colortype == LCT_PALETTE) { - error = addChunk_PLTE(&outv, &info.color); - if(error) goto cleanup; - } - if(state->encoder.force_palette && (info.color.colortype == LCT_RGB || info.color.colortype == LCT_RGBA)) { - /*force_palette means: write suggested palette for truecolor in PLTE chunk*/ - error = addChunk_PLTE(&outv, &info.color); - if(error) goto cleanup; - } - /*tRNS (this will only add if when necessary) */ - error = addChunk_tRNS(&outv, &info.color); - if(error) goto cleanup; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /*bKGD (must come between PLTE and the IDAt chunks*/ - if(info.background_defined) { - error = addChunk_bKGD(&outv, &info); - if(error) goto cleanup; - } - /*pHYs (must come before the IDAT chunks)*/ - if(info.phys_defined) { - error = addChunk_pHYs(&outv, &info); - if(error) goto cleanup; - } - - /*unknown chunks between PLTE and IDAT*/ - if(info.unknown_chunks_data[1]) { - error = addUnknownChunks(&outv, info.unknown_chunks_data[1], info.unknown_chunks_size[1]); - if(error) goto cleanup; - } -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - /*IDAT (multiple IDAT chunks must be consecutive)*/ - error = addChunk_IDAT(&outv, data, datasize, &state->encoder.zlibsettings); - if(error) goto cleanup; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /*tIME*/ - if(info.time_defined) { - error = addChunk_tIME(&outv, &info.time); - if(error) goto cleanup; - } - /*tEXt and/or zTXt*/ - for(i = 0; i != info.text_num; ++i) { - if(lodepng_strlen(info.text_keys[i]) > 79) { - error = 66; /*text chunk too large*/ - goto cleanup; - } - if(lodepng_strlen(info.text_keys[i]) < 1) { - error = 67; /*text chunk too small*/ - goto cleanup; - } - if(state->encoder.text_compression) { - error = addChunk_zTXt(&outv, info.text_keys[i], info.text_strings[i], &state->encoder.zlibsettings); - if(error) goto cleanup; - } else { - error = addChunk_tEXt(&outv, info.text_keys[i], info.text_strings[i]); - if(error) goto cleanup; - } - } - /*LodePNG version id in text chunk*/ - if(state->encoder.add_id) { - unsigned already_added_id_text = 0; - for(i = 0; i != info.text_num; ++i) { - const char* k = info.text_keys[i]; - /* Could use strcmp, but we're not calling or reimplementing this C library function for this use only */ - if(k[0] == 'L' && k[1] == 'o' && k[2] == 'd' && k[3] == 'e' && - k[4] == 'P' && k[5] == 'N' && k[6] == 'G' && k[7] == '\0') { - already_added_id_text = 1; - break; - } - } - if(already_added_id_text == 0) { - error = addChunk_tEXt(&outv, "LodePNG", LODEPNG_VERSION_STRING); /*it's shorter as tEXt than as zTXt chunk*/ - if(error) goto cleanup; - } - } - /*iTXt*/ - for(i = 0; i != info.itext_num; ++i) { - if(lodepng_strlen(info.itext_keys[i]) > 79) { - error = 66; /*text chunk too large*/ - goto cleanup; - } - if(lodepng_strlen(info.itext_keys[i]) < 1) { - error = 67; /*text chunk too small*/ - goto cleanup; - } - error = addChunk_iTXt( - &outv, state->encoder.text_compression, - info.itext_keys[i], info.itext_langtags[i], info.itext_transkeys[i], info.itext_strings[i], - &state->encoder.zlibsettings); - if(error) goto cleanup; - } - - /*unknown chunks between IDAT and IEND*/ - if(info.unknown_chunks_data[2]) { - error = addUnknownChunks(&outv, info.unknown_chunks_data[2], info.unknown_chunks_size[2]); - if(error) goto cleanup; - } -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - error = addChunk_IEND(&outv); - if(error) goto cleanup; - } - -cleanup: - lodepng_info_cleanup(&info); - lodepng_free(data); - lodepng_color_mode_cleanup(&auto_color); - - /*instead of cleaning the vector up, give it to the output*/ - *out = outv.data; - *outsize = outv.size; - - state->error = error; /*TODO: remove this and make input state const*/ - - return error; -} - -unsigned lodepng_encode_memory(unsigned char** out, size_t* outsize, const unsigned char* image, - unsigned w, unsigned h, LodePNGColorType colortype, unsigned bitdepth) { - unsigned error; - LodePNGState state; - lodepng_state_init(&state); - state.info_raw.colortype = colortype; - state.info_raw.bitdepth = bitdepth; - state.info_png.color.colortype = colortype; - state.info_png.color.bitdepth = bitdepth; - error = lodepng_encode(out, outsize, image, w, h, &state); - lodepng_state_cleanup(&state); - return error; -} - -unsigned lodepng_encode32(unsigned char** out, size_t* outsize, const unsigned char* image, unsigned w, unsigned h) { - return lodepng_encode_memory(out, outsize, image, w, h, LCT_RGBA, 8); -} - -unsigned lodepng_encode24(unsigned char** out, size_t* outsize, const unsigned char* image, unsigned w, unsigned h) { - return lodepng_encode_memory(out, outsize, image, w, h, LCT_RGB, 8); -} - -#ifdef LODEPNG_COMPILE_DISK -unsigned lodepng_encode_file(const char* filename, const unsigned char* image, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth) { - unsigned char* buffer; - size_t buffersize; - unsigned error = lodepng_encode_memory(&buffer, &buffersize, image, w, h, colortype, bitdepth); - if(!error) error = lodepng_save_file(buffer, buffersize, filename); - lodepng_free(buffer); - return error; -} - -unsigned lodepng_encode32_file(const char* filename, const unsigned char* image, unsigned w, unsigned h) { - return lodepng_encode_file(filename, image, w, h, LCT_RGBA, 8); -} - -unsigned lodepng_encode24_file(const char* filename, const unsigned char* image, unsigned w, unsigned h) { - return lodepng_encode_file(filename, image, w, h, LCT_RGB, 8); -} -#endif /*LODEPNG_COMPILE_DISK*/ - -void lodepng_encoder_settings_init(LodePNGEncoderSettings* settings) { - lodepng_compress_settings_init(&settings->zlibsettings); - settings->filter_palette_zero = 1; - settings->filter_strategy = LFS_MINSUM; - settings->auto_convert = 1; - settings->force_palette = 0; - settings->predefined_filters = 0; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - settings->add_id = 0; - settings->text_compression = 1; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ -} - -#endif /*LODEPNG_COMPILE_ENCODER*/ -#endif /*LODEPNG_COMPILE_PNG*/ - -#ifdef LODEPNG_COMPILE_ERROR_TEXT -/* -This returns the description of a numerical error code in English. This is also -the documentation of all the error codes. -*/ -const char* lodepng_error_text(unsigned code) { - switch(code) { - case 0: return "no error, everything went ok"; - case 1: return "nothing done yet"; /*the Encoder/Decoder has done nothing yet, error checking makes no sense yet*/ - case 10: return "end of input memory reached without huffman end code"; /*while huffman decoding*/ - case 11: return "error in code tree made it jump outside of huffman tree"; /*while huffman decoding*/ - case 13: return "problem while processing dynamic deflate block"; - case 14: return "problem while processing dynamic deflate block"; - case 15: return "problem while processing dynamic deflate block"; - /*this error could happen if there are only 0 or 1 symbols present in the huffman code:*/ - case 16: return "invalid code while processing dynamic deflate block"; - case 17: return "end of out buffer memory reached while inflating"; - case 18: return "invalid distance code while inflating"; - case 19: return "end of out buffer memory reached while inflating"; - case 20: return "invalid deflate block BTYPE encountered while decoding"; - case 21: return "NLEN is not ones complement of LEN in a deflate block"; - - /*end of out buffer memory reached while inflating: - This can happen if the inflated deflate data is longer than the amount of bytes required to fill up - all the pixels of the image, given the color depth and image dimensions. Something that doesn't - happen in a normal, well encoded, PNG image.*/ - case 22: return "end of out buffer memory reached while inflating"; - case 23: return "end of in buffer memory reached while inflating"; - case 24: return "invalid FCHECK in zlib header"; - case 25: return "invalid compression method in zlib header"; - case 26: return "FDICT encountered in zlib header while it's not used for PNG"; - case 27: return "PNG file is smaller than a PNG header"; - /*Checks the magic file header, the first 8 bytes of the PNG file*/ - case 28: return "incorrect PNG signature, it's no PNG or corrupted"; - case 29: return "first chunk is not the header chunk"; - case 30: return "chunk length too large, chunk broken off at end of file"; - case 31: return "illegal PNG color type or bpp"; - case 32: return "illegal PNG compression method"; - case 33: return "illegal PNG filter method"; - case 34: return "illegal PNG interlace method"; - case 35: return "chunk length of a chunk is too large or the chunk too small"; - case 36: return "illegal PNG filter type encountered"; - case 37: return "illegal bit depth for this color type given"; - case 38: return "the palette is too small or too big"; /*0, or more than 256 colors*/ - case 39: return "tRNS chunk before PLTE or has more entries than palette size"; - case 40: return "tRNS chunk has wrong size for grayscale image"; - case 41: return "tRNS chunk has wrong size for RGB image"; - case 42: return "tRNS chunk appeared while it was not allowed for this color type"; - case 43: return "bKGD chunk has wrong size for palette image"; - case 44: return "bKGD chunk has wrong size for grayscale image"; - case 45: return "bKGD chunk has wrong size for RGB image"; - case 48: return "empty input buffer given to decoder. Maybe caused by non-existing file?"; - case 49: return "jumped past memory while generating dynamic huffman tree"; - case 50: return "jumped past memory while generating dynamic huffman tree"; - case 51: return "jumped past memory while inflating huffman block"; - case 52: return "jumped past memory while inflating"; - case 53: return "size of zlib data too small"; - case 54: return "repeat symbol in tree while there was no value symbol yet"; - /*jumped past tree while generating huffman tree, this could be when the - tree will have more leaves than symbols after generating it out of the - given lengths. They call this an oversubscribed dynamic bit lengths tree in zlib.*/ - case 55: return "jumped past tree while generating huffman tree"; - case 56: return "given output image colortype or bitdepth not supported for color conversion"; - case 57: return "invalid CRC encountered (checking CRC can be disabled)"; - case 58: return "invalid ADLER32 encountered (checking ADLER32 can be disabled)"; - case 59: return "requested color conversion not supported"; - case 60: return "invalid window size given in the settings of the encoder (must be 0-32768)"; - case 61: return "invalid BTYPE given in the settings of the encoder (only 0, 1 and 2 are allowed)"; - /*LodePNG leaves the choice of RGB to grayscale conversion formula to the user.*/ - case 62: return "conversion from color to grayscale not supported"; - /*(2^31-1)*/ - case 63: return "length of a chunk too long, max allowed for PNG is 2147483647 bytes per chunk"; - /*this would result in the inability of a deflated block to ever contain an end code. It must be at least 1.*/ - case 64: return "the length of the END symbol 256 in the Huffman tree is 0"; - case 66: return "the length of a text chunk keyword given to the encoder is longer than the maximum of 79 bytes"; - case 67: return "the length of a text chunk keyword given to the encoder is smaller than the minimum of 1 byte"; - case 68: return "tried to encode a PLTE chunk with a palette that has less than 1 or more than 256 colors"; - case 69: return "unknown chunk type with 'critical' flag encountered by the decoder"; - case 71: return "invalid interlace mode given to encoder (must be 0 or 1)"; - case 72: return "while decoding, invalid compression method encountered in zTXt, iTXt or iCCP chunk (it must be 0)"; - case 73: return "invalid tIME chunk size"; - case 74: return "invalid pHYs chunk size"; - /*length could be wrong, or data chopped off*/ - case 75: return "no null termination char found while decoding text chunk"; - case 76: return "iTXt chunk too short to contain required bytes"; - case 77: return "integer overflow in buffer size"; - case 78: return "failed to open file for reading"; /*file doesn't exist or couldn't be opened for reading*/ - case 79: return "failed to open file for writing"; - case 80: return "tried creating a tree of 0 symbols"; - case 81: return "lazy matching at pos 0 is impossible"; - case 82: return "color conversion to palette requested while a color isn't in palette, or index out of bounds"; - case 83: return "memory allocation failed"; - case 84: return "given image too small to contain all pixels to be encoded"; - case 86: return "impossible offset in lz77 encoding (internal bug)"; - case 87: return "must provide custom zlib function pointer if LODEPNG_COMPILE_ZLIB is not defined"; - case 88: return "invalid filter strategy given for LodePNGEncoderSettings.filter_strategy"; - case 89: return "text chunk keyword too short or long: must have size 1-79"; - /*the windowsize in the LodePNGCompressSettings. Requiring POT(==> & instead of %) makes encoding 12% faster.*/ - case 90: return "windowsize must be a power of two"; - case 91: return "invalid decompressed idat size"; - case 92: return "integer overflow due to too many pixels"; - case 93: return "zero width or height is invalid"; - case 94: return "header chunk must have a size of 13 bytes"; - case 95: return "integer overflow with combined idat chunk size"; - case 96: return "invalid gAMA chunk size"; - case 97: return "invalid cHRM chunk size"; - case 98: return "invalid sRGB chunk size"; - case 99: return "invalid sRGB rendering intent"; - case 100: return "invalid ICC profile color type, the PNG specification only allows RGB or GRAY"; - case 101: return "PNG specification does not allow RGB ICC profile on gray color types and vice versa"; - case 102: return "not allowed to set grayscale ICC profile with colored pixels by PNG specification"; - case 103: return "invalid palette index in bKGD chunk. Maybe it came before PLTE chunk?"; - case 104: return "invalid bKGD color while encoding (e.g. palette index out of range)"; - case 105: return "integer overflow of bitsize"; - case 106: return "PNG file must have PLTE chunk if color type is palette"; - case 107: return "color convert from palette mode requested without setting the palette data in it"; - case 108: return "tried to add more than 256 values to a palette"; - /*this limit can be configured in LodePNGDecompressSettings*/ - case 109: return "tried to decompress zlib or deflate data larger than desired max_output_size"; - case 110: return "custom zlib or inflate decompression failed"; - case 111: return "custom zlib or deflate compression failed"; - /*max text size limit can be configured in LodePNGDecoderSettings. This error prevents - unreasonable memory consumption when decoding due to impossibly large text sizes.*/ - case 112: return "compressed text unreasonably large"; - /*max ICC size limit can be configured in LodePNGDecoderSettings. This error prevents - unreasonable memory consumption when decoding due to impossibly large ICC profile*/ - case 113: return "ICC profile unreasonably large"; - case 114: return "sBIT chunk has wrong size for the color type of the image"; - case 115: return "sBIT value out of range"; - case 116: return "cICP value out of range"; - case 117: return "invalid cICP chunk size"; - case 118: return "mDCV value out of range"; - case 119: return "invalid mDCV chunk size"; - case 120: return "invalid cLLI chunk size"; - case 121: return "invalid chunk type name: may only contain [a-zA-Z]"; - case 122: return "invalid chunk type name: third character must be uppercase"; - case 123: return "invalid ICC profile size"; - } - return "unknown error code"; -} -#endif /*LODEPNG_COMPILE_ERROR_TEXT*/ - -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* // C++ Wrapper // */ -/* ////////////////////////////////////////////////////////////////////////// */ -/* ////////////////////////////////////////////////////////////////////////// */ - -#ifdef LODEPNG_COMPILE_CPP -namespace lodepng { - -#ifdef LODEPNG_COMPILE_DISK -/* Resizes the vector to the file size and reads the file into it. Returns error code.*/ -static unsigned load_file_(std::vector& buffer, FILE* file) { - long size = lodepng_filesize(file); - if(size < 0) return 78; - buffer.resize((size_t)size); - if(size == 0) return 0; /*ok*/ - if(fread(&buffer[0], 1, buffer.size(), file) != buffer.size()) return 78; - return 0; /*ok*/ -} - -unsigned load_file(std::vector& buffer, const std::string& filename) { - unsigned error; - FILE* file = fopen(filename.c_str(), "rb"); - if(!file) return 78; - error = load_file_(buffer, file); - fclose(file); - return error; -} - -/*write given buffer to the file, overwriting the file, it doesn't append to it.*/ -unsigned save_file(const std::vector& buffer, const std::string& filename) { - return lodepng_save_file(buffer.empty() ? 0 : &buffer[0], buffer.size(), filename.c_str()); -} -#endif /* LODEPNG_COMPILE_DISK */ - -#ifdef LODEPNG_COMPILE_ZLIB -#ifdef LODEPNG_COMPILE_DECODER -unsigned decompress(std::vector& out, const unsigned char* in, size_t insize, - const LodePNGDecompressSettings& settings) { - unsigned char* buffer = 0; - size_t buffersize = 0; - unsigned error = zlib_decompress(&buffer, &buffersize, 0, in, insize, &settings); - if(buffer) { - out.insert(out.end(), buffer, &buffer[buffersize]); - lodepng_free(buffer); - } - return error; -} - -unsigned decompress(std::vector& out, const std::vector& in, - const LodePNGDecompressSettings& settings) { - return decompress(out, in.empty() ? 0 : &in[0], in.size(), settings); -} -#endif /* LODEPNG_COMPILE_DECODER */ - -#ifdef LODEPNG_COMPILE_ENCODER -unsigned compress(std::vector& out, const unsigned char* in, size_t insize, - const LodePNGCompressSettings& settings) { - unsigned char* buffer = 0; - size_t buffersize = 0; - unsigned error = zlib_compress(&buffer, &buffersize, in, insize, &settings); - if(buffer) { - out.insert(out.end(), buffer, &buffer[buffersize]); - lodepng_free(buffer); - } - return error; -} - -unsigned compress(std::vector& out, const std::vector& in, - const LodePNGCompressSettings& settings) { - return compress(out, in.empty() ? 0 : &in[0], in.size(), settings); -} -#endif /* LODEPNG_COMPILE_ENCODER */ -#endif /* LODEPNG_COMPILE_ZLIB */ - - -#ifdef LODEPNG_COMPILE_PNG - -State::State() { - lodepng_state_init(this); -} - -State::State(const State& other) { - lodepng_state_init(this); - lodepng_state_copy(this, &other); -} - -State::~State() { - lodepng_state_cleanup(this); -} - -State& State::operator=(const State& other) { - lodepng_state_copy(this, &other); - return *this; -} - -#ifdef LODEPNG_COMPILE_DECODER - -unsigned decode(std::vector& out, unsigned& w, unsigned& h, const unsigned char* in, - size_t insize, LodePNGColorType colortype, unsigned bitdepth) { - unsigned char* buffer = 0; - unsigned error = lodepng_decode_memory(&buffer, &w, &h, in, insize, colortype, bitdepth); - if(buffer && !error) { - State state; - state.info_raw.colortype = colortype; - state.info_raw.bitdepth = bitdepth; - size_t buffersize = lodepng_get_raw_size(w, h, &state.info_raw); - out.insert(out.end(), buffer, &buffer[buffersize]); - } - lodepng_free(buffer); - return error; -} - -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - const std::vector& in, LodePNGColorType colortype, unsigned bitdepth) { - return decode(out, w, h, in.empty() ? 0 : &in[0], (unsigned)in.size(), colortype, bitdepth); -} - -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - State& state, - const unsigned char* in, size_t insize) { - unsigned char* buffer = NULL; - unsigned error = lodepng_decode(&buffer, &w, &h, &state, in, insize); - if(buffer && !error) { - size_t buffersize = lodepng_get_raw_size(w, h, &state.info_raw); - out.insert(out.end(), buffer, &buffer[buffersize]); - } - lodepng_free(buffer); - return error; -} - -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - State& state, - const std::vector& in) { - return decode(out, w, h, state, in.empty() ? 0 : &in[0], in.size()); -} - -#ifdef LODEPNG_COMPILE_DISK -unsigned decode(std::vector& out, unsigned& w, unsigned& h, const std::string& filename, - LodePNGColorType colortype, unsigned bitdepth) { - std::vector buffer; - /* safe output values in case error happens */ - w = h = 0; - unsigned error = load_file(buffer, filename); - if(error) return error; - return decode(out, w, h, buffer, colortype, bitdepth); -} -#endif /* LODEPNG_COMPILE_DECODER */ -#endif /* LODEPNG_COMPILE_DISK */ - -#ifdef LODEPNG_COMPILE_ENCODER -unsigned encode(std::vector& out, const unsigned char* in, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth) { - unsigned char* buffer; - size_t buffersize; - unsigned error = lodepng_encode_memory(&buffer, &buffersize, in, w, h, colortype, bitdepth); - if(buffer) { - out.insert(out.end(), buffer, &buffer[buffersize]); - lodepng_free(buffer); - } - return error; -} - -unsigned encode(std::vector& out, - const std::vector& in, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth) { - if(lodepng_get_raw_size_lct(w, h, colortype, bitdepth) > in.size()) return 84; - return encode(out, in.empty() ? 0 : &in[0], w, h, colortype, bitdepth); -} - -unsigned encode(std::vector& out, - const unsigned char* in, unsigned w, unsigned h, - State& state) { - unsigned char* buffer; - size_t buffersize; - unsigned error = lodepng_encode(&buffer, &buffersize, in, w, h, &state); - if(buffer) { - out.insert(out.end(), buffer, &buffer[buffersize]); - lodepng_free(buffer); - } - return error; -} - -unsigned encode(std::vector& out, - const std::vector& in, unsigned w, unsigned h, - State& state) { - if(lodepng_get_raw_size(w, h, &state.info_raw) > in.size()) return 84; - return encode(out, in.empty() ? 0 : &in[0], w, h, state); -} - -#ifdef LODEPNG_COMPILE_DISK -unsigned encode(const std::string& filename, - const unsigned char* in, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth) { - std::vector buffer; - unsigned error = encode(buffer, in, w, h, colortype, bitdepth); - if(!error) error = save_file(buffer, filename); - return error; -} - -unsigned encode(const std::string& filename, - const std::vector& in, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth) { - if(lodepng_get_raw_size_lct(w, h, colortype, bitdepth) > in.size()) return 84; - return encode(filename, in.empty() ? 0 : &in[0], w, h, colortype, bitdepth); -} -#endif /* LODEPNG_COMPILE_DISK */ -#endif /* LODEPNG_COMPILE_ENCODER */ -#endif /* LODEPNG_COMPILE_PNG */ -} /* namespace lodepng */ -#endif /*LODEPNG_COMPILE_CPP*/ diff --git a/server/deps/lodepng/lodepng.h b/server/deps/lodepng/lodepng.h deleted file mode 100644 index 8517eaeb8..000000000 --- a/server/deps/lodepng/lodepng.h +++ /dev/null @@ -1,2188 +0,0 @@ -/* -LodePNG version 20260119 - -Copyright (c) 2005-2026 Lode Vandevenne - -This software is provided 'as-is', without any express or implied -warranty. In no event will the authors be held liable for any damages -arising from the use of this software. - -Permission is granted to anyone to use this software for any purpose, -including commercial applications, and to alter it and redistribute it -freely, subject to the following restrictions: - - 1. The origin of this software must not be misrepresented; you must not - claim that you wrote the original software. If you use this software - in a product, an acknowledgment in the product documentation would be - appreciated but is not required. - - 2. Altered source versions must be plainly marked as such, and must not be - misrepresented as being the original software. - - 3. This notice may not be removed or altered from any source - distribution. -*/ - -#ifndef LODEPNG_H -#define LODEPNG_H - -#include /*for size_t*/ - -extern const char* LODEPNG_VERSION_STRING; - -/* -The following #defines are used to create code sections. They can be disabled -to disable code sections, which can give faster compile time and smaller binary. -The "NO_COMPILE" defines are designed to be used to pass as defines to the -compiler command to disable them without modifying this header, e.g. --DLODEPNG_NO_COMPILE_ZLIB for gcc or clang. -*/ -/*deflate & zlib. If disabled, you must specify alternative zlib functions in -the custom_zlib field of the compress and decompress settings*/ -#ifndef LODEPNG_NO_COMPILE_ZLIB -/*pass -DLODEPNG_NO_COMPILE_ZLIB to the compiler to disable this, or comment out LODEPNG_COMPILE_ZLIB below*/ -#define LODEPNG_COMPILE_ZLIB -#endif - -/*png encoder and png decoder*/ -#ifndef LODEPNG_NO_COMPILE_PNG -/*pass -DLODEPNG_NO_COMPILE_PNG to the compiler to disable this, or comment out LODEPNG_COMPILE_PNG below*/ -#define LODEPNG_COMPILE_PNG -#endif - -/*deflate&zlib decoder and png decoder*/ -#ifndef LODEPNG_NO_COMPILE_DECODER -/*pass -DLODEPNG_NO_COMPILE_DECODER to the compiler to disable this, or comment out LODEPNG_COMPILE_DECODER below*/ -#define LODEPNG_COMPILE_DECODER -#endif - -/*deflate&zlib encoder and png encoder*/ -#ifndef LODEPNG_NO_COMPILE_ENCODER -/*pass -DLODEPNG_NO_COMPILE_ENCODER to the compiler to disable this, or comment out LODEPNG_COMPILE_ENCODER below*/ -#define LODEPNG_COMPILE_ENCODER -#endif - -/*the optional built in harddisk file loading and saving functions*/ -#ifndef LODEPNG_NO_COMPILE_DISK -/*pass -DLODEPNG_NO_COMPILE_DISK to the compiler to disable this, or comment out LODEPNG_COMPILE_DISK below*/ -#define LODEPNG_COMPILE_DISK -#endif - -/*support for chunks other than IHDR, IDAT, PLTE, tRNS, IEND: ancillary and unknown chunks*/ -#ifndef LODEPNG_NO_COMPILE_ANCILLARY_CHUNKS -/*pass -DLODEPNG_NO_COMPILE_ANCILLARY_CHUNKS to the compiler to disable this, -or comment out LODEPNG_COMPILE_ANCILLARY_CHUNKS below*/ -#define LODEPNG_COMPILE_ANCILLARY_CHUNKS -#endif - -/*ability to convert error numerical codes to English text string*/ -#ifndef LODEPNG_NO_COMPILE_ERROR_TEXT -/*pass -DLODEPNG_NO_COMPILE_ERROR_TEXT to the compiler to disable this, -or comment out LODEPNG_COMPILE_ERROR_TEXT below*/ -#define LODEPNG_COMPILE_ERROR_TEXT -#endif - -/*Compile the default allocators (C's free, malloc and realloc). If you disable this, -you can define the functions lodepng_free, lodepng_malloc and lodepng_realloc in your -source files with custom allocators.*/ -#ifndef LODEPNG_NO_COMPILE_ALLOCATORS -/*pass -DLODEPNG_NO_COMPILE_ALLOCATORS to the compiler to disable the built-in ones, -or comment out LODEPNG_COMPILE_ALLOCATORS below*/ -#define LODEPNG_COMPILE_ALLOCATORS -#endif - -/*Disable built-in CRC function, in that case a custom implementation of -lodepng_crc32 must be defined externally so that it can be linked in. -The default built-in CRC code comes with 8KB of lookup tables, so for memory constrained environment you may want it -disabled and provide a much smaller implementation externally as said above. You can find such an example implementation -in a comment in the lodepng.c(pp) file in the 'else' case of the searchable LODEPNG_COMPILE_CRC section.*/ -#ifndef LODEPNG_NO_COMPILE_CRC -/*pass -DLODEPNG_NO_COMPILE_CRC to the compiler to disable the built-in one, -or comment out LODEPNG_COMPILE_CRC below*/ -#define LODEPNG_COMPILE_CRC -#endif - -/*compile the C++ version (you can disable the C++ wrapper here even when compiling for C++)*/ -#ifdef __cplusplus -#ifndef LODEPNG_NO_COMPILE_CPP -/*pass -DLODEPNG_NO_COMPILE_CPP to the compiler to disable C++ (not needed if a C-only compiler), -or comment out LODEPNG_COMPILE_CPP below*/ -#define LODEPNG_COMPILE_CPP -#endif -#endif - -#ifdef LODEPNG_COMPILE_CPP -#include -#include -#endif /*LODEPNG_COMPILE_CPP*/ - -#ifdef LODEPNG_COMPILE_PNG -/*The PNG color types (also used for raw image).*/ -typedef enum LodePNGColorType { - LCT_GREY = 0, /*grayscale: 1,2,4,8,16 bit*/ - LCT_RGB = 2, /*RGB: 8,16 bit*/ - LCT_PALETTE = 3, /*palette: 1,2,4,8 bit*/ - LCT_GREY_ALPHA = 4, /*grayscale with alpha: 8,16 bit*/ - LCT_RGBA = 6, /*RGB with alpha: 8,16 bit*/ - /*LCT_MAX_OCTET_VALUE lets the compiler allow this enum to represent any invalid - byte value from 0 to 255 that could be present in an invalid PNG file header. Do - not use, compare with or set the name LCT_MAX_OCTET_VALUE, instead either use - the valid color type names above, or numeric values like 1 or 7 when checking for - particular disallowed color type byte values, or cast to integer to print it.*/ - LCT_MAX_OCTET_VALUE = 255 -} LodePNGColorType; - -#ifdef LODEPNG_COMPILE_DECODER -/* -Converts PNG data in memory to raw pixel data. -out: Output parameter. Pointer to buffer that will contain the raw pixel data. - After decoding, its size is w * h * (bytes per pixel) bytes larger than - initially. Bytes per pixel depends on colortype and bitdepth. - Must be freed after usage with free(*out). - Note: for 16-bit per channel colors, uses big endian format like PNG does. -w: Output parameter. Pointer to width of pixel data. -h: Output parameter. Pointer to height of pixel data. -in: Memory buffer with the PNG file. -insize: size of the in buffer. -colortype: the desired color type for the raw output image. See explanation on PNG color types. -bitdepth: the desired bit depth for the raw output image. See explanation on PNG color types. -Return value: LodePNG error code (0 means no error). -*/ -unsigned lodepng_decode_memory(unsigned char** out, unsigned* w, unsigned* h, - const unsigned char* in, size_t insize, - LodePNGColorType colortype, unsigned bitdepth); - -/*Same as lodepng_decode_memory, but always decodes to 32-bit RGBA raw image*/ -unsigned lodepng_decode32(unsigned char** out, unsigned* w, unsigned* h, - const unsigned char* in, size_t insize); - -/*Same as lodepng_decode_memory, but always decodes to 24-bit RGB raw image*/ -unsigned lodepng_decode24(unsigned char** out, unsigned* w, unsigned* h, - const unsigned char* in, size_t insize); - -#ifdef LODEPNG_COMPILE_DISK -/* -Load PNG from disk, from file with given name. -Same as the other decode functions, but instead takes a filename as input. - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and decode in-memory.*/ -unsigned lodepng_decode_file(unsigned char** out, unsigned* w, unsigned* h, - const char* filename, - LodePNGColorType colortype, unsigned bitdepth); - -/*Same as lodepng_decode_file, but always decodes to 32-bit RGBA raw image. - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and decode in-memory.*/ -unsigned lodepng_decode32_file(unsigned char** out, unsigned* w, unsigned* h, - const char* filename); - -/*Same as lodepng_decode_file, but always decodes to 24-bit RGB raw image. - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and decode in-memory.*/ -unsigned lodepng_decode24_file(unsigned char** out, unsigned* w, unsigned* h, - const char* filename); -#endif /*LODEPNG_COMPILE_DISK*/ -#endif /*LODEPNG_COMPILE_DECODER*/ - - -#ifdef LODEPNG_COMPILE_ENCODER -/* -Converts raw pixel data into a PNG image in memory. The colortype and bitdepth - of the output PNG image cannot be chosen, they are automatically determined - by the colortype, bitdepth and content of the input pixel data. - Note: for 16-bit per channel colors, needs big endian format like PNG does. -out: Output parameter. Pointer to buffer that will contain the PNG image data. - Must be freed after usage with free(*out). -outsize: Output parameter. Pointer to the size in bytes of the out buffer. -image: The raw pixel data to encode. The size of this buffer should be - w * h * (bytes per pixel), bytes per pixel depends on colortype and bitdepth. -w: width of the raw pixel data in pixels. -h: height of the raw pixel data in pixels. -colortype: the color type of the raw input image. See explanation on PNG color types. -bitdepth: the bit depth of the raw input image. See explanation on PNG color types. -Return value: LodePNG error code (0 means no error). -*/ -unsigned lodepng_encode_memory(unsigned char** out, size_t* outsize, - const unsigned char* image, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth); - -/*Same as lodepng_encode_memory, but always encodes from 32-bit RGBA raw image.*/ -unsigned lodepng_encode32(unsigned char** out, size_t* outsize, - const unsigned char* image, unsigned w, unsigned h); - -/*Same as lodepng_encode_memory, but always encodes from 24-bit RGB raw image.*/ -unsigned lodepng_encode24(unsigned char** out, size_t* outsize, - const unsigned char* image, unsigned w, unsigned h); - -#ifdef LODEPNG_COMPILE_DISK -/* -Converts raw pixel data into a PNG file on disk. -Same as the other encode functions, but instead takes a filename as output. - -NOTE: This overwrites existing files without warning! - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and encode in-memory.*/ -unsigned lodepng_encode_file(const char* filename, - const unsigned char* image, unsigned w, unsigned h, - LodePNGColorType colortype, unsigned bitdepth); - -/*Same as lodepng_encode_file, but always encodes from 32-bit RGBA raw image. - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and encode in-memory.*/ -unsigned lodepng_encode32_file(const char* filename, - const unsigned char* image, unsigned w, unsigned h); - -/*Same as lodepng_encode_file, but always encodes from 24-bit RGB raw image. - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and encode in-memory.*/ -unsigned lodepng_encode24_file(const char* filename, - const unsigned char* image, unsigned w, unsigned h); -#endif /*LODEPNG_COMPILE_DISK*/ -#endif /*LODEPNG_COMPILE_ENCODER*/ - - -#ifdef LODEPNG_COMPILE_CPP -namespace lodepng { -#ifdef LODEPNG_COMPILE_DECODER -/*Same as lodepng_decode_memory, but decodes to an std::vector. The colortype -is the format to output the pixels to. Default is RGBA 8-bit per channel.*/ -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - const unsigned char* in, size_t insize, - LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - const std::vector& in, - LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); -#ifdef LODEPNG_COMPILE_DISK -/* -Converts PNG file from disk to raw pixel data in memory. -Same as the other decode functions, but instead takes a filename as input. - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and decode in-memory. -*/ -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - const std::string& filename, - LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); -#endif /* LODEPNG_COMPILE_DISK */ -#endif /* LODEPNG_COMPILE_DECODER */ - -#ifdef LODEPNG_COMPILE_ENCODER -/*Same as lodepng_encode_memory, but encodes to an std::vector. colortype -is that of the raw input data. The output PNG color type will be auto chosen.*/ -unsigned encode(std::vector& out, - const unsigned char* in, unsigned w, unsigned h, - LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); -unsigned encode(std::vector& out, - const std::vector& in, unsigned w, unsigned h, - LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); -#ifdef LODEPNG_COMPILE_DISK -/* -Converts 32-bit RGBA raw pixel data into a PNG file on disk. -Same as the other encode functions, but instead takes a filename as output. - -NOTE: This overwrites existing files without warning! - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and decode in-memory. -*/ -unsigned encode(const std::string& filename, - const unsigned char* in, unsigned w, unsigned h, - LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); -unsigned encode(const std::string& filename, - const std::vector& in, unsigned w, unsigned h, - LodePNGColorType colortype = LCT_RGBA, unsigned bitdepth = 8); -#endif /* LODEPNG_COMPILE_DISK */ -#endif /* LODEPNG_COMPILE_ENCODER */ -} /* namespace lodepng */ -#endif /*LODEPNG_COMPILE_CPP*/ -#endif /*LODEPNG_COMPILE_PNG*/ - -#ifdef LODEPNG_COMPILE_ERROR_TEXT -/*Returns an English description of the numerical error code.*/ -const char* lodepng_error_text(unsigned code); -#endif /*LODEPNG_COMPILE_ERROR_TEXT*/ - -#ifdef LODEPNG_COMPILE_DECODER -/*Settings for zlib decompression*/ -typedef struct LodePNGDecompressSettings LodePNGDecompressSettings; -struct LodePNGDecompressSettings { - /* Check LodePNGDecoderSettings for more ignorable errors such as ignore_crc */ - unsigned ignore_adler32; /*if 1, continue and don't give an error message if the Adler32 checksum is corrupted*/ - unsigned ignore_nlen; /*ignore complement of len checksum in uncompressed blocks*/ - - /*Maximum decompressed size, beyond this the decoder may (and is encouraged to) stop decoding, - return an error, output a data size > max_output_size and all the data up to that point. This is - not hard limit nor a guarantee, but can prevent excessive memory usage. This setting is - ignored by the PNG decoder, but is used by the deflate/zlib decoder and can be used by custom ones. - Set to 0 to impose no limit (the default).*/ - size_t max_output_size; - - /*use custom zlib decoder instead of built in one (default: null). - Should return 0 if success, any non-0 if error (numeric value not exposed).*/ - unsigned (*custom_zlib)(unsigned char**, size_t*, - const unsigned char*, size_t, - const LodePNGDecompressSettings*); - /*use custom deflate decoder instead of built in one (default: null) - if custom_zlib is not null, custom_inflate is ignored (the zlib format uses deflate). - Should return 0 if success, any non-0 if error (numeric value not exposed).*/ - unsigned (*custom_inflate)(unsigned char**, size_t*, - const unsigned char*, size_t, - const LodePNGDecompressSettings*); - - const void* custom_context; /*optional custom settings for custom functions*/ -}; - -extern const LodePNGDecompressSettings lodepng_default_decompress_settings; -void lodepng_decompress_settings_init(LodePNGDecompressSettings* settings); -#endif /*LODEPNG_COMPILE_DECODER*/ - -#ifdef LODEPNG_COMPILE_ENCODER -/* -Settings for zlib compression. Tweaking these settings tweaks the balance -between speed and compression ratio. -*/ -typedef struct LodePNGCompressSettings LodePNGCompressSettings; -struct LodePNGCompressSettings /*deflate = compress*/ { - /*LZ77 related settings*/ - unsigned btype; /*the block type for LZ (0, 1, 2 or 3, see zlib standard). Should be 2 for proper compression.*/ - unsigned use_lz77; /*whether or not to use LZ77. Should be 1 for proper compression.*/ - unsigned windowsize; /*must be a power of two <= 32768. higher compresses more but is slower. Default value: 2048.*/ - unsigned minmatch; /*minimum lz77 length. 3 is normally best, 6 can be better for some PNGs. Default: 0*/ - unsigned nicematch; /*stop searching if >= this length found. Set to 258 for best compression. Default: 128*/ - unsigned lazymatching; /*use lazy matching: better compression but a bit slower. Default: true*/ - - /*use custom zlib encoder instead of built in one (default: null)*/ - unsigned (*custom_zlib)(unsigned char**, size_t*, - const unsigned char*, size_t, - const LodePNGCompressSettings*); - /*use custom deflate encoder instead of built in one (default: null) - if custom_zlib is used, custom_deflate is ignored since only the built in - zlib function will call custom_deflate*/ - unsigned (*custom_deflate)(unsigned char**, size_t*, - const unsigned char*, size_t, - const LodePNGCompressSettings*); - - const void* custom_context; /*optional custom settings for custom functions*/ -}; - -extern const LodePNGCompressSettings lodepng_default_compress_settings; -void lodepng_compress_settings_init(LodePNGCompressSettings* settings); -#endif /*LODEPNG_COMPILE_ENCODER*/ - -#ifdef LODEPNG_COMPILE_PNG -/* -Color mode of an image. Contains all information required to decode the pixel -bits to RGBA colors. This information is the same as used in the PNG file -format, and is used both for PNG and raw image data in LodePNG. -*/ -typedef struct LodePNGColorMode { - /*header (IHDR)*/ - LodePNGColorType colortype; /*color type, see PNG standard or documentation further in this header file*/ - unsigned bitdepth; /*bits per sample, see PNG standard or documentation further in this header file*/ - - /* - palette (PLTE and tRNS) - - Dynamically allocated with the colors of the palette, including alpha. - This field may not be allocated directly, use lodepng_color_mode_init first, - then lodepng_palette_add per color to correctly initialize it (to ensure size - of exactly 1024 bytes). - - The alpha channels must be set as well, set them to 255 for opaque images. - - When decoding, with the default settings you can ignore this palette, since - LodePNG already fills the palette colors in the pixels of the raw RGBA output, - but when decoding to the original PNG color mode it is needed to reconstruct - the colors. - - The palette is only supported for color type 3. - */ - unsigned char* palette; /*palette in RGBARGBA... order. Must be either 0, or when allocated must have 1024 bytes*/ - size_t palettesize; /*palette size in number of colors (amount of used bytes is 4 * palettesize)*/ - - /* - transparent color key (tRNS) - - This color uses the same bit depth as the bitdepth value in this struct, which can be 1-bit to 16-bit. - For grayscale PNGs, r, g and b will all 3 be set to the same. - - When decoding, by default you can ignore this information, since LodePNG sets - pixels with this key to transparent already in the raw RGBA output. - - The color key is only supported for color types 0 and 2. - */ - unsigned key_defined; /*is a transparent color key given? 0 = false, 1 = true*/ - unsigned key_r; /*red/grayscale component of color key*/ - unsigned key_g; /*green component of color key*/ - unsigned key_b; /*blue component of color key*/ -} LodePNGColorMode; - -/*init, cleanup and copy functions to use with this struct*/ -void lodepng_color_mode_init(LodePNGColorMode* info); -void lodepng_color_mode_cleanup(LodePNGColorMode* info); -/*return value is error code (0 means no error)*/ -unsigned lodepng_color_mode_copy(LodePNGColorMode* dest, const LodePNGColorMode* source); -/* Makes a temporary LodePNGColorMode that does not need cleanup (no palette) */ -LodePNGColorMode lodepng_color_mode_make(LodePNGColorType colortype, unsigned bitdepth); - -void lodepng_palette_clear(LodePNGColorMode* info); -/*add 1 color to the palette*/ -unsigned lodepng_palette_add(LodePNGColorMode* info, - unsigned char r, unsigned char g, unsigned char b, unsigned char a); - -/*get the total amount of bits per pixel, based on colortype and bitdepth in the struct*/ -unsigned lodepng_get_bpp(const LodePNGColorMode* info); -/*get the amount of color channels used, based on colortype in the struct. -If a palette is used, it counts as 1 channel.*/ -unsigned lodepng_get_channels(const LodePNGColorMode* info); -/*is it a grayscale type? (only colortype 0 or 4)*/ -unsigned lodepng_is_greyscale_type(const LodePNGColorMode* info); -/*has it got an alpha channel? (only colortype 2 or 6)*/ -unsigned lodepng_is_alpha_type(const LodePNGColorMode* info); -/*has it got a palette? (only colortype 3)*/ -unsigned lodepng_is_palette_type(const LodePNGColorMode* info); -/*only returns true if there is a palette and there is a value in the palette with alpha < 255. -Loops through the palette to check this.*/ -unsigned lodepng_has_palette_alpha(const LodePNGColorMode* info); -/* -Check if the given color info indicates the possibility of having non-opaque pixels in the PNG image. -Returns true if the image can have translucent or invisible pixels (it still be opaque if it doesn't use such pixels). -Returns false if the image can only have opaque pixels. -In detail, it returns true only if it's a color type with alpha, or has a palette with non-opaque values, -or if "key_defined" is true. -*/ -unsigned lodepng_can_have_alpha(const LodePNGColorMode* info); -/*Returns the byte size of a raw image buffer with given width, height and color mode*/ -size_t lodepng_get_raw_size(unsigned w, unsigned h, const LodePNGColorMode* color); - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS -/*The information of a Time chunk in PNG.*/ -typedef struct LodePNGTime { - unsigned year; /*2 bytes used (0-65535)*/ - unsigned month; /*1-12*/ - unsigned day; /*1-31*/ - unsigned hour; /*0-23*/ - unsigned minute; /*0-59*/ - unsigned second; /*0-60 (to allow for leap seconds)*/ -} LodePNGTime; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - -/*Information about the PNG image, except pixels, width and height.*/ -typedef struct LodePNGInfo { - /*header (IHDR), palette (PLTE) and transparency (tRNS) chunks*/ - unsigned compression_method;/*compression method of the original file. Always 0.*/ - unsigned filter_method; /*filter method of the original file*/ - unsigned interlace_method; /*interlace method of the original file: 0=none, 1=Adam7*/ - LodePNGColorMode color; /*color type and bits, palette and transparency of the PNG file*/ - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /* - Suggested background color chunk (bKGD) - - This uses the same color mode and bit depth as the PNG (except no alpha channel), - with values truncated to the bit depth in the unsigned integer. - - For grayscale and palette PNGs, the value is stored in background_r. The values - in background_g and background_b are then unused. The decoder will set them - equal to background_r, the encoder ignores them in this case. - - When decoding, you may get these in a different color mode than the one you requested - for the raw pixels: the colortype and bitdepth defined by info_png.color, that is the - ones defined in the header of the PNG image, are used. - - When encoding with auto_convert, you must use the color model defined in info_png.color for - these values. The encoder normally ignores info_png.color when auto_convert is on, but will - use it to interpret these values (and convert copies of them to its chosen color model). - - When encoding, avoid setting this to an expensive color, such as a non-gray value - when the image is gray, or the compression will be worse since it will be forced to - write the PNG with a more expensive color mode (when auto_convert is on). - - The decoder does not use this background color to edit the color of pixels. This is a - completely optional metadata feature. - */ - unsigned background_defined; /*is a suggested background color given?*/ - unsigned background_r; /*red/gray/palette component of suggested background color*/ - unsigned background_g; /*green component of suggested background color*/ - unsigned background_b; /*blue component of suggested background color*/ - - /* - Non-international text chunks (tEXt and zTXt) - - The char** arrays each contain num strings. The actual messages are in - text_strings, while text_keys are keywords that give a short description what - the actual text represents, e.g. Title, Author, Description, or anything else. - - All the string fields below including strings, keys, names and language tags are null terminated. - The PNG specification uses null characters for the keys, names and tags, and forbids null - characters to appear in the main text which is why we can use null termination everywhere here. - - A keyword is minimum 1 character and maximum 79 characters long (plus the - additional null terminator). It's discouraged to use a single line length - longer than 79 characters for texts. - - Don't allocate these text buffers yourself. Use the init/cleanup functions - correctly and use lodepng_add_text and lodepng_clear_text. - - Standard text chunk keywords and strings are encoded using Latin-1. - */ - size_t text_num; /*the amount of texts in these char** buffers (there may be more texts in itext)*/ - char** text_keys; /*the keyword of a text chunk (e.g. "Comment")*/ - char** text_strings; /*the actual text*/ - - /* - International text chunks (iTXt) - Similar to the non-international text chunks, but with additional strings - "langtags" and "transkeys", and the following text encodings are used: - keys: Latin-1, langtags: ASCII, transkeys and strings: UTF-8. - keys must be 1-79 characters (plus the additional null terminator), the other - strings are any length. - */ - size_t itext_num; /*the amount of international texts in this PNG*/ - char** itext_keys; /*the English keyword of the text chunk (e.g. "Comment")*/ - char** itext_langtags; /*language tag for this text's language, ISO/IEC 646 string, e.g. ISO 639 language tag*/ - char** itext_transkeys; /*keyword translated to the international language - UTF-8 string*/ - char** itext_strings; /*the actual international text - UTF-8 string*/ - - /* - Optional exif metadata in exif_size bytes. - Don't allocate this buffer yourself. Use the init/cleanup functions - correctly and use lodepng_set_exif and lodepng_clear_exif. - The exif data is in exif-encoded form but without JPEG markers, starting with the 'II' or 'MM' marker that indicates - endianness. It's up to an exif handling library to encode/decode its information. - */ - unsigned exif_defined; /* Whether exif metadata is present, that is, the PNG image has an eXIf chunk */ - unsigned char* exif; /* The bytes of the exif metadata, if present */ - unsigned exif_size; /* The size of the exif data in bytes */ - - - /*time chunk (tIME)*/ - unsigned time_defined; /*set to 1 to make the encoder generate a tIME chunk*/ - LodePNGTime time; - - /*phys chunk (pHYs)*/ - unsigned phys_defined; /*if 0, there is no pHYs chunk and the values below are undefined, if 1 else there is one*/ - unsigned phys_x; /*pixels per unit in x direction*/ - unsigned phys_y; /*pixels per unit in y direction*/ - unsigned phys_unit; /*may be 0 (unknown unit) or 1 (metre)*/ - - /* - Color profile related chunk types: cICP, iCPP, sRGB, gAMA, cHRM, sBIT - - LodePNG does not apply any color conversions on pixels in the encoder or decoder and does not interpret these color - profile values. It merely passes on the information. If you wish to use color profiles and convert colors, a separate - color management library should be used. There is also a limited library for this in lodepng_util.h. - - There are 4 types of (sets of) chunks providing color information. If multiple are present, each will be decoded by - LodePNG, but only one should be handled by the user, with the following order of priority depending on what the user - supports: - 1: cICP: Coding-independent code points (CICP) - 2: iCCP: ICC profile - 3: sRGB: indicates the image is in the sRGB color profile - 4: gAMA and cHRM: indicates a gamma and chromaticity value to define the color profile - */ - - /* - gAMA chunk: Image gamma - Optional, overridden by cICP, iCCP or sRGB if those are present. - Together with cHRM, this is a primitive way of specifying the image color profile. - */ - unsigned gama_defined; /* Whether a gAMA chunk is present (0 = not present, 1 = present). */ - unsigned gama_gamma; /* Gamma exponent times 100000 */ - - /* - cHRM chunk: Primary chromaticities and white point - Optional, overridden by cICP, iCCP or sRGB if those are present. - Together with gAMA, this is a primitive way of specifying the image color profile. - */ - unsigned chrm_defined; /* Whether a cHRM chunk is present (0 = not present, 1 = present). */ - unsigned chrm_white_x; /* White Point x times 100000 */ - unsigned chrm_white_y; /* White Point y times 100000 */ - unsigned chrm_red_x; /* Red x times 100000 */ - unsigned chrm_red_y; /* Red y times 100000 */ - unsigned chrm_green_x; /* Green x times 100000 */ - unsigned chrm_green_y; /* Green y times 100000 */ - unsigned chrm_blue_x; /* Blue x times 100000 */ - unsigned chrm_blue_y; /* Blue y times 100000 */ - - /* - sRGB chunk: Indicates the image is in the sRGB color space. - Optional. Should not appear at the same time as iCCP. - If gAMA is also present gAMA must contain value 45455. - If cHRM is also present cHRM must contain respectively 31270,32900,64000,33000,30000,60000,15000,6000. - */ - unsigned srgb_defined; /* Whether an sRGB chunk is present (0 = not present, 1 = present). */ - unsigned srgb_intent; /* Rendering intent: 0=perceptual, 1=rel. colorimetric, 2=saturation, 3=abs. colorimetric */ - - /* - iCCP chunk: Embedded ICC profile. - Optional. Should not appear at the same time as sRGB. - - Contains ICC profile, which can use any version of the ICC.1 specification by the International Color Consortium. See - its specification for more details. LodePNG does not parse or use the ICC profile (except its color space header - field for "RGB" or "GRAY", see below), a separate library to handle the ICC data format is needed to use it for color - management and conversions. - - For encoding, if iCCP is present, the PNG specification recommends to also add gAMA and cHRM chunks that approximate - the ICC profile, for compatibility with applications that don't use the ICC chunk. This is not required, and it's up - to the user to compute approximate values and set then in the appropriate gama_ and chrm_ fields, LodePNG does not do - this automatically since it does not interpret the ICC profile. - - For encoding, the ICC profile is required by the PNG specification to be an "RGB" profile for non-gray PNG color - types (types 2, 3 and 6) and a "GRAY" profile for gray PNG color types (types 1 and 4). If you disable auto_convert, - you must ensure the ICC profile type matches your requested color type, else the encoder gives an error. If - auto_convert is enabled (the default), and the ICC profile is not a correct match for the pixel data, this will result - in an encoder error if the pixel data has non-gray pixels for a GRAY profile, or a silent less-optimal compression of - the pixel data if the pixels could be encoded as grayscale but the ICC profile is RGB. - - To avoid this do not set an ICC profile in the image unless there is a good reason for it, and when doing so - make sure you compute it carefully to avoid the above problems. - */ - unsigned iccp_defined; /* Whether an iCCP chunk is present (0 = not present, 1 = present). */ - char* iccp_name; /* Null terminated string with profile name, 1-79 bytes */ - /* - The ICC profile in iccp_profile_size bytes. - Don't allocate this buffer yourself. Use the init/cleanup functions - correctly and use lodepng_set_icc and lodepng_clear_icc. - */ - unsigned char* iccp_profile; - unsigned iccp_profile_size; /* The size of iccp_profile in bytes */ - - /* - cICP chunk: Coding-independent code points for video signal type identification. - Optional. If present, and supported, overrides iCCP, sRGB, gAMA and cHRM. - The meaning of the values are as defined in the specification ITU-T-H.273. LodePNG does not - use these values, only passes on the metadata. The meaning of the values is they are enum - values representing certain color spaces, including HDR color spaces, such as Display P3, - PQ and HLG. The video full range flag value should typically be 1 for the use cases of PNG - images, but can be 0 for narrow-range images in certain video editing workflows. - */ - unsigned cicp_defined; /* Whether an cICP chunk is present (0 = not present, 1 = present). */ - unsigned cicp_color_primaries; /* Colour primaries value */ - unsigned cicp_transfer_function; /* Transfer characteristics value */ - unsigned cicp_matrix_coefficients; /* Matrix coefficients value */ - unsigned cicp_video_full_range_flag; /* Video full range flag value */ - - /* - mDCV chunk: Mastering Display Color Volume. - Optional, typically used in conjunction with certain HDR color spaces that can - be represented by the cICP chunk. - See the PNG specification, third edition, for more information on this chunk. - All the red, green, blue and white x and y values are encoded as 16-bit - integers and therefore must be in range 0-65536. The min and max luminance - values are 32-bit integers. - */ - unsigned mdcv_defined; /* Whether an mDCV chunk is present (0 = not present, 1 = present). */ - /* Mastering display color primary chromaticities (CIE 1931 x,y of R,G,B) */ - unsigned mdcv_red_x; /* Red x times 50000 */ - unsigned mdcv_red_y; /* Red y times 50000 */ - unsigned mdcv_green_x; /* Green x times 50000 */ - unsigned mdcv_green_y; /* Green y times 50000 */ - unsigned mdcv_blue_x; /* Blue x times 50000 */ - unsigned mdcv_blue_y; /* Blue y times 50000 */ - /* Mastering display white point chromaticity (CIE 1931 x,y) */ - unsigned mdcv_white_x; /* White Point x times 50000 */ - unsigned mdcv_white_y; /* White Point y times 50000 */ - /* Mastering display luminance */ - unsigned mdcv_max_luminance; /* Max luminance in cd/m^2 times 10000 */ - unsigned mdcv_min_luminance; /* Min luminance in cd/m^2 times 10000 */ - - /* - cLLI chunk: Content Light Level Information. - Optional, typically used in conjunction with certain HDR color spaces that can - be represented by the cICP chunk. - See the PNG specification, third edition, for more information on this chunk. - The clli_max_cll and clli_max_fall values are 32-bit integers. - */ - unsigned clli_defined; /* Whether a cLLI chunk is present (0 = not present, 1 = present). */ - unsigned clli_max_cll; /* Maximum Content Light Level (MaxCLL) in cd/m^2 times 10000 */ - unsigned clli_max_fall; /* Maximum Frame-Average Light Level (MaxFALL) in cd/m^2 times 10000 */ - - /* - sBIT chunk: significant bits. - Optional metadata, only set this if needed. - - If defined, these values give the bit depth of the original data. Since PNG only stores 1, 2, 4, 8 or 16-bit - per channel data, the significant bits value can be used to indicate the original encoded data has another - sample depth, such as 10 or 12. - - Encoders using this value, when storing the pixel data, should use the most significant bits - of the data to store the original bits, and use a good sample depth scaling method such as - "left bit replication" to fill in the least significant bits, rather than fill zeroes. - - Decoders using this value, if able to work with data that's e.g. 10-bit or 12-bit, should right - shift the data to go back to the original bit depth, but decoders are also allowed to ignore - sbit and work e.g. with the 8-bit or 16-bit data from the PNG directly, since thanks - to the encoder contract, the values encoded in PNG are in valid range for the PNG bit depth. - - For grayscale images, sbit_g and sbit_b are not used, and for images that don't use color - type RGBA or grayscale+alpha, sbit_a is not used (it's not used even for palette images with - translucent palette values, or images with color key). The values that are used must be - greater than zero and smaller than or equal to the PNG bit depth. - - The color type from the header in the PNG image defines these used and unused fields: if - decoding with a color mode conversion, such as always decoding to RGBA, this metadata still - only uses the color type of the original PNG, and may e.g. lack the alpha channel info - if the PNG was RGB. When encoding with auto_convert (as well as without), also always the - color model defined in info_png.color determines this. - - NOTE: enabling sbit can hurt compression, because the encoder can then not always use - auto_convert to choose a more optimal color mode for the data, because the PNG format has - strict requirements for the allowed sbit values in combination with color modes. - For example, setting these fields to 10-bit will force the encoder to keep using a 16-bit per channel - color mode, even if the pixel data would in fact fit in a more efficient 8-bit mode. - */ - unsigned sbit_defined; /*is significant bits given? if not, the values below are unused*/ - unsigned sbit_r; /*red or gray component of significant bits*/ - unsigned sbit_g; /*green component of significant bits*/ - unsigned sbit_b; /*blue component of significant bits*/ - unsigned sbit_a; /*alpha component of significant bits*/ - - /* End of color profile related chunks */ - - - /* - unknown chunks: chunks not known by LodePNG, passed on byte for byte. - - There are 3 buffers, one for each position in the PNG where unknown chunks can appear. - Each buffer contains all unknown chunks for that position consecutively. - The 3 positions are: - 0: between IHDR and PLTE, 1: between PLTE and IDAT, 2: between IDAT and IEND. - - For encoding, do not store critical chunks or known chunks that are enabled with a "_defined" flag - above in here, since the encoder will blindly follow this and could then encode an invalid PNG file - (such as one with two IHDR chunks or the disallowed combination of sRGB with iCCP). But do use - this if you wish to store an ancillary chunk that is not supported by LodePNG (such as sPLT or hIST), - or any non-standard PNG chunk. - - Do not allocate or traverse this data yourself. Use the chunk traversing functions declared - later, such as lodepng_chunk_next and lodepng_chunk_append, to read/write this struct. - */ - unsigned char* unknown_chunks_data[3]; - size_t unknown_chunks_size[3]; /*size in bytes of the unknown chunks, given for protection*/ -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ -} LodePNGInfo; - -/*init, cleanup and copy functions to use with this struct*/ -void lodepng_info_init(LodePNGInfo* info); -/*destructs the LodePNGInfo and brings it to invalid state, requiring lodepng_info_init again before reusing it*/ -void lodepng_info_cleanup(LodePNGInfo* info); -/*return value is error code (0 means no error)*/ -unsigned lodepng_info_copy(LodePNGInfo* dest, const LodePNGInfo* source); - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS -unsigned lodepng_add_text(LodePNGInfo* info, const char* key, const char* str); /*push back both texts at once*/ -void lodepng_clear_text(LodePNGInfo* info); /*use this to clear the texts again after you filled them in*/ - -unsigned lodepng_add_itext(LodePNGInfo* info, const char* key, const char* langtag, - const char* transkey, const char* str); /*push back the 4 texts of 1 chunk at once*/ -void lodepng_clear_itext(LodePNGInfo* info); /*use this to clear the itexts again after you filled them in*/ - -/*replaces if exists*/ -unsigned lodepng_set_icc(LodePNGInfo* info, const char* name, const unsigned char* profile, unsigned profile_size); -void lodepng_clear_icc(LodePNGInfo* info); /*use this to clear the profile again after you filled it in*/ - -/*replaces if exists*/ -unsigned lodepng_set_exif(LodePNGInfo* info, const unsigned char* exif, unsigned exif_size); -void lodepng_clear_exif(LodePNGInfo* info); /*use this to clear the exif metadata again after you filled it in*/ -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ - -/* -Converts raw buffer from one color type to another color type, based on -LodePNGColorMode structs to describe the input and output color type. -See the reference manual at the end of this header file to see which color conversions are supported. -return value = LodePNG error code (0 if all went ok, an error if the conversion isn't supported) -The out buffer must have size (w * h * bpp + 7) / 8, where bpp is the bits per pixel -of the output color type (lodepng_get_bpp). -For < 8 bpp images, there should not be padding bits at the end of scanlines. -For 16-bit per channel colors, uses big endian format like PNG does. -Return value is LodePNG error code -*/ -unsigned lodepng_convert(unsigned char* out, const unsigned char* in, - const LodePNGColorMode* mode_out, const LodePNGColorMode* mode_in, - unsigned w, unsigned h); - -#ifdef LODEPNG_COMPILE_DECODER -/* -Settings for the decoder. This contains settings for the PNG and the Zlib -decoder, but not the Info settings from the Info structs. -*/ -typedef struct LodePNGDecoderSettings { - LodePNGDecompressSettings zlibsettings; /*in here is the setting to ignore Adler32 checksums*/ - - /* Check LodePNGDecompressSettings for more ignorable errors such as ignore_adler32 */ - unsigned ignore_crc; /*ignore CRC checksums*/ - unsigned ignore_critical; /*ignore unknown critical chunks*/ - unsigned ignore_end; /*ignore issues at end of file if possible (missing IEND chunk, too large chunk, ...)*/ - /* TODO: make a system involving warnings with levels and a strict mode instead. Other potentially recoverable - errors: srgb rendering intent value, size of content of ancillary chunks, more than 79 characters for some - strings, placement/combination rules for ancillary chunks, crc of unknown chunks, allowed characters - in string keys, invalid characters in chunk types names, etc... */ - - unsigned color_convert; /*whether to convert the PNG to the color type you want. Default: yes*/ - -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - unsigned read_text_chunks; /*if false but remember_unknown_chunks is true, they're stored in the unknown chunks*/ - - /*store all bytes from unknown chunks in the LodePNGInfo (off by default, useful for a png editor)*/ - unsigned remember_unknown_chunks; - - /* maximum size for decompressed text chunks. If a text chunk's text is larger than this, an error is returned, - unless reading text chunks is disabled or this limit is set higher or disabled. Set to 0 to allow any size. - By default it is a value that prevents unreasonably large strings from hogging memory. */ - size_t max_text_size; - - /* maximum size for compressed ICC chunks. If the ICC profile is larger than this, an error will be returned. Set to - 0 to allow any size. By default this is a value that prevents ICC profiles that would be much larger than any - legitimate profile could be to hog memory. */ - size_t max_icc_size; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ -} LodePNGDecoderSettings; - -void lodepng_decoder_settings_init(LodePNGDecoderSettings* settings); -#endif /*LODEPNG_COMPILE_DECODER*/ - -#ifdef LODEPNG_COMPILE_ENCODER -/*strategy to use to choose the PNG filter per scanline. Strategies 0-4 correspond -to each of the 5 filter types PNG supports, the next values are adaptive strategies*/ -typedef enum LodePNGFilterStrategy { - /*every filter at zero*/ - LFS_ZERO = 0, - /*every filter at 1, 2, 3 or 4 (paeth), unlike LFS_ZERO not a good choice, but for testing*/ - LFS_ONE = 1, - LFS_TWO = 2, - LFS_THREE = 3, - LFS_FOUR = 4, - /*Use the filter out of the 5 above types that gives minimum sum, by trying each one. This is the adaptive filtering - suggested heuristic in the PNG standard chapter 'Filter selection'.*/ - LFS_MINSUM, - /*Use the filter type that gives smallest Shannon entropy for this scanline. Depending - on the image, this is better or worse than minsum.*/ - LFS_ENTROPY, - /* - Brute-force-search PNG filters by compressing each filter for each scanline. - Experimental, very slow, and only rarely gives better compression than MINSUM. - */ - LFS_BRUTE_FORCE, - /*use predefined_filters buffer: you specify the filter type for each scanline*/ - LFS_PREDEFINED -} LodePNGFilterStrategy; - -/*Gives characteristics about the integer RGBA colors of the image (count, alpha channel usage, bit depth, ...), -which helps decide which color model to use for encoding. -Used internally by default if "auto_convert" is enabled. Public because it's useful for custom algorithms.*/ -typedef struct LodePNGColorStats { - unsigned colored; /*not grayscale*/ - unsigned key; /*image is not opaque and color key is possible instead of full alpha*/ - unsigned short key_r; /*key values, always as 16-bit, in 8-bit case the byte is duplicated, e.g. 65535 means 255*/ - unsigned short key_g; - unsigned short key_b; - unsigned alpha; /*image is not opaque and alpha channel or alpha palette required*/ - unsigned numcolors; /*amount of colors, up to 257. Not valid if bits == 16 or allow_palette is disabled.*/ - unsigned char palette[1024]; /*Remembers up to the first 256 RGBA colors, in no particular order, only valid when numcolors is valid*/ - unsigned bits; /*bits per channel (not for palette). 1,2 or 4 for grayscale only. 16 if 16-bit per channel required.*/ - size_t numpixels; - - /*user settings for computing/using the stats*/ - unsigned allow_palette; /*default 1. if 0, disallow choosing palette colortype in auto_choose_color, and don't count numcolors*/ - unsigned allow_greyscale; /*default 1. if 0, choose RGB or RGBA even if the image only has gray colors*/ -} LodePNGColorStats; - -void lodepng_color_stats_init(LodePNGColorStats* stats); - -/*Get a LodePNGColorStats of the image. The stats must already have been inited. -Returns error code (e.g. alloc fail) or 0 if ok.*/ -unsigned lodepng_compute_color_stats(LodePNGColorStats* stats, - const unsigned char* image, unsigned w, unsigned h, - const LodePNGColorMode* mode_in); - -/*Settings for the encoder.*/ -typedef struct LodePNGEncoderSettings { - LodePNGCompressSettings zlibsettings; /*settings for the zlib encoder, such as window size, ...*/ - - /*automatically choose output PNG color type. If false, must explicitly choose the output color - type in state.info_png.color.colortype, info_png.color.bitdepth and optionally its palette. - Default: true*/ - unsigned auto_convert; - - /*If true, follows the suggestion in the PNG standard in chapter 'Filter selection': if the PNG uses - a palette or lower than 8 bit depth, set all filters to zero. - In other cases this will use the heuristic from the chosen filter_strategy. The PNG standard - suggests LFS_MINSUM for those cases.*/ - unsigned filter_palette_zero; - /*Which filter strategy to use when not using zeroes due to filter_palette_zero. - Set filter_palette_zero to 0 to ensure always using your chosen strategy. Default: LFS_MINSUM*/ - LodePNGFilterStrategy filter_strategy; - /*used if filter_strategy is LFS_PREDEFINED. In that case, this must point to a buffer with - the same length as the amount of scanlines in the image, and each value must <= 5. You - have to cleanup this buffer, LodePNG will never free it. Don't forget that filter_palette_zero - must be set to 0 to ensure this is also used on palette or low bitdepth images.*/ - const unsigned char* predefined_filters; - - /*force creating a PLTE chunk if colortype is 2 or 6 (= a suggested palette). - If colortype is 3, PLTE is always created. If color type is explicitly set - to a grayscale type (1 or 4), this is not done and is ignored. If enabling this, - a palette must be present in the info_png. - NOTE: enabling this may worsen compression if auto_convert is used to choose - optimal color mode, because it cannot use grayscale color modes in this case*/ - unsigned force_palette; -#ifdef LODEPNG_COMPILE_ANCILLARY_CHUNKS - /*add LodePNG identifier and version as a text chunk, for debugging*/ - unsigned add_id; - /*encode text chunks as zTXt chunks instead of tEXt chunks, and use compression in iTXt chunks*/ - unsigned text_compression; -#endif /*LODEPNG_COMPILE_ANCILLARY_CHUNKS*/ -} LodePNGEncoderSettings; - -void lodepng_encoder_settings_init(LodePNGEncoderSettings* settings); -#endif /*LODEPNG_COMPILE_ENCODER*/ - - -#if defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) -/*The settings, state and information for extended encoding and decoding. - -Using this struct requires using lodepng_state_init to initialize it -and using lodepng_state_cleanup to deconstruct it. If using C++, you can -use lodepng::State instead which does those things automatically with RAII. - -While a LodePNGState can be reused once in a chain of lodepng_decode followed by -lodepng_encode, it's not recommended to reuse it for multiple encode, decode -or inspect calls, and if any such function returns an error code, the -LodePNGState should not be reused at all as it can be in an unexpected state.. -*/ -typedef struct LodePNGState { -#ifdef LODEPNG_COMPILE_DECODER - LodePNGDecoderSettings decoder; /*the decoding settings*/ -#endif /*LODEPNG_COMPILE_DECODER*/ -#ifdef LODEPNG_COMPILE_ENCODER - LodePNGEncoderSettings encoder; /*the encoding settings*/ -#endif /*LODEPNG_COMPILE_ENCODER*/ - LodePNGColorMode info_raw; /*specifies the format in which you would like to get the raw pixel buffer*/ - LodePNGInfo info_png; /*info of the PNG image obtained after decoding*/ - unsigned error; /*deprecated, use the return value of the encode/decode functions to check errors instead*/ -} LodePNGState; - -/*init, cleanup and copy functions to use with this struct*/ -void lodepng_state_init(LodePNGState* state); -/*destructs the LodePNGState and brings it to invalid state, requiring lodepng_info_init again before reusing it*/ -void lodepng_state_cleanup(LodePNGState* state); -/*return value is error code (0 means no error)*/ -unsigned lodepng_state_copy(LodePNGState* dest, const LodePNGState* source); -#endif /* defined(LODEPNG_COMPILE_DECODER) || defined(LODEPNG_COMPILE_ENCODER) */ - -#ifdef LODEPNG_COMPILE_DECODER -/* -Same as lodepng_decode_memory, but uses a LodePNGState to allow custom settings and -getting much more information about the PNG image and color mode. -*/ -unsigned lodepng_decode(unsigned char** out, unsigned* w, unsigned* h, - LodePNGState* state, - const unsigned char* in, size_t insize); - -/* -Read the PNG header, but not the actual data. This returns only the information -that is in the IHDR chunk of the PNG, such as width, height and color type. The -information is placed in the info_png field of the LodePNGState. -*/ -unsigned lodepng_inspect(unsigned* w, unsigned* h, - LodePNGState* state, - const unsigned char* in, size_t insize); -#endif /*LODEPNG_COMPILE_DECODER*/ - -/* -Reads one metadata chunk (other than IHDR, which is handled by lodepng_inspect) -of the PNG file and outputs what it read in the state. Returns error code on failure. -Use lodepng_inspect first with a new state, then e.g. lodepng_chunk_find_const -to find the desired chunk type, and if non null use lodepng_inspect_chunk (with -chunk_pointer - start_of_file as pos). -Supports most metadata chunks from the PNG standard (gAMA, bKGD, tEXt, ...). -Ignores unsupported, unknown, non-metadata or IHDR chunks (without error). -Requirements: &in[pos] must point to start of a chunk, must use regular -lodepng_inspect first since format of most other chunks depends on IHDR, and if -there is a PLTE chunk, that one must be inspected before tRNS or bKGD. -*/ -unsigned lodepng_inspect_chunk(LodePNGState* state, size_t pos, - const unsigned char* in, size_t insize); - -#ifdef LODEPNG_COMPILE_ENCODER -/*This function allocates the out buffer with standard malloc and stores the size in *outsize.*/ -unsigned lodepng_encode(unsigned char** out, size_t* outsize, - const unsigned char* image, unsigned w, unsigned h, - LodePNGState* state); -#endif /*LODEPNG_COMPILE_ENCODER*/ - -/* -The lodepng_chunk functions are normally not needed, except to traverse the -unknown chunks stored in the LodePNGInfo struct, or add new ones to it. -It also allows traversing the chunks of an encoded PNG file yourself. - -The chunk pointer always points to the beginning of the chunk itself, that is -the first byte of the 4 length bytes. - -In the PNG file format, chunks have the following format: --4 bytes length: length of the data of the chunk in bytes (chunk itself is 12 bytes longer) --4 bytes chunk type (ASCII a-z,A-Z only, see below) --length bytes of data (may be 0 bytes if length was 0) --4 bytes of CRC, computed on chunk name + data - -The first chunk starts at the 8th byte of the PNG file, the entire rest of the file -exists out of concatenated chunks with the above format. - -PNG standard chunk ASCII naming conventions: --First byte: uppercase = critical, lowercase = ancillary --Second byte: uppercase = public, lowercase = private --Third byte: must be uppercase --Fourth byte: uppercase = unsafe to copy, lowercase = safe to copy -*/ - -/* -Gets the length of the data of the chunk. Total chunk length has 12 bytes more. -There must be at least 4 bytes to read from. If the result value is too large, -it may be corrupt data. -*/ -unsigned lodepng_chunk_length(const unsigned char* chunk); - -/*puts the 4-byte type in null terminated string*/ -void lodepng_chunk_type(char type[5], const unsigned char* chunk); - -/*check if the type is the given type*/ -unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, const char* type); - -/*0: it's one of the critical chunk types, 1: it's an ancillary chunk (see PNG standard)*/ -unsigned char lodepng_chunk_ancillary(const unsigned char* chunk); - -/*0: public, 1: private (see PNG standard)*/ -unsigned char lodepng_chunk_private(const unsigned char* chunk); - -/*0: the chunk is unsafe to copy, 1: the chunk is safe to copy (see PNG standard)*/ -unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk); - -/*get pointer to the data of the chunk, where the input points to the header of the chunk*/ -unsigned char* lodepng_chunk_data(unsigned char* chunk); -const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk); - -/*returns 0 if the crc is correct, 1 if it's incorrect (0 for OK as usual!)*/ -unsigned lodepng_chunk_check_crc(const unsigned char* chunk); - -/*generates the correct CRC from the data and puts it in the last 4 bytes of the chunk*/ -void lodepng_chunk_generate_crc(unsigned char* chunk); - -/* -Iterate to next chunks, allows iterating through all chunks of the PNG file. -Input must be at the beginning of a chunk (result of a previous lodepng_chunk_next call, -or the 8th byte of a PNG file which always has the first chunk), or alternatively may -point to the first byte of the PNG file (which is not a chunk but the magic header, the -function will then skip over it and return the first real chunk). -Will output pointer to the start of the next chunk, or at or beyond end of the file if there -is no more chunk after this or possibly if the chunk is corrupt. -Start this process at the 8th byte of the PNG file. -In a non-corrupt PNG file, the last chunk should have name "IEND". -*/ -unsigned char* lodepng_chunk_next(unsigned char* chunk, unsigned char* end); -const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk, const unsigned char* end); - -/*Finds the first chunk with the given type in the range [chunk, end), or returns NULL if not found.*/ -unsigned char* lodepng_chunk_find(unsigned char* chunk, unsigned char* end, const char type[5]); -const unsigned char* lodepng_chunk_find_const(const unsigned char* chunk, const unsigned char* end, const char type[5]); - -/* -Appends chunk to the data in out. The given chunk should already have its chunk header. -The out variable and outsize are updated to reflect the new reallocated buffer. -Returns error code (0 if it went ok) -*/ -unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsigned char* chunk); - -/* -Appends new chunk to out. The chunk to append is given by giving its length, type -and data separately. The type is a 4-letter string. -The out variable and outsize are updated to reflect the new reallocated buffer. -Returns error code (0 if it went ok) -*/ -unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, size_t length, - const char* type, const unsigned char* data); - - -/*Calculate CRC32 of buffer*/ -unsigned lodepng_crc32(const unsigned char* buf, size_t len); -#endif /*LODEPNG_COMPILE_PNG*/ - - -#ifdef LODEPNG_COMPILE_ZLIB -/* -This zlib part can be used independently to zlib compress and decompress a -buffer. It cannot be used to create gzip files however, and it only supports the -part of zlib that is required for PNG, it does not support dictionaries. -*/ - -#ifdef LODEPNG_COMPILE_DECODER -/*Inflate a buffer. Inflate is the decompression step of deflate. Out buffer must be freed after use.*/ -unsigned lodepng_inflate(unsigned char** out, size_t* outsize, - const unsigned char* in, size_t insize, - const LodePNGDecompressSettings* settings); - -/* -Decompresses Zlib data. Reallocates the out buffer and appends the data. The -data must be according to the zlib specification. -Either, *out must be NULL and *outsize must be 0, or, *out must be a valid -buffer and *outsize its size in bytes. out must be freed by user after usage. -*/ -unsigned lodepng_zlib_decompress(unsigned char** out, size_t* outsize, - const unsigned char* in, size_t insize, - const LodePNGDecompressSettings* settings); -#endif /*LODEPNG_COMPILE_DECODER*/ - -#ifdef LODEPNG_COMPILE_ENCODER -/* -Compresses data with Zlib. Reallocates the out buffer and appends the data. -Zlib adds a small header and trailer around the deflate data. -The data is output in the format of the zlib specification. -Either, *out must be NULL and *outsize must be 0, or, *out must be a valid -buffer and *outsize its size in bytes. out must be freed by user after usage. -*/ -unsigned lodepng_zlib_compress(unsigned char** out, size_t* outsize, - const unsigned char* in, size_t insize, - const LodePNGCompressSettings* settings); - -/* -Find length-limited Huffman code for given frequencies. This function is in the -public interface only for tests, it's used internally by lodepng_deflate. -*/ -unsigned lodepng_huffman_code_lengths(unsigned* lengths, const unsigned* frequencies, - size_t numcodes, unsigned maxbitlen); - -/*Compress a buffer with deflate. See RFC 1951. Out buffer must be freed after use.*/ -unsigned lodepng_deflate(unsigned char** out, size_t* outsize, - const unsigned char* in, size_t insize, - const LodePNGCompressSettings* settings); - -#endif /*LODEPNG_COMPILE_ENCODER*/ -#endif /*LODEPNG_COMPILE_ZLIB*/ - -#ifdef LODEPNG_COMPILE_DISK -/* -Load a file from disk into buffer. The function allocates the out buffer, and -after usage you should free it. -out: output parameter, contains pointer to loaded buffer. -outsize: output parameter, size of the allocated out buffer -filename: the path to the file to load -return value: error code (0 means ok) - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and decode in-memory. -*/ -unsigned lodepng_load_file(unsigned char** out, size_t* outsize, const char* filename); - -/* -Save a file from buffer to disk. Warning, if it exists, this function overwrites -the file without warning! -buffer: the buffer to write -buffersize: size of the buffer to write -filename: the path to the file to save to -return value: error code (0 means ok) - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and encode in-memory -*/ -unsigned lodepng_save_file(const unsigned char* buffer, size_t buffersize, const char* filename); -#endif /*LODEPNG_COMPILE_DISK*/ - -#ifdef LODEPNG_COMPILE_CPP -/* The LodePNG C++ wrapper uses std::vectors instead of manually allocated memory buffers. */ -namespace lodepng { -#ifdef LODEPNG_COMPILE_PNG -/* Wrapper around LodePNGState, which automatically calls lodepng_state_init in the constructor -and lodepng_state_cleanup in the desctructor.*/ -class State : public LodePNGState { - public: - State(); - State(const State& other); - ~State(); - State& operator=(const State& other); -}; - -#ifdef LODEPNG_COMPILE_DECODER -/* Same as other lodepng::decode, but using a State for more settings and information. */ -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - State& state, - const unsigned char* in, size_t insize); -unsigned decode(std::vector& out, unsigned& w, unsigned& h, - State& state, - const std::vector& in); -#endif /*LODEPNG_COMPILE_DECODER*/ - -#ifdef LODEPNG_COMPILE_ENCODER -/* Same as other lodepng::encode, but using a State for more settings and information. */ -unsigned encode(std::vector& out, - const unsigned char* in, unsigned w, unsigned h, - State& state); -unsigned encode(std::vector& out, - const std::vector& in, unsigned w, unsigned h, - State& state); -#endif /*LODEPNG_COMPILE_ENCODER*/ - -#ifdef LODEPNG_COMPILE_DISK -/* -Load a file from disk into an std::vector. -return value: error code (0 means ok) - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and decode in-memory -*/ -unsigned load_file(std::vector& buffer, const std::string& filename); - -/* -Save the binary data in an std::vector to a file on disk. The file is overwritten -without warning. - -NOTE: Wide-character filenames are not supported, you can use an external method -to handle such files and encode in-memory -*/ -unsigned save_file(const std::vector& buffer, const std::string& filename); -#endif /* LODEPNG_COMPILE_DISK */ -#endif /* LODEPNG_COMPILE_PNG */ - -#ifdef LODEPNG_COMPILE_ZLIB -#ifdef LODEPNG_COMPILE_DECODER -/* Zlib-decompress an unsigned char buffer */ -unsigned decompress(std::vector& out, const unsigned char* in, size_t insize, - const LodePNGDecompressSettings& settings = lodepng_default_decompress_settings); - -/* Zlib-decompress an std::vector */ -unsigned decompress(std::vector& out, const std::vector& in, - const LodePNGDecompressSettings& settings = lodepng_default_decompress_settings); -#endif /* LODEPNG_COMPILE_DECODER */ - -#ifdef LODEPNG_COMPILE_ENCODER -/* Zlib-compress an unsigned char buffer */ -unsigned compress(std::vector& out, const unsigned char* in, size_t insize, - const LodePNGCompressSettings& settings = lodepng_default_compress_settings); - -/* Zlib-compress an std::vector */ -unsigned compress(std::vector& out, const std::vector& in, - const LodePNGCompressSettings& settings = lodepng_default_compress_settings); -#endif /* LODEPNG_COMPILE_ENCODER */ -#endif /* LODEPNG_COMPILE_ZLIB */ -} /* namespace lodepng */ -#endif /*LODEPNG_COMPILE_CPP*/ - -/* -TODO: -[.] test if there are no memory leaks or security exploits - done a lot but needs to be checked often -[.] check compatibility with various compilers - done but needs to be redone for every newer version -[X] converting color to 16-bit per channel types -[X] support color profile chunk types (but never let them touch RGB values by default) -[ ] support all second edition public PNG chunk types (almost done except sPLT and hIST) -[X] support non-animation third edition public PNG chunk types: eXIf, cICP, mDCV, cLLI -[ ] make sure encoder generates no chunks with size > (2^31)-1 -[ ] partial decoding (stream processing) -[X] let the "isFullyOpaque" function check color keys and transparent palettes too -[X] better name for the variables "codes", "codesD", "codelengthcodes", "clcl" and "lldl" -[ ] allow treating some errors like warnings, when image is recoverable (e.g. 69, 57, 58) -[ ] make warnings like: oob palette, checksum fail, data after iend, wrong/unknown crit chunk, no null terminator in text, ... -[ ] error messages with line numbers (and version) -[ ] errors in state instead of as return code? -[ ] new errors/warnings like suspiciously big decompressed ztxt or iccp chunk -[ ] let the C++ wrapper catch exceptions coming from the standard library and return LodePNG error codes -[ ] allow user to provide custom color conversion functions, e.g. for premultiplied alpha, padding bits or not, ... -[ ] allow user to give data (void*) to custom allocator -[X] provide alternatives for C library functions not present on some platforms (memcpy, ...) -*/ - -#endif /*LODEPNG_H inclusion guard*/ - -/* -LodePNG Documentation ---------------------- - -0. table of contents --------------------- - - 1. about - 1.1. supported features - 1.2. features not supported - 2. C and C++ version - 3. security - 4. decoding - 5. encoding - 6. color conversions - 6.1. PNG color types - 6.2. color conversions - 6.3. padding bits - 6.4. A note about 16-bits per channel and endianness - 7. error values - 8. chunks and PNG editing - 9. compiler support - 10. examples - 10.1. decoder C++ example - 10.2. decoder C example - 11. state settings reference - 12. changes - 13. contact information - - -1. about --------- - -PNG is a file format to store raster images losslessly with good compression, -supporting different color types and alpha channel. - -LodePNG is a PNG codec according to the Portable Network Graphics (PNG) -Specification (Second Edition) - W3C Recommendation 10 November 2003. - -The specifications used are: - -*) Portable Network Graphics (PNG) Specification (Second Edition): - http://www.w3.org/TR/2003/REC-PNG-20031110 -*) RFC 1950 ZLIB Compressed Data Format version 3.3: - http://www.gzip.org/zlib/rfc-zlib.html -*) RFC 1951 DEFLATE Compressed Data Format Specification ver 1.3: - http://www.gzip.org/zlib/rfc-deflate.html - -The most recent version of LodePNG can currently be found at -http://lodev.org/lodepng/ - -LodePNG works both in C (ISO C90) and C++, with a C++ wrapper that adds -extra functionality. - -LodePNG exists out of two files: --lodepng.h: the header file for both C and C++ --lodepng.c(pp): give it the name lodepng.c or lodepng.cpp (or .cc) depending on your usage - -If you want to start using LodePNG right away without reading this doc, get the -examples from the LodePNG website to see how to use it in code, or check the -smaller examples in chapter 13 here. - -LodePNG is simple but only supports the basic requirements. To achieve -simplicity, the following design choices were made: There are no dependencies -on any external library. There are functions to decode and encode a PNG with -a single function call, and extended versions of these functions taking a -LodePNGState struct allowing to specify or get more information. By default -the colors of the raw image are always RGB or RGBA, no matter what color type -the PNG file uses. To read and write files, there are simple functions to -convert the files to/from buffers in memory. - -This all makes LodePNG suitable for loading textures in games, demos and small -programs, ... It's less suitable for full fledged image editors, loading PNGs -over network (it requires all the image data to be available before decoding can -begin), life-critical systems, ... - -1.1. supported features ------------------------ - -The following features are supported by the decoder: - -*) decoding of PNGs with any color type, bit depth and interlace mode, to a 24- or 32-bit color raw image, - or the same color type as the PNG -*) encoding of PNGs, from any raw image to 24- or 32-bit color, or the same color type as the raw image -*) Adam7 interlace and deinterlace for any color type -*) loading the image from harddisk or decoding it from a buffer from other sources than harddisk -*) support for alpha channels, including RGBA color model, translucent palettes and color keying -*) zlib decompression (inflate) -*) zlib compression (deflate) -*) CRC32 and ADLER32 checksums -*) colorimetric color profile conversions: currently experimentally available in lodepng_util.cpp only, - plus alternatively ability to pass on chroma/gamma/ICC profile information to other color management system. -*) handling of unknown chunks, allowing making a PNG editor that stores custom and unknown chunks. -*) the following chunks are supported by both encoder and decoder: - IHDR: header information - PLTE: color palette - IDAT: pixel data - IEND: the final chunk - tRNS: transparency for palettized images - tEXt: textual information - zTXt: compressed textual information - iTXt: international textual information - bKGD: suggested background color - pHYs: physical dimensions - tIME: modification time - cHRM: RGB chromaticities - gAMA: RGB gamma correction - iCCP: ICC color profile - sRGB: rendering intent - sBIT: significant bits - -1.2. features not supported ---------------------------- - -The following features are not (yet) supported: - -*) some features needed to make a conformant PNG-Editor might be still missing. -*) partial loading/stream processing. All data must be available and is processed in one call. -*) The hIST and sPLT public chunks are not (yet) supported but treated as unknown chunks - - -2. C and C++ version --------------------- - -The C version uses buffers allocated with alloc that you need to free() -yourself. You need to use init and cleanup functions for each struct whenever -using a struct from the C version to avoid exploits and memory leaks. - -The C++ version has extra functions with std::vectors in the interface and the -lodepng::State class which is a LodePNGState with constructor and destructor. - -These files work without modification for both C and C++ compilers because all -the additional C++ code is in "#ifdef __cplusplus" blocks that make C-compilers -ignore it, and the C code is made to compile both with strict ISO C90 and C++. - -To use the C++ version, you need to rename the source file to lodepng.cpp -(instead of lodepng.c), and compile it with a C++ compiler. - -To use the C version, you need to rename the source file to lodepng.c (instead -of lodepng.cpp), and compile it with a C compiler. - - -3. Security ------------ - -Even if carefully designed, it's always possible that LodePNG contains possible -exploits. If you discover one, please let me know, and it will be fixed. - -When using LodePNG, care has to be taken with the C version of LodePNG, as well -as the C-style structs when working with C++. The following conventions are used -for all C-style structs: - --if a struct has a corresponding init function, always call the init function when making a new one --if a struct has a corresponding cleanup function, call it before the struct disappears to avoid memory leaks --if a struct has a corresponding copy function, use the copy function instead of "=". - The destination must also be inited already. - - -4. Decoding ------------ - -Decoding converts a PNG compressed image to a raw pixel buffer. - -Most documentation on using the decoder is at its declarations in the header -above. For C, simple decoding can be done with functions such as -lodepng_decode32, and more advanced decoding can be done with the struct -LodePNGState and lodepng_decode. For C++, all decoding can be done with the -various lodepng::decode functions, and lodepng::State can be used for advanced -features. - -When using the LodePNGState, it uses the following fields for decoding: -*) LodePNGInfo info_png: it stores extra information about the PNG (the input) in here -*) LodePNGColorMode info_raw: here you can say what color mode of the raw image (the output) you want to get -*) LodePNGDecoderSettings decoder: you can specify a few extra settings for the decoder to use - -LodePNGInfo info_png --------------------- - -After decoding, this contains extra information of the PNG image, except the actual -pixels, width and height because these are already gotten directly from the decoder -functions. - -It contains for example the original color type of the PNG image, text comments, -suggested background color, etc... More details about the LodePNGInfo struct are -at its declaration documentation. - -LodePNGColorMode info_raw -------------------------- - -When decoding, here you can specify which color type you want -the resulting raw image to be. If this is different from the colortype of the -PNG, then the decoder will automatically convert the result. This conversion -always works, except if you want it to convert a color PNG to grayscale or to -a palette with missing colors. - -By default, 32-bit color is used for the result. - -LodePNGDecoderSettings decoder ------------------------------- - -The settings can be used to ignore the errors created by invalid CRC and Adler32 -chunks, and to disable the decoding of tEXt chunks. - -There's also a setting color_convert, true by default. If false, no conversion -is done, the resulting data will be as it was in the PNG (after decompression) -and you'll have to puzzle the colors of the pixels together yourself using the -color type information in the LodePNGInfo. - - -5. Encoding ------------ - -Encoding converts a raw pixel buffer to a PNG compressed image. - -Most documentation on using the encoder is at its declarations in the header -above. For C, simple encoding can be done with functions such as -lodepng_encode32, and more advanced decoding can be done with the struct -LodePNGState and lodepng_encode. For C++, all encoding can be done with the -various lodepng::encode functions, and lodepng::State can be used for advanced -features. - -Like the decoder, the encoder can also give errors. However it gives less errors -since the encoder input is trusted, the decoder input (a PNG image that could -be forged by anyone) is not trusted. - -When using the LodePNGState, it uses the following fields for encoding: -*) LodePNGInfo info_png: here you specify how you want the PNG (the output) to be. -*) LodePNGColorMode info_raw: here you say what color type of the raw image (the input) has -*) LodePNGEncoderSettings encoder: you can specify a few settings for the encoder to use - -LodePNGInfo info_png --------------------- - -When encoding, you use this the opposite way as when decoding: for encoding, -you fill in the values you want the PNG to have before encoding. By default it's -not needed to specify a color type for the PNG since it's automatically chosen, -but it's possible to choose it yourself given the right settings. - -The encoder will not always exactly match the LodePNGInfo struct you give, -it tries as close as possible. Some things are ignored by the encoder. The -encoder uses, for example, the following settings from it when applicable: -colortype and bitdepth, text chunks, time chunk, the color key, the palette, the -background color, the interlace method, unknown chunks, ... - -When encoding to a PNG with colortype 3, the encoder will generate a PLTE chunk. -If the palette contains any colors for which the alpha channel is not 255 (so -there are translucent colors in the palette), it'll add a tRNS chunk. - -LodePNGColorMode info_raw -------------------------- - -You specify the color type of the raw image that you give to the input here, -including a possible transparent color key and palette you happen to be using in -your raw image data. - -By default, 32-bit color is assumed, meaning your input has to be in RGBA -format with 4 bytes (unsigned chars) per pixel. - -LodePNGEncoderSettings encoder ------------------------------- - -The following settings are supported (some are in sub-structs): -*) auto_convert: when this option is enabled, the encoder will -automatically choose the smallest possible color mode (including color key) that -can encode the colors of all pixels without information loss. -*) btype: the block type for LZ77. 0 = uncompressed, 1 = fixed huffman tree, - 2 = dynamic huffman tree (best compression). Should be 2 for proper - compression. -*) use_lz77: whether or not to use LZ77 for compressed block types. Should be - true for proper compression. -*) windowsize: the window size used by the LZ77 encoder (1 - 32768). Has value - 2048 by default, but can be set to 32768 for better, but slow, compression. -*) force_palette: if colortype is 2 or 6, you can make the encoder write a PLTE - chunk if force_palette is true. This can used as suggested palette to convert - to by viewers that don't support more than 256 colors (if those still exist) -*) add_id: add text chunk "Encoder: LodePNG " to the image. -*) text_compression: default 1. If 1, it'll store texts as zTXt instead of tEXt chunks. - zTXt chunks use zlib compression on the text. This gives a smaller result on - large texts but a larger result on small texts (such as a single program name). - It's all tEXt or all zTXt though, there's no separate setting per text yet. - - -6. color conversions --------------------- - -An important thing to note about LodePNG, is that the color type of the PNG, and -the color type of the raw image, are completely independent. By default, when -you decode a PNG, you get the result as a raw image in the color type you want, -no matter whether the PNG was encoded with a palette, grayscale or RGBA color. -And if you encode an image, by default LodePNG will automatically choose the PNG -color type that gives good compression based on the values of colors and amount -of colors in the image. It can be configured to let you control it instead as -well, though. - -To be able to do this, LodePNG does conversions from one color mode to another. -It can convert from almost any color type to any other color type, except the -following conversions: RGB to grayscale is not supported, and converting to a -palette when the palette doesn't have a required color is not supported. This is -not supported on purpose: this is information loss which requires a color -reduction algorithm that is beyond the scope of a PNG encoder (yes, RGB to gray -is easy, but there are multiple ways if you want to give some channels more -weight). - -By default, when decoding, you get the raw image in 32-bit RGBA or 24-bit RGB -color, no matter what color type the PNG has. And by default when encoding, -LodePNG automatically picks the best color model for the output PNG, and expects -the input image to be 32-bit RGBA or 24-bit RGB. So, unless you want to control -the color format of the images yourself, you can skip this chapter. - -6.1. PNG color types --------------------- - -A PNG image can have many color types, ranging from 1-bit color to 64-bit color, -as well as palettized color modes. After the zlib decompression and unfiltering -in the PNG image is done, the raw pixel data will have that color type and thus -a certain amount of bits per pixel. If you want the output raw image after -decoding to have another color type, a conversion is done by LodePNG. - -The PNG specification gives the following color types: - -0: grayscale, bit depths 1, 2, 4, 8, 16 -2: RGB, bit depths 8 and 16 -3: palette, bit depths 1, 2, 4 and 8 -4: grayscale with alpha, bit depths 8 and 16 -6: RGBA, bit depths 8 and 16 - -Bit depth is the amount of bits per pixel per color channel. So the total amount -of bits per pixel is: amount of channels * bitdepth. - -6.2. color conversions ----------------------- - -As explained in the sections about the encoder and decoder, you can specify -color types and bit depths in info_png and info_raw to change the default -behaviour. - -If, when decoding, you want the raw image to be something else than the default, -you need to set the color type and bit depth you want in the LodePNGColorMode, -or the parameters colortype and bitdepth of the simple decoding function. - -If, when encoding, you use another color type than the default in the raw input -image, you need to specify its color type and bit depth in the LodePNGColorMode -of the raw image, or use the parameters colortype and bitdepth of the simple -encoding function. - -If, when encoding, you don't want LodePNG to choose the output PNG color type -but control it yourself, you need to set auto_convert in the encoder settings -to false, and specify the color type you want in the LodePNGInfo of the -encoder (including palette: it can generate a palette if auto_convert is true, -otherwise not). - -If the input and output color type differ (whether user chosen or auto chosen), -LodePNG will do a color conversion, which follows the rules below, and may -sometimes result in an error. - -To avoid some confusion: --the decoder converts from PNG to raw image --the encoder converts from raw image to PNG --the colortype and bitdepth in LodePNGColorMode info_raw, are those of the raw image --the colortype and bitdepth in the color field of LodePNGInfo info_png, are those of the PNG --when encoding, the color type in LodePNGInfo is ignored if auto_convert - is enabled, it is automatically generated instead --when decoding, the color type in LodePNGInfo is set by the decoder to that of the original - PNG image, but it can be ignored since the raw image has the color type you requested instead --if the color type of the LodePNGColorMode and PNG image aren't the same, a conversion - between the color types is done if the color types are supported. If it is not - supported, an error is returned. If the types are the same, no conversion is done. --even though some conversions aren't supported, LodePNG supports loading PNGs from any - colortype and saving PNGs to any colortype, sometimes it just requires preparing - the raw image correctly before encoding. --both encoder and decoder use the same color converter. - -The function lodepng_convert does the color conversion. It is available in the -interface but normally isn't needed since the encoder and decoder already call -it. - -Non supported color conversions: --color to grayscale when non-gray pixels are present: no error is thrown, but -the result will look ugly because only the red channel is taken (it assumes all -three channels are the same in this case so ignores green and blue). The reason -no error is given is to allow converting from three-channel grayscale images to -one-channel even if there are numerical imprecisions. --anything to palette when the palette does not have an exact match for a from-color -in it: in this case an error is thrown - -Supported color conversions: --anything to 8-bit RGB, 8-bit RGBA, 16-bit RGB, 16-bit RGBA --any gray or gray+alpha, to gray or gray+alpha --anything to a palette, as long as the palette has the requested colors in it --removing alpha channel --higher to smaller bitdepth, and vice versa - -If you want no color conversion to be done (e.g. for speed or control): --In the encoder, you can make it save a PNG with any color type by giving the -raw color mode and LodePNGInfo the same color mode, and setting auto_convert to -false. --In the decoder, you can make it store the pixel data in the same color type -as the PNG has, by setting the color_convert setting to false. Settings in -info_raw are then ignored. - -6.3. padding bits ------------------ - -In the PNG file format, if a less than 8-bit per pixel color type is used and the scanlines -have a bit amount that isn't a multiple of 8, then padding bits are used so that each -scanline starts at a fresh byte. But that is NOT true for the LodePNG raw input and output. -The raw input image you give to the encoder, and the raw output image you get from the decoder -will NOT have these padding bits, e.g. in the case of a 1-bit image with a width -of 7 pixels, the first pixel of the second scanline will the 8th bit of the first byte, -not the first bit of a new byte. - -6.4. A note about 16-bits per channel and endianness ----------------------------------------------------- - -LodePNG uses unsigned char arrays for 16-bit per channel colors too, just like -for any other color format. The 16-bit values are stored in big endian (most -significant byte first) in these arrays. This is the opposite order of the -little endian used by x86 CPU's. - -LodePNG always uses big endian because the PNG file format does so internally. -Conversions to other formats than PNG uses internally are not supported by -LodePNG on purpose, there are myriads of formats, including endianness of 16-bit -colors, the order in which you store R, G, B and A, and so on. Supporting and -converting to/from all that is outside the scope of LodePNG. - -This may mean that, depending on your use case, you may want to convert the big -endian output of LodePNG to little endian with a for loop. This is certainly not -always needed, many applications and libraries support big endian 16-bit colors -anyway, but it means you cannot simply cast the unsigned char* buffer to an -unsigned short* buffer on x86 CPUs. - - -7. error values ---------------- - -All functions in LodePNG that return an error code, return 0 if everything went -OK, or a non-zero code if there was an error. - -The meaning of the LodePNG error values can be retrieved with the function -lodepng_error_text: given the numerical error code, it returns a description -of the error in English as a string. - -Check the implementation of lodepng_error_text to see the meaning of each code. - -It is not recommended to use the numerical values to programmatically make -different decisions based on error types as the numbers are not guaranteed to -stay backwards compatible. They are for human consumption only. Programmatically -only 0 or non-0 matter. - - -8. chunks and PNG editing -------------------------- - -If you want to add extra chunks to a PNG you encode, or use LodePNG for a PNG -editor that should follow the rules about handling of unknown chunks, or if your -program is able to read other types of chunks than the ones handled by LodePNG, -then that's possible with the chunk functions of LodePNG. - -A PNG chunk has the following layout: - -4 bytes length -4 bytes type name -length bytes data -4 bytes CRC - -8.1. iterating through chunks ------------------------------ - -If you have a buffer containing the PNG image data, then the first chunk (the -IHDR chunk) starts at byte number 8 of that buffer. The first 8 bytes are the -signature of the PNG and are not part of a chunk. But if you start at byte 8 -then you have a chunk, and can check the following things of it. - -NOTE: none of these functions check for memory buffer boundaries. To avoid -exploits, always make sure the buffer contains all the data of the chunks. -When using lodepng_chunk_next, make sure the returned value is within the -allocated memory. - -unsigned lodepng_chunk_length(const unsigned char* chunk): - -Get the length of the chunk's data. The total chunk length is this length + 12. - -void lodepng_chunk_type(char type[5], const unsigned char* chunk): -unsigned char lodepng_chunk_type_equals(const unsigned char* chunk, const char* type): - -Get the type of the chunk or compare if it's a certain type - -unsigned char lodepng_chunk_critical(const unsigned char* chunk): -unsigned char lodepng_chunk_private(const unsigned char* chunk): -unsigned char lodepng_chunk_safetocopy(const unsigned char* chunk): - -Check if the chunk is critical in the PNG standard (only IHDR, PLTE, IDAT and IEND are). -Check if the chunk is private (public chunks are part of the standard, private ones not). -Check if the chunk is safe to copy. If it's not, then, when modifying data in a critical -chunk, unsafe to copy chunks of the old image may NOT be saved in the new one if your -program doesn't handle that type of unknown chunk. - -unsigned char* lodepng_chunk_data(unsigned char* chunk): -const unsigned char* lodepng_chunk_data_const(const unsigned char* chunk): - -Get a pointer to the start of the data of the chunk. - -unsigned lodepng_chunk_check_crc(const unsigned char* chunk): -void lodepng_chunk_generate_crc(unsigned char* chunk): - -Check if the crc is correct or generate a correct one. - -unsigned char* lodepng_chunk_next(unsigned char* chunk): -const unsigned char* lodepng_chunk_next_const(const unsigned char* chunk): - -Iterate to the next chunk. This works if you have a buffer with consecutive chunks. Note that these -functions do no boundary checking of the allocated data whatsoever, so make sure there is enough -data available in the buffer to be able to go to the next chunk. - -unsigned lodepng_chunk_append(unsigned char** out, size_t* outsize, const unsigned char* chunk): -unsigned lodepng_chunk_create(unsigned char** out, size_t* outsize, unsigned length, - const char* type, const unsigned char* data): - -These functions are used to create new chunks that are appended to the data in *out that has -length *outsize. The append function appends an existing chunk to the new data. The create -function creates a new chunk with the given parameters and appends it. Type is the 4-letter -name of the chunk. - -8.2. chunks in info_png ------------------------ - -The LodePNGInfo struct contains fields with the unknown chunk in it. It has 3 -buffers (each with size) to contain 3 types of unknown chunks: -the ones that come before the PLTE chunk, the ones that come between the PLTE -and the IDAT chunks, and the ones that come after the IDAT chunks. -It's necessary to make the distinction between these 3 cases because the PNG -standard forces to keep the ordering of unknown chunks compared to the critical -chunks, but does not force any other ordering rules. - -info_png.unknown_chunks_data[0] is the chunks before PLTE -info_png.unknown_chunks_data[1] is the chunks after PLTE, before IDAT -info_png.unknown_chunks_data[2] is the chunks after IDAT - -The chunks in these 3 buffers can be iterated through and read by using the same -way described in the previous subchapter. - -When using the decoder to decode a PNG, you can make it store all unknown chunks -if you set the option settings.remember_unknown_chunks to 1. By default, this -option is off (0). - -The encoder will always encode unknown chunks that are stored in the info_png. -If you need it to add a particular chunk that isn't known by LodePNG, you can -use lodepng_chunk_append or lodepng_chunk_create to the chunk data in -info_png.unknown_chunks_data[x]. - -Chunks that are known by LodePNG should not be added in that way. E.g. to make -LodePNG add a bKGD chunk, set background_defined to true and add the correct -parameters there instead. - - -9. compiler support -------------------- - -No libraries other than the current standard C library are needed to compile -LodePNG. For the C++ version, only the standard C++ library is needed on top. -Add the files lodepng.c(pp) and lodepng.h to your project, include -lodepng.h where needed, and your program can read/write PNG files. - -It is compatible with C90 and up, and C++03 and up. - -If performance is important, use optimization when compiling! For both the -encoder and decoder, this makes a large difference. - -Make sure that LodePNG is compiled with the same compiler of the same version -and with the same settings as the rest of the program, or the interfaces with -std::vectors and std::strings in C++ can be incompatible. - -CHAR_BITS must be 8 or higher, because LodePNG uses unsigned chars for octets. - -*) gcc and g++ - -LodePNG is developed in gcc so this compiler is natively supported. It gives no -warnings with compiler options "-Wall -Wextra -pedantic -ansi", with gcc and g++ -version 4.7.1 on Linux, 32-bit and 64-bit. - -*) Clang - -Fully supported and warning-free. - -*) Mingw - -The Mingw compiler (a port of gcc for Windows) should be fully supported by -LodePNG. - -*) Visual Studio and Visual C++ Express Edition - -LodePNG should be warning-free with warning level W4. Two warnings were disabled -with pragmas though: warning 4244 about implicit conversions, and warning 4996 -where it wants to use a non-standard function fopen_s instead of the standard C -fopen. - -Visual Studio may want "stdafx.h" files to be included in each source file and -give an error "unexpected end of file while looking for precompiled header". -This is not standard C++ and will not be added to the stock LodePNG. You can -disable it for lodepng.cpp only by right clicking it, Properties, C/C++, -Precompiled Headers, and set it to Not Using Precompiled Headers there. - -NOTE: Modern versions of VS should be fully supported, but old versions, e.g. -VS6, are not guaranteed to work. - -*) Compilers on Macintosh - -LodePNG has been reported to work both with gcc and LLVM for Macintosh, both for -C and C++. - -*) Other Compilers - -If you encounter problems on any compilers, feel free to let me know and I may -try to fix it if the compiler is modern and standards compliant. - - -10. examples ------------- - -This decoder example shows the most basic usage of LodePNG. More complex -examples can be found on the LodePNG website. - -NOTE: these examples do not support wide-character filenames, you can use an -external method to handle such files and encode or decode in-memory - -10.1. decoder C++ example -------------------------- - -#include "lodepng.h" -#include - -int main(int argc, char *argv[]) { - const char* filename = argc > 1 ? argv[1] : "test.png"; - - //load and decode - std::vector image; - unsigned width, height; - unsigned error = lodepng::decode(image, width, height, filename); - - //if there's an error, display it - if(error) std::cout << "decoder error " << error << ": " << lodepng_error_text(error) << std::endl; - - //the pixels are now in the vector "image", 4 bytes per pixel, ordered RGBARGBA..., use it as texture, draw it, ... -} - -10.2. decoder C example ------------------------ - -#include "lodepng.h" - -int main(int argc, char *argv[]) { - unsigned error; - unsigned char* image; - size_t width, height; - const char* filename = argc > 1 ? argv[1] : "test.png"; - - error = lodepng_decode32_file(&image, &width, &height, filename); - - if(error) printf("decoder error %u: %s\n", error, lodepng_error_text(error)); - - / * use image here * / - - free(image); - return 0; -} - -11. state settings reference ----------------------------- - -A quick reference of some settings to set on the LodePNGState - -For decoding: - -state.decoder.zlibsettings.ignore_adler32: ignore ADLER32 checksums -state.decoder.zlibsettings.custom_...: use custom inflate function -state.decoder.ignore_crc: ignore CRC checksums -state.decoder.ignore_critical: ignore unknown critical chunks -state.decoder.ignore_end: ignore missing IEND chunk. May fail if this corruption causes other errors -state.decoder.color_convert: convert internal PNG color to chosen one -state.decoder.read_text_chunks: whether to read in text metadata chunks -state.decoder.remember_unknown_chunks: whether to read in unknown chunks -state.info_raw.colortype: desired color type for decoded image -state.info_raw.bitdepth: desired bit depth for decoded image -state.info_raw....: more color settings, see struct LodePNGColorMode -state.info_png....: no settings for decoder but output, see struct LodePNGInfo - -For encoding: - -state.encoder.zlibsettings.btype: disable compression by setting it to 0 -state.encoder.zlibsettings.use_lz77: use LZ77 in compression -state.encoder.zlibsettings.windowsize: tweak LZ77 windowsize -state.encoder.zlibsettings.minmatch: tweak min LZ77 length to match -state.encoder.zlibsettings.nicematch: tweak LZ77 match where to stop searching -state.encoder.zlibsettings.lazymatching: try one more LZ77 matching -state.encoder.zlibsettings.custom_...: use custom deflate function -state.encoder.auto_convert: choose optimal PNG color type, if 0 uses info_png -state.encoder.filter_palette_zero: PNG filter strategy for palette -state.encoder.filter_strategy: PNG filter strategy to encode with -state.encoder.force_palette: add palette even if not encoding to one -state.encoder.add_id: add LodePNG identifier and version as a text chunk -state.encoder.text_compression: use compressed text chunks for metadata -state.info_raw.colortype: color type of raw input image you provide -state.info_raw.bitdepth: bit depth of raw input image you provide -state.info_raw: more color settings, see struct LodePNGColorMode -state.info_png.color.colortype: desired color type if auto_convert is false -state.info_png.color.bitdepth: desired bit depth if auto_convert is false -state.info_png.color....: more color settings, see struct LodePNGColorMode -state.info_png....: more PNG related settings, see struct LodePNGInfo - - -12. changes ------------ - -The version number of LodePNG is the date of the change given in the format -yyyymmdd. - -Some changes aren't backwards compatible. Those are indicated with a (!) -symbol. - -Not all changes are listed here, the commit history in github lists more: -https://github.com/lvandeve/lodepng - -*) 6 may 2025 (!): renamed mDCv to mDCV and cLLi to cLLI as per the recent - rename in the draft png third edition spec. Please note that as long as the - third edition is not finalized, backwards-incompatible changes to its - features are possible. -*) 23 dec 2024: added support for the mDCv and cLLi chunks (for png third - edition spec) -*) 22 dec 2024: added support for the cICP chunk (for png third edition spec) -*) 15 dec 2024: added support for the eXIf chunk (for png third edition spec) -*) 10 apr 2023: faster CRC32 implementation, but with larger lookup table. -*) 13 jun 2022: added support for the sBIT chunk. -*) 09 jan 2022: minor decoder speed improvements. -*) 27 jun 2021: added warnings that file reading/writing functions don't support - wide-character filenames (support for this is not planned, opening files is - not the core part of PNG decoding/decoding and is platform dependent). -*) 17 oct 2020: prevent decoding too large text/icc chunks by default. -*) 06 mar 2020: simplified some of the dynamic memory allocations. -*) 12 jan 2020: (!) added 'end' argument to lodepng_chunk_next to allow correct - overflow checks. -*) 14 aug 2019: around 25% faster decoding thanks to huffman lookup tables. -*) 15 jun 2019: (!) auto_choose_color API changed (for bugfix: don't use palette - if gray ICC profile) and non-ICC LodePNGColorProfile renamed to - LodePNGColorStats. -*) 30 dec 2018: code style changes only: removed newlines before opening braces. -*) 10 sep 2018: added way to inspect metadata chunks without full decoding. -*) 19 aug 2018: (!) fixed color mode bKGD is encoded with and made it use - palette index in case of palette. -*) 10 aug 2018: (!) added support for gAMA, cHRM, sRGB and iCCP chunks. This - change is backwards compatible unless you relied on unknown_chunks for those. -*) 11 jun 2018: less restrictive check for pixel size integer overflow -*) 14 jan 2018: allow optionally ignoring a few more recoverable errors -*) 17 sep 2017: fix memory leak for some encoder input error cases -*) 27 nov 2016: grey+alpha auto color model detection bugfix -*) 18 apr 2016: Changed qsort to custom stable sort (for platforms w/o qsort). -*) 09 apr 2016: Fixed colorkey usage detection, and better file loading (within - the limits of pure C90). -*) 08 dec 2015: Made load_file function return error if file can't be opened. -*) 24 oct 2015: Bugfix with decoding to palette output. -*) 18 apr 2015: Boundary PM instead of just package-merge for faster encoding. -*) 24 aug 2014: Moved to github -*) 23 aug 2014: Reduced needless memory usage of decoder. -*) 28 jun 2014: Removed fix_png setting, always support palette OOB for - simplicity. Made ColorProfile public. -*) 09 jun 2014: Faster encoder by fixing hash bug and more zeros optimization. -*) 22 dec 2013: Power of two windowsize required for optimization. -*) 15 apr 2013: Fixed bug with LAC_ALPHA and color key. -*) 25 mar 2013: Added an optional feature to ignore some PNG errors (fix_png). -*) 11 mar 2013: (!) Bugfix with custom free. Changed from "my" to "lodepng_" - prefix for the custom allocators and made it possible with a new #define to - use custom ones in your project without needing to change lodepng's code. -*) 28 jan 2013: Bugfix with color key. -*) 27 oct 2012: Tweaks in text chunk keyword length error handling. -*) 8 oct 2012: (!) Added new filter strategy (entropy) and new auto color mode. - (no palette). Better deflate tree encoding. New compression tweak settings. - Faster color conversions while decoding. Some internal cleanups. -*) 23 sep 2012: Reduced warnings in Visual Studio a little bit. -*) 1 sep 2012: (!) Removed #define's for giving custom (de)compression functions - and made it work with function pointers instead. -*) 23 jun 2012: Added more filter strategies. Made it easier to use custom alloc - and free functions and toggle #defines from compiler flags. Small fixes. -*) 6 may 2012: (!) Made plugging in custom zlib/deflate functions more flexible. -*) 22 apr 2012: (!) Made interface more consistent, renaming a lot. Removed - redundant C++ codec classes. Reduced amount of structs. Everything changed, - but it is cleaner now imho and functionality remains the same. Also fixed - several bugs and shrunk the implementation code. Made new samples. -*) 6 nov 2011: (!) By default, the encoder now automatically chooses the best - PNG color model and bit depth, based on the amount and type of colors of the - raw image. For this, autoLeaveOutAlphaChannel replaced by auto_choose_color. -*) 9 oct 2011: simpler hash chain implementation for the encoder. -*) 8 sep 2011: lz77 encoder lazy matching instead of greedy matching. -*) 23 aug 2011: tweaked the zlib compression parameters after benchmarking. - A bug with the PNG filtertype heuristic was fixed, so that it chooses much - better ones (it's quite significant). A setting to do an experimental, slow, - brute force search for PNG filter types is added. -*) 17 aug 2011: (!) changed some C zlib related function names. -*) 16 aug 2011: made the code less wide (max 120 characters per line). -*) 17 apr 2011: code cleanup. Bugfixes. Convert low to 16-bit per sample colors. -*) 21 feb 2011: fixed compiling for C90. Fixed compiling with sections disabled. -*) 11 dec 2010: encoding is made faster, based on suggestion by Peter Eastman - to optimize long sequences of zeros. -*) 13 nov 2010: added LodePNG_InfoColor_hasPaletteAlpha and - LodePNG_InfoColor_canHaveAlpha functions for convenience. -*) 7 nov 2010: added LodePNG_error_text function to get error code description. -*) 30 oct 2010: made decoding slightly faster -*) 26 oct 2010: (!) changed some C function and struct names (more consistent). - Reorganized the documentation and the declaration order in the header. -*) 08 aug 2010: only changed some comments and external samples. -*) 05 jul 2010: fixed bug thanks to warnings in the new gcc version. -*) 14 mar 2010: fixed bug where too much memory was allocated for char buffers. -*) 02 sep 2008: fixed bug where it could create empty tree that linux apps could - read by ignoring the problem but windows apps couldn't. -*) 06 jun 2008: added more error checks for out of memory cases. -*) 26 apr 2008: added a few more checks here and there to ensure more safety. -*) 06 mar 2008: crash with encoding of strings fixed -*) 02 feb 2008: support for international text chunks added (iTXt) -*) 23 jan 2008: small cleanups, and #defines to divide code in sections -*) 20 jan 2008: support for unknown chunks allowing using LodePNG for an editor. -*) 18 jan 2008: support for tIME and pHYs chunks added to encoder and decoder. -*) 17 jan 2008: ability to encode and decode compressed zTXt chunks added - Also various fixes, such as in the deflate and the padding bits code. -*) 13 jan 2008: Added ability to encode Adam7-interlaced images. Improved - filtering code of encoder. -*) 07 jan 2008: (!) changed LodePNG to use ISO C90 instead of C++. A - C++ wrapper around this provides an interface almost identical to before. - Having LodePNG be pure ISO C90 makes it more portable. The C and C++ code - are together in these files but it works both for C and C++ compilers. -*) 29 dec 2007: (!) changed most integer types to unsigned int + other tweaks -*) 30 aug 2007: bug fixed which makes this Borland C++ compatible -*) 09 aug 2007: some VS2005 warnings removed again -*) 21 jul 2007: deflate code placed in new namespace separate from zlib code -*) 08 jun 2007: fixed bug with 2- and 4-bit color, and small interlaced images -*) 04 jun 2007: improved support for Visual Studio 2005: crash with accessing - invalid std::vector element [0] fixed, and level 3 and 4 warnings removed -*) 02 jun 2007: made the encoder add a tag with version by default -*) 27 may 2007: zlib and png code separated (but still in the same file), - simple encoder/decoder functions added for more simple usage cases -*) 19 may 2007: minor fixes, some code cleaning, new error added (error 69), - moved some examples from here to lodepng_examples.cpp -*) 12 may 2007: palette decoding bug fixed -*) 24 apr 2007: changed the license from BSD to the zlib license -*) 11 mar 2007: very simple addition: ability to encode bKGD chunks. -*) 04 mar 2007: (!) tEXt chunk related fixes, and support for encoding - palettized PNG images. Plus little interface change with palette and texts. -*) 03 mar 2007: Made it encode dynamic Huffman shorter with repeat codes. - Fixed a bug where the end code of a block had length 0 in the Huffman tree. -*) 26 feb 2007: Huffman compression with dynamic trees (BTYPE 2) now implemented - and supported by the encoder, resulting in smaller PNGs at the output. -*) 27 jan 2007: Made the Adler-32 test faster so that a timewaste is gone. -*) 24 jan 2007: gave encoder an error interface. Added color conversion from any - greyscale type to 8-bit greyscale with or without alpha. -*) 21 jan 2007: (!) Totally changed the interface. It allows more color types - to convert to and is more uniform. See the manual for how it works now. -*) 07 jan 2007: Some cleanup & fixes, and a few changes over the last days: - encode/decode custom tEXt chunks, separate classes for zlib & deflate, and - at last made the decoder give errors for incorrect Adler32 or Crc. -*) 01 jan 2007: Fixed bug with encoding PNGs with less than 8 bits per channel. -*) 29 dec 2006: Added support for encoding images without alpha channel, and - cleaned out code as well as making certain parts faster. -*) 28 dec 2006: Added "Settings" to the encoder. -*) 26 dec 2006: The encoder now does LZ77 encoding and produces much smaller files now. - Removed some code duplication in the decoder. Fixed little bug in an example. -*) 09 dec 2006: (!) Placed output parameters of public functions as first parameter. - Fixed a bug of the decoder with 16-bit per color. -*) 15 oct 2006: Changed documentation structure -*) 09 oct 2006: Encoder class added. It encodes a valid PNG image from the - given image buffer, however for now it's not compressed. -*) 08 sep 2006: (!) Changed to interface with a Decoder class -*) 30 jul 2006: (!) LodePNG_InfoPng , width and height are now retrieved in different - way. Renamed decodePNG to decodePNGGeneric. -*) 29 jul 2006: (!) Changed the interface: image info is now returned as a - struct of type LodePNG::LodePNG_Info, instead of a vector, which was a bit clumsy. -*) 28 jul 2006: Cleaned the code and added new error checks. - Corrected terminology "deflate" into "inflate". -*) 23 jun 2006: Added SDL example in the documentation in the header, this - example allows easy debugging by displaying the PNG and its transparency. -*) 22 jun 2006: (!) Changed way to obtain error value. Added - loadFile function for convenience. Made decodePNG32 faster. -*) 21 jun 2006: (!) Changed type of info vector to unsigned. - Changed position of palette in info vector. Fixed an important bug that - happened on PNGs with an uncompressed block. -*) 16 jun 2006: Internally changed unsigned into unsigned where - needed, and performed some optimizations. -*) 07 jun 2006: (!) Renamed functions to decodePNG and placed them - in LodePNG namespace. Changed the order of the parameters. Rewrote the - documentation in the header. Renamed files to lodepng.cpp and lodepng.h -*) 22 apr 2006: Optimized and improved some code -*) 07 sep 2005: (!) Changed to std::vector interface -*) 12 aug 2005: Initial release (C++, decoder only) -*/ From 34d0b06a5f1b51a0895da79529511e1e381d6da9 Mon Sep 17 00:00:00 2001 From: mrciffa Date: Wed, 23 Sep 2026 02:05:17 +0200 Subject: [PATCH 123/123] docs(vision): quick start from the published Lucebox files, LUCE_MMPROJ for Docker Download and launch commands for Qwen3.8-27B on one GPU and DeepSeek V4 Flash Vision on a Strix Halo, both pointing at the files on the Lucebox Hugging Face repos, plus a curl example that sends an image. The Docker entrypoint maps LUCE_MMPROJ to --mmproj. The README links the guide. Co-Authored-By: Claude Opus 5.5 (1M context) --- README.md | 1 + docs/ds4v-mmproj.md | 4 +- docs/image-input.md | 87 +++++++++++++++++++++++++++++++----- server/docs/ENVIRONMENT.md | 2 + server/scripts/entrypoint.sh | 1 + 5 files changed, 83 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 095385012..e99f4682a 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,7 @@ See [Continuous batching in Lucebox](https://www.lucebox.com/blog/continuous-bat | OpenAI Chat Completions, Responses, and Anthropic Messages | [API reference](server/docs/API.md) | | CUDA, HIP, and mixed-device placement | [Mixed-backend guide](server/docs/MIXED_BACKEND.md) | | DeepSeek V4 single-device and heterogeneous profiles | [DeepSeek V4 guide](server/docs/DS4.md) | +| Image input (Qwen3.8, DeepSeek V4 Flash Vision) | [Image input guide](docs/image-input.md) | | Environment variables | [Environment reference](server/docs/ENVIRONMENT.md) | | Server internals | [Architecture](server/docs/ARCHITECTURE.md) | | Client integration and qualification | [Harness guide](harness/README.md) | diff --git a/docs/ds4v-mmproj.md b/docs/ds4v-mmproj.md index 180c2a67f..6d989de55 100644 --- a/docs/ds4v-mmproj.md +++ b/docs/ds4v-mmproj.md @@ -2,7 +2,9 @@ `server/tools/export_ds4v_mmproj.py` extracts the vision tower and aligner from the verified DeepSeek-V4 parent model without loading or changing tensor values. -It requires only Python's standard library. +It requires only Python's standard library. The output is published as +[`DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf`](https://huggingface.co/Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3); +run the exporter only to rebuild it. ```bash python3 server/tools/export_ds4v_mmproj.py \ diff --git a/docs/image-input.md b/docs/image-input.md index 9eb31d3c5..d4a264b9d 100644 --- a/docs/image-input.md +++ b/docs/image-input.md @@ -4,15 +4,84 @@ The server accepts JPEG and PNG images through OpenAI chat completions when a model is started with its vision projector, `--mmproj `. Without `--mmproj` nothing in the text serving path changes. -| Model | Projector file | Runs on | -| --- | --- | --- | -| Qwen3.5 / Qwen3.8 dense | the `mmproj-*.gguf` published next to the model (llama.cpp `clip` format, type `qwen3vl_merger`) | one GPU, any backend | -| DeepSeek V4 Flash Vision (DS4V) | [exported with our tool](ds4v-mmproj.md) | HIP: one GPU, or two GPUs splitting the experts | +| Model | Decoder | Projector | Runs on | +| --- | --- | --- | --- | +| Qwen3.8-27B | [`Qwen3.8-27B-IQ4_XS-pure.gguf`](https://huggingface.co/Lucebox/Qwen3.8-27B-IQ4_XS-fast-GGUF) (any Qwen3.5 / Qwen3.8 dense GGUF works) | [`Qwen3.8-27B-mmproj-Q8_0.gguf`](https://huggingface.co/Lucebox/Qwen3.8-27B-IQ4_XS-fast-GGUF), or any published `qwen3vl_merger` mmproj | one GPU, any backend | +| DeepSeek V4 Flash Vision (DS4V) | [`DeepSeek-V4-Flash-Vision-Exp-ROCMFPX-MIX-STRIX.gguf`](https://huggingface.co/Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3) | [`DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf`](https://huggingface.co/Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3) | HIP: a Strix Halo alone, or R9700 + Strix Halo | **Status: experimental.** Both models answer image questions correctly end to end; see each model's verification notes for what has and has not been measured. +## Quick start + +Build the server as in the [README](../README.md#run-the-server). DS4V also +needs hipBLASLt at build time (`hipblaslt-dev` on ROCm; CMake prints +`hipBLASLt found: building the DS4V vision ops`). + +### Qwen3.8-27B on one GPU (R9700) + +```bash +hf download Lucebox/Qwen3.8-27B-IQ4_XS-fast-GGUF \ + Qwen3.8-27B-IQ4_XS-pure.gguf Qwen3.8-27B-mmproj-Q8_0.gguf --local-dir models +hf download Lucebox/Qwen3.8-27B-DFlash2-GGUF \ + Qwen3.8-27B-DFlash2-Q8_0.gguf --local-dir models + +./server/build-hip/luce_server models/Qwen3.8-27B-IQ4_XS-pure.gguf \ + --target-device hip:0 \ + --draft models/Qwen3.8-27B-DFlash2-Q8_0.gguf --draft-device hip:0 \ + --draft-block-size 16 --max-ctx 32768 \ + --cache-type-k q8_0 --cache-type-v q8_0 \ + --mmproj models/Qwen3.8-27B-mmproj-Q8_0.gguf \ + --port 8216 +``` + +About 21 GiB of VRAM at the peak of an image request. Text requests keep the +DFlash2 drafter; image requests decode without it. + +### DeepSeek V4 Flash Vision on a Strix Halo + +```bash +hf download Lucebox/DeepSeek-V4-Flash-0731-ROCmFP3 \ + DeepSeek-V4-Flash-Vision-Exp-ROCMFPX-MIX-STRIX.gguf \ + DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf --local-dir models +hf download Lucebox/DeepSeek-V4-Flash-0731-DSpark-GGUF \ + DeepSeek-V4-Flash-0731-DSpark-draft-Q4RMFP4-denseF16.gguf --local-dir models + +LUCE_DS4_SPEC=1 \ +LUCE_DS4_DRAFT=models/DeepSeek-V4-Flash-0731-DSpark-draft-Q4RMFP4-denseF16.gguf \ +LUCE_DS4_SPARSE_DECODE_FLASH=1 \ +./server/build-hip/luce_server models/DeepSeek-V4-Flash-Vision-Exp-ROCMFPX-MIX-STRIX.gguf \ + --target-device hip:0 --max-ctx 131072 --chunk 8192 \ + --cache-type-k q4_0 --cache-type-v q4_0 \ + --ds4-fused-decode --ds4-fused-verify-f16-kv \ + --ds4-expert-top-k 6 --ds4-prefill sparse \ + --mmproj models/DeepSeek-V4-Flash-Vision-Exp-mmproj-BF16.gguf \ + --port 8216 +``` + +`hip:0` must be the Strix Halo; on a host with a discrete GPU too, expose the +Strix Halo alone with `HIP_VISIBLE_DEVICES`. This is the text model's published +launch plus `--mmproj`: the Vision file replaces +`DeepSeek-V4-Flash-0731-ROCMFPX-MIX-STRIX.gguf` for text as well and decodes +at least as fast (numbers below). For R9700 + Strix Halo see [DS4V](#ds4v) below. + +### Send an image + +```bash +IMG=$(base64 < photo.png | tr -d '\n') +curl -s http://127.0.0.1:8216/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -d '{"messages":[{"role":"user","content":[ + {"type":"text","text":"What does this chart show?"}, + {"type":"image_url","image_url":{"url":"data:image/png;base64,'"$IMG"'"}}]}], + "max_tokens":256}' +``` + +`GET /props` reports `capabilities.image_input_supported: true` once the +projector has loaded. In the Docker images, set `LUCE_MMPROJ` to the projector +path inside the container. + ## Request contract Use `POST /v1/chat/completions` with user-message content parts in display order: @@ -49,11 +118,7 @@ support images. `/props` reports the effective capability in ## Qwen3.5 / Qwen3.8 -``` -luce_server Qwen3.8-27B-IQ4_XS-pure.gguf --target-device hip:0 \ - --draft Qwen3.8-27B-DFlash2-Q8_0.gguf --draft-device hip:0 \ - --mmproj Qwen3.8-27B-mmproj-Q8_0.gguf -``` +The launch is in the [quick start](#qwen38-27b-on-one-gpu-r9700). The projector is read directly from the published `clip` file, BF16, F16 or Q8_0; Q8_0 is recommended (below). Projectors with @@ -115,8 +180,8 @@ on ROCm). CMake reports `hipBLASLt found: building the DS4V vision ops`; a build without it refuses `--mmproj` for this model at startup. Image input needs Linux HIP, a DeepSeek4 decoder whose GGUF carries the image -router biases, `--ds4-prefill sparse`, and `--mmproj` pointing at the -[exported projector](ds4v-mmproj.md). Two layouts work: +router biases, `--ds4-prefill sparse`, and `--mmproj` pointing at the published projector (or one [exported with our +tool](ds4v-mmproj.md)). Two layouts work: - **One GPU holding the whole model** (for example a Strix Halo): nothing else to set. The projector is loaded after the weights and must fit beside them. diff --git a/server/docs/ENVIRONMENT.md b/server/docs/ENVIRONMENT.md index 3ccb2a7ec..7b30ed230 100644 --- a/server/docs/ENVIRONMENT.md +++ b/server/docs/ENVIRONMENT.md @@ -79,6 +79,7 @@ consolidation of this list into CLI flags is tracked as follow-up work. | `LUCE_KVFLASH` | unset | Prefer the CLI: `--kvflash` (token count or `auto`). | | `LUCE_PREFIX_CACHE_SLOTS` | 32 | Container-entrypoint equivalent of `--prefix-cache-slots`; not read directly by the native binary. | | `LUCE_PREFILL_CACHE_SLOTS` | 0 | Container-entrypoint equivalent of `--prefill-cache-slots`; not read directly by the native binary. | +| `LUCE_MMPROJ` | unset | Container-entrypoint equivalent of `--mmproj` (vision projector path, enables image input); not read directly by the native binary. | | `LUCE_PREFILL_POOL_TRIM_TOKENS` | unset | OPT-IN: trim cached allocations from legacy CUDA/HIP device pools at completed Qwen3.5 prefill chunk boundaries after each configured token interval. Intended for long, shape-changing prefills on non-VMM devices; each trim synchronizes the target backend and retires captured graphs. | | `LUCE_SPLIT_FAST_ROLLBACK` | unset | OPT-IN: exact F32 checkpoints and replay-free rollback for local qwen35 target layer splits. Prefer `--target-split-fast-rollback`; adds checkpoint VRAM (~1.65 GiB for the measured Qwen3.6-27B q=16 split). | | `LUCE_STALL_TOOL_PREFIX` | unset | OPT-IN: recover a stalled tool call by injecting the prepared tool prefix when generation stops after an action suffix. | @@ -293,6 +294,7 @@ consolidation of this list into CLI flags is tracked as follow-up work. - `LUCE_NO_MOE_SWIGLU_FUSE` - qwen35moe_ffn.cpp - `LUCE_NO_PREAD` - deepseek4_loader.cpp - `LUCE_PROF` - prof_env.h +- `LUCE_MMPROJ` - scripts/entrypoint.sh (maps to `--mmproj`) - `LUCE_PREFILL_CACHE_SLOTS` - scripts/entrypoint.sh (maps to `--prefill-cache-slots`) - `LUCE_PREFILL_POOL_TRIM_TOKENS` - qwen35_backend.cpp (OPT-IN: trim legacy device pools during long prefills) - `LUCE_PREFILL_TIMING` - qwen35_backend.cpp (DEBUG: per-ubatch prefill build/alloc/compute timing) diff --git a/server/scripts/entrypoint.sh b/server/scripts/entrypoint.sh index dfcdcef91..9f8c2524d 100755 --- a/server/scripts/entrypoint.sh +++ b/server/scripts/entrypoint.sh @@ -501,6 +501,7 @@ CMD=("$LUCE_SERVER_BIN" "$LUCE_TARGET" [ -n "$DRAFT_ARG" ] && CMD+=(--ddtree --ddtree-budget "$LUCE_BUDGET") [ -n "$LUCE_DEFAULT_MAX_TOKENS" ] && CMD+=(--default-max-tokens "$LUCE_DEFAULT_MAX_TOKENS") [ -n "$LUCE_MODEL_NAME" ] && CMD+=(--model-name "$LUCE_MODEL_NAME") +[ -n "${LUCE_MMPROJ:-}" ] && CMD+=(--mmproj "$LUCE_MMPROJ") # `--lazy-draft` is silently dropped by the C++ server unless both # `--prefill-drafter` and `--draft` are present (look for the runtime # warning `--lazy-draft ignored: requires both --prefill-drafter and