mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-07 20:28:07 +02:00
Compare commits
25
Commits
rpc_tensor
...
b10301
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b87ed30f8 | ||
|
|
d8d9887228 | ||
|
|
e40bf88642 | ||
|
|
15586e2d71 | ||
|
|
6a32c29a74 | ||
|
|
eb5667a169 | ||
|
|
3db4ff877d | ||
|
|
e700bfb37f | ||
|
|
a1f96d4fc2 | ||
|
|
9de0fcf2b3 | ||
|
|
803b7fcae8 | ||
|
|
c8e03ce812 | ||
|
|
f9e832c10e | ||
|
|
360e1349f0 | ||
|
|
b06aa774c0 | ||
|
|
cd0fa6051a | ||
|
|
717dad5c8e | ||
|
|
9a688e51e6 | ||
|
|
9303cdd8d3 | ||
|
|
a035a88878 | ||
|
|
020760adfc | ||
|
|
61881b1f7f | ||
|
|
3e3a7a416d | ||
|
|
d52ec04a66 | ||
|
|
e031d95679 |
@@ -60,7 +60,6 @@ jobs:
|
||||
-DCMAKE_BUILD_RPATH="@loader_path" \
|
||||
-DLLAMA_FATAL_WARNINGS=ON \
|
||||
-DLLAMA_BUILD_BORINGSSL=ON \
|
||||
-DGGML_METAL_USE_BF16=ON \
|
||||
-DGGML_METAL_EMBED_LIBRARY=OFF \
|
||||
-DGGML_METAL_SHADER_DEBUG=ON \
|
||||
-DGGML_RPC=ON \
|
||||
@@ -127,7 +126,6 @@ jobs:
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_USE_BF16=ON \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
@@ -178,7 +176,6 @@ jobs:
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_USE_BF16=ON \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_BUILD_COMMON=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
@@ -212,7 +209,6 @@ jobs:
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_USE_BF16=ON \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_BUILD_COMMON=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
@@ -257,7 +253,6 @@ jobs:
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_USE_BF16=ON \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
|
||||
@@ -71,6 +71,26 @@ jobs:
|
||||
nvidia-smi
|
||||
GG_BUILD_CUDA=1 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
|
||||
gpu-rocm:
|
||||
runs-on: [self-hosted, Linux, AMD]
|
||||
|
||||
steps:
|
||||
- name: Clone
|
||||
id: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Test
|
||||
id: ggml-ci
|
||||
# HIP_LAUNCH_BLOCKING=1: workaround for an async-execution correctness
|
||||
# issue on integrated RDNA3.5 (gfx1151) where batched inference returns
|
||||
# incorrect output (perplexity ~88 vs ~9.4). Serializing kernel launches
|
||||
# restores correctness. Remove once the underlying ROCm/HIP issue is fixed.
|
||||
env:
|
||||
HIP_LAUNCH_BLOCKING: "1"
|
||||
run: |
|
||||
rocminfo
|
||||
GG_BUILD_ROCM=1 GG_BUILD_AMDGPU_TARGETS=gfx1151 bash ./ci/run.sh ~/results/llama.cpp ~/mnt/llama.cpp
|
||||
|
||||
gpu-vulkan-nvidia-cm:
|
||||
runs-on: [self-hosted, Linux, NVIDIA]
|
||||
|
||||
|
||||
@@ -93,13 +93,13 @@ jobs:
|
||||
- build: 'arm64'
|
||||
arch: 'arm64'
|
||||
os: macos-26
|
||||
defines: "-DGGML_METAL_USE_BF16=ON -DGGML_METAL_EMBED_LIBRARY=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3"
|
||||
defines: "-DGGML_METAL_EMBED_LIBRARY=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3"
|
||||
# TODO: this build is disabled to save Github Actions resources (https://github.com/ggml-org/llama.cpp/pull/23780)
|
||||
# in order to enable it again, we have to provision dedicated runners to run it
|
||||
#- build: 'arm64-kleidiai'
|
||||
# arch: 'arm64'
|
||||
# os: macos-14
|
||||
# defines: "-DGGML_METAL_USE_BF16=ON -DGGML_METAL_EMBED_LIBRARY=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 -DGGML_CPU_KLEIDIAI=ON"
|
||||
# defines: "-DGGML_METAL_EMBED_LIBRARY=ON -DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 -DGGML_CPU_KLEIDIAI=ON"
|
||||
- build: 'x64'
|
||||
arch: 'x64'
|
||||
os: macos-15-intel
|
||||
@@ -1402,7 +1402,6 @@ jobs:
|
||||
run: |
|
||||
sysctl -a
|
||||
cmake -B build -G Xcode \
|
||||
-DGGML_METAL_USE_BF16=ON \
|
||||
-DGGML_METAL_EMBED_LIBRARY=ON \
|
||||
-DLLAMA_OPENSSL=OFF \
|
||||
-DLLAMA_BUILD_APP=OFF \
|
||||
|
||||
@@ -21,11 +21,18 @@ Please disclose it as a private [security advisory](https://github.com/ggml-org/
|
||||
|
||||
A team of volunteers on a reasonable-effort basis maintains this project. As such, please give us at least 90 days to work on a fix before public exposure.
|
||||
|
||||
### AI-powered code scan
|
||||
|
||||
llama.cpp has an AI security scanner that scans the code periodically. The full prompts and tool set can be found in [ggml-org/security-scan-prompt](https://github.com/ggml-org/security-scan-prompt).
|
||||
|
||||
We greatly appreciate reports that reflect genuine research effort, and we are happy to spend our time reviewing them. Findings that an autonomous AI agent can surface on its own add little on top of the scans we already run.
|
||||
|
||||
### Requirements
|
||||
|
||||
Before submitting your report, ensure you meet the following requirements:
|
||||
|
||||
- You have read this policy and fully understand it.
|
||||
- You have searched for existing discussions of the issue. If it has already been reported, your report will likely be rejected as a duplicate.
|
||||
- AI is only permitted in an assistive capacity as stated in [AGENTS.md](AGENTS.md). We do not accept reports that are written exclusively by AI.
|
||||
- Your report must include a working Proof-of-Concept in the form of a script and/or attached files.
|
||||
|
||||
@@ -46,6 +53,8 @@ Only vulnerabilities that fall within these parts of the project are considered
|
||||
|
||||
Note that none of the topics under [Using llama.cpp securely](#using-llamacpp-securely) are considered vulnerabilities in LLaMA C++.
|
||||
|
||||
Denial-of-Service (DoS) bugs are generally not treated as vulnerabilities. We don't reject them outright, but we look at them case-by-case and only accept those that are genuinely worth fixing.
|
||||
|
||||
For vulnerabilities that fall within the `vendor` directory, please report them directly to the third-party project.
|
||||
|
||||
## Using llama.cpp securely
|
||||
|
||||
@@ -17,7 +17,6 @@ LLAMA_BUILD_MTMD=ON
|
||||
GGML_METAL=ON
|
||||
GGML_METAL_EMBED_LIBRARY=ON
|
||||
GGML_BLAS_DEFAULT=ON
|
||||
GGML_METAL_USE_BF16=ON
|
||||
GGML_OPENMP=OFF
|
||||
|
||||
COMMON_C_FLAGS="-Wno-macro-redefined -Wno-shorten-64-to-32 -Wno-unused-command-line-argument -g"
|
||||
@@ -44,7 +43,6 @@ COMMON_CMAKE_ARGS=(
|
||||
-DGGML_METAL_EMBED_LIBRARY=${GGML_METAL_EMBED_LIBRARY}
|
||||
-DGGML_BLAS_DEFAULT=${GGML_BLAS_DEFAULT}
|
||||
-DGGML_METAL=${GGML_METAL}
|
||||
-DGGML_METAL_USE_BF16=${GGML_METAL_USE_BF16}
|
||||
-DGGML_NATIVE=OFF
|
||||
-DGGML_OPENMP=${GGML_OPENMP}
|
||||
)
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
# # with CUDA support
|
||||
# GG_BUILD_CUDA=1 bash ./ci/run.sh ./tmp/results ./tmp/mnt
|
||||
#
|
||||
# # with ROCm support
|
||||
# GG_BUILD_ROCM=1 GG_BUILD_AMDGPU_TARGETS=gfx1151 bash ./ci/run.sh ./tmp/results ./tmp/mnt
|
||||
#
|
||||
# # with SYCL support
|
||||
# GG_BUILD_SYCL=1 bash ./ci/run.sh ./tmp/results ./tmp/mnt
|
||||
#
|
||||
@@ -89,7 +92,7 @@ if [ ! -z ${GG_BUILD_CUDA} ]; then
|
||||
fi
|
||||
|
||||
if [ ! -z ${GG_BUILD_ROCM} ]; then
|
||||
CMAKE_EXTRA="${CMAKE_EXTRA} -DGGML_HIP=ON"
|
||||
CMAKE_EXTRA="${CMAKE_EXTRA} -DCMAKE_HIP_COMPILER=$(hipconfig -l)/clang -DGGML_HIP=ON -DGGML_HIP_ROCWMMA_FATTN=ON"
|
||||
if [ -z ${GG_BUILD_AMDGPU_TARGETS} ]; then
|
||||
echo "Missing GG_BUILD_AMDGPU_TARGETS, please set it to your GPU architecture (e.g. gfx90a, gfx1100, etc.)"
|
||||
exit 1
|
||||
@@ -640,39 +643,52 @@ function gg_sum_rerank_tiny {
|
||||
|
||||
function gg_check_build_requirements {
|
||||
if ! command -v git &> /dev/null; then
|
||||
gg_printf 'git not found, please install'
|
||||
gg_printf 'git not found, please install\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v git-lfs &> /dev/null; then
|
||||
gg_printf 'git-lfs not found, please install'
|
||||
gg_printf 'git-lfs not found, please install\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! git config --get filter.lfs.clean &> /dev/null; then
|
||||
gg_printf 'git-lfs not initialized, please run `git lfs install`\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v wget &> /dev/null; then
|
||||
gg_printf 'wget not found, please install'
|
||||
gg_printf 'wget not found, please install\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
gg_printf 'python3 not found, please install'
|
||||
gg_printf 'python3 not found, please install\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v pip3 &> /dev/null; then
|
||||
gg_printf 'pip3 not found, please install'
|
||||
gg_printf 'pip3 not found, please install\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! python3 -m ensurepip --help &> /dev/null; then
|
||||
gg_printf 'ensurepip not found, please install python3-venv package'
|
||||
gg_printf 'ensurepip not found, please install python3-venv package\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v cmake &> /dev/null; then
|
||||
gg_printf 'cmake not found, please install'
|
||||
gg_printf 'cmake not found, please install\n'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v ccache &> /dev/null; then
|
||||
gg_printf 'ccache not found, please consider installing for faster builds'
|
||||
gg_printf 'ccache not found, please consider installing for faster builds\n'
|
||||
fi
|
||||
|
||||
if ! command -v ctest &> /dev/null; then
|
||||
gg_printf 'ctest not found, please install'
|
||||
gg_printf 'ctest not found, please install\n'
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -136,7 +136,10 @@ static std::vector<llama_device_memory_data> common_get_device_memory_data_impl(
|
||||
devs.push_back(llama_model_get_device(model, i));
|
||||
}
|
||||
|
||||
hp_ngl = llama_model_n_layer(model) + llama_model_n_layer_nextn(model);
|
||||
hp_ngl = llama_model_n_layer(model);
|
||||
if (mparams->load_mtp) {
|
||||
hp_ngl += llama_model_n_layer_nextn(model);
|
||||
}
|
||||
hp_n_ctx_train = llama_model_n_ctx_train(model);
|
||||
hp_n_expert = llama_model_n_expert(model);
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ TEXT_MODEL_MAP: dict[str, str] = {
|
||||
"Exaone4ForCausalLM": "exaone",
|
||||
"ExaoneForCausalLM": "exaone",
|
||||
"ExaoneMoEForCausalLM": "exaone",
|
||||
"ExaoneMoeForCausalLM": "exaone",
|
||||
"FalconForCausalLM": "falcon",
|
||||
"FalconH1ForCausalLM": "falcon_h1",
|
||||
"FalconMambaForCausalLM": "mamba",
|
||||
|
||||
+21
-1
@@ -17,8 +17,11 @@ from .base import LazyTorchTensor, MmprojModel, ModelBase, TextModel, gguf, logg
|
||||
from .qwen import QwenModel
|
||||
|
||||
|
||||
@ModelBase.register("DeepseekOCRForCausalLM", "UnlimitedOCRForCausalLM")
|
||||
@ModelBase.register("DeepseekOCRForCausalLM")
|
||||
class DeepseekOCRVisionModel(MmprojModel):
|
||||
# HF dynamic_preprocess() max_num, which differs per model
|
||||
preproc_max_tiles = 9
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.clip_projector_type = gguf.VisionProjectorType.DEEPSEEKOCR
|
||||
@@ -43,6 +46,9 @@ class DeepseekOCRVisionModel(MmprojModel):
|
||||
# @bluebread: there's no window_size in config but just add it here anyway
|
||||
self.gguf_writer.add_vision_window_size(self.hparams.get("window_size", 14))
|
||||
|
||||
self.gguf_writer.add_vision_preproc_min_tiles(2)
|
||||
self.gguf_writer.add_vision_preproc_max_tiles(self.preproc_max_tiles)
|
||||
|
||||
# SAM configuration
|
||||
sam_hparams = hparams['sam']
|
||||
self.gguf_writer.add_vision_sam_layers_count(sam_hparams['layers'])
|
||||
@@ -93,8 +99,15 @@ class DeepseekOCRVisionModel(MmprojModel):
|
||||
return super().filter_tensors((name, gen))
|
||||
|
||||
|
||||
@ModelBase.register("UnlimitedOCRForCausalLM")
|
||||
class UnlimitedOCRVisionModel(DeepseekOCRVisionModel):
|
||||
preproc_max_tiles = 32
|
||||
|
||||
|
||||
@ModelBase.register("DeepseekOCR2ForCausalLM")
|
||||
class DeepseekOCR2VisionModel(DeepseekOCRVisionModel):
|
||||
preproc_max_tiles = 6
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.clip_projector_type = gguf.VisionProjectorType.DEEPSEEKOCR2
|
||||
@@ -520,6 +533,13 @@ class DeepseekV4Model(TextModel):
|
||||
for key, value in raw_hparams.items():
|
||||
self.hparams.setdefault(key, value)
|
||||
|
||||
# workaround for special rope_parameters (main/compress) in transformers 5.x
|
||||
if self.rope_parameters.get("full_attention", self.rope_parameters).get("rope_type") is None:
|
||||
if (rope_scaling := raw_hparams.get("rope_scaling")) is not None:
|
||||
if "rope_type" not in rope_scaling and (rope_type := rope_scaling.get("type")) is not None:
|
||||
rope_scaling["rope_type"] = rope_type
|
||||
self.rope_parameters.update(**rope_scaling)
|
||||
|
||||
self.block_count = self.hparams["num_hidden_layers"]
|
||||
if self.mtp_only:
|
||||
self.block_count += self.hparams.get("num_nextn_predict_layers", 0)
|
||||
|
||||
@@ -123,7 +123,9 @@ class Exaone4Model(TextModel):
|
||||
yield (self.format_tensor_name(gguf.MODEL_TENSOR.ROPE_FREQS), torch.tensor(rope_factors, dtype=torch.float32))
|
||||
|
||||
|
||||
@ModelBase.register("ExaoneMoEForCausalLM")
|
||||
# note: transformers >= 5.1 renamed the class to "ExaoneMoeForCausalLM" (lowercase 'e'),
|
||||
# so accept both spellings - LG AI have updated the configs of already-released models
|
||||
@ModelBase.register("ExaoneMoEForCausalLM", "ExaoneMoeForCausalLM")
|
||||
class ExaoneMoEModel(Exaone4Model):
|
||||
model_arch = gguf.MODEL_ARCH.EXAONE_MOE
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ CMD_ARGS+=("../../convert_hf_to_gguf.py" "--verbose")
|
||||
CMD_ARGS+=("${MODEL_PATH}")
|
||||
CMD_ARGS+=("--outfile" "${CONVERTED_MODEL}")
|
||||
CMD_ARGS+=("--outtype" "${TYPE}")
|
||||
CMD_ARGS+=("--model-name" "${MODEL_NAME}")
|
||||
[[ -n "$METADATA_OVERRIDE" ]] && CMD_ARGS+=("--metadata" "${METADATA_OVERRIDE}")
|
||||
[[ -n "$MMPROJ" ]] && CMD_ARGS+=("${MMPROJ}")
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ python ../../convert_hf_to_gguf.py --verbose \
|
||||
${EMBEDDING_MODEL_PATH} \
|
||||
--outfile ${CONVERTED_MODEL} \
|
||||
--outtype ${TYPE} \
|
||||
--model-name ${MODEL_NAME} \
|
||||
${SENTENCE_TRANSFORMERS}
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -2788,6 +2788,12 @@ extern "C" {
|
||||
struct ggml_cgraph * cgraph,
|
||||
struct ggml_tensor * tensor);
|
||||
|
||||
// add the tensor and its parents to the graph without marking them for compute
|
||||
// the flag is set later, when the tensor is reached from a node that computes
|
||||
GGML_API void ggml_build_forward_order(
|
||||
struct ggml_cgraph * cgraph,
|
||||
struct ggml_tensor * tensor);
|
||||
|
||||
GGML_API void ggml_build_backward_expand(
|
||||
struct ggml_context * ctx, // context for gradient computation
|
||||
struct ggml_cgraph * cgraph,
|
||||
|
||||
@@ -4033,7 +4033,11 @@ static void ggml_cuda_graph_evaluate_and_capture(ggml_backend_cuda_context * cud
|
||||
continue;
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
assert(node->buffer->buft == ggml_backend_cuda_buffer_type(cuda_ctx->device));
|
||||
// On integrated GPUs (APUs, e.g. RDNA3.5) the scheduler may place a
|
||||
// node's output on the host-visible buffer, which the compute path
|
||||
// handles. Allow that here, mirroring the src-tensor check below.
|
||||
assert(node->buffer->buft == ggml_backend_cuda_buffer_type(cuda_ctx->device) ||
|
||||
(integrated && ggml_backend_buft_is_cuda_host(node->buffer->buft)));
|
||||
for (int j = 0; j < GGML_MAX_SRC; j++) {
|
||||
if (node->src[j] != nullptr) {
|
||||
assert(node->src[j]->buffer);
|
||||
@@ -5205,6 +5209,7 @@ static bool ggml_backend_cuda_device_offload_op(ggml_backend_dev_t dev, const gg
|
||||
|
||||
static ggml_backend_event_t ggml_backend_cuda_device_event_new(ggml_backend_dev_t dev) {
|
||||
#ifdef GGML_CUDA_NO_PEER_COPY
|
||||
GGML_UNUSED(dev);
|
||||
return nullptr;
|
||||
#else
|
||||
ggml_backend_cuda_device_context * dev_ctx = (ggml_backend_cuda_device_context *)dev->context;
|
||||
|
||||
@@ -8,7 +8,6 @@ struct __builtin_align__(32) float8 {
|
||||
float x; float y; float z; float w;
|
||||
float p; float q; float r; float s;
|
||||
};
|
||||
#endif
|
||||
|
||||
#if CUDART_VERSION >= 12080
|
||||
static __device__ __forceinline__ float nvfp4_native_scale_error(
|
||||
@@ -49,6 +48,7 @@ static __device__ __forceinline__ float nvfp4_native_scale_error(
|
||||
return err;
|
||||
}
|
||||
#endif // CUDART_VERSION >= 12080
|
||||
#endif // defined(BLACKWELL_MMA_AVAILABLE)
|
||||
|
||||
__launch_bounds__(CUDA_QUANTIZE_BLOCK_SIZE, 1)
|
||||
static __global__ void quantize_q8_1(
|
||||
|
||||
@@ -11328,8 +11328,8 @@ kernel void kernel_lightning_indexer(
|
||||
const int i_kv_0 = tgpig.x*NK; // first key of this threadgroup
|
||||
const int i_kv = i_kv_0 + sgitg*NKPSG; // first key of this simdgroup
|
||||
|
||||
threadgroup half4x4 sk4x4[NK*DK16];
|
||||
threadgroup half * sk = (threadgroup half *) sk4x4;
|
||||
threadgroup half sk[NK * DK16 * 16];
|
||||
threadgroup half4x4 * sk4x4 = (threadgroup half4x4 *) sk;
|
||||
|
||||
for (short i = tiitg; i < NK*DK16; i += NTG) {
|
||||
const short ik = i/DK16;
|
||||
|
||||
@@ -186,13 +186,22 @@ static bool is_pow2(uint32_t x) { return x > 1 && (x & (x-1)) == 0; }
|
||||
|
||||
#define VK_DEVICE_DESCRIPTOR_POOL_SIZE 256
|
||||
|
||||
#define VK_CHECK(err, msg) \
|
||||
#define VK_CHECK(err, msg, dev) \
|
||||
do { \
|
||||
vk::Result err_ = (err); \
|
||||
vk::Result err_; \
|
||||
try { \
|
||||
err_ = (err); \
|
||||
} catch (vk::DeviceLostError &) { \
|
||||
ggml_vk_print_device_lost_info(dev); \
|
||||
GGML_LOG_ERROR("ggml_vulkan: %s at %s:%d\n", \
|
||||
#err, __FILE__, __LINE__); \
|
||||
throw; \
|
||||
} \
|
||||
if (err_ != vk::Result::eSuccess) { \
|
||||
fprintf(stderr, "ggml_vulkan: %s error %s at %s:%d\n", \
|
||||
GGML_LOG_ERROR("ggml_vulkan: %s error %s at %s:%d\n", \
|
||||
#err, to_string(err_).c_str(), __FILE__, __LINE__); \
|
||||
exit(1); \
|
||||
throw vk::SystemError(vk::make_error_code(err_), \
|
||||
"ggml_vulkan: " msg); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
@@ -302,9 +311,13 @@ struct vk_command_pool {
|
||||
}
|
||||
};
|
||||
|
||||
static void ggml_vk_print_device_fault_info(const vk_device& device);
|
||||
static void ggml_vk_print_device_lost_info(const vk_device& device);
|
||||
|
||||
// Prevent simultaneous submissions to the same queue.
|
||||
struct vk_queue_handle {
|
||||
vk::Queue queue;
|
||||
vk_device_ref device;
|
||||
virtual void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) = 0;
|
||||
virtual void lock() {} // no-op by default (internally synchronized case)
|
||||
virtual void unlock() {}
|
||||
@@ -315,7 +328,14 @@ struct vk_queue_handle_synchronized : vk_queue_handle {
|
||||
std::mutex mutex;
|
||||
void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
queue.submit(submits, fence);
|
||||
try {
|
||||
queue.submit(submits, fence);
|
||||
} catch (vk::DeviceLostError &) {
|
||||
if (auto dev = device.lock()) {
|
||||
ggml_vk_print_device_lost_info(dev);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
void lock() override { mutex.lock(); }
|
||||
void unlock() override { mutex.unlock(); }
|
||||
@@ -324,7 +344,14 @@ struct vk_queue_handle_synchronized : vk_queue_handle {
|
||||
struct vk_queue_handle_unsynchronized : vk_queue_handle {
|
||||
void submit(vk::ArrayProxy<const vk::SubmitInfo> submits, vk::Fence fence) override {
|
||||
// Driver guarantees internal synchronization via VK_KHR_internally_synchronized_queues
|
||||
queue.submit(submits, fence);
|
||||
try {
|
||||
queue.submit(submits, fence);
|
||||
} catch (vk::DeviceLostError &) {
|
||||
if (auto dev = device.lock()) {
|
||||
ggml_vk_print_device_lost_info(dev);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
// lock()/unlock() inherited no-ops
|
||||
};
|
||||
@@ -835,6 +862,15 @@ struct vk_device_struct {
|
||||
|
||||
bool pipeline_executable_properties_support {};
|
||||
|
||||
bool device_fault {};
|
||||
PFN_vkGetDeviceFaultInfoEXT pfn_vkGetDeviceFaultInfoEXT {};
|
||||
|
||||
bool serialize_submissions {};
|
||||
|
||||
const ggml_cgraph * diag_cgraph {};
|
||||
int diag_prev_start = -1;
|
||||
int diag_prev_end = -1;
|
||||
|
||||
size_t idx;
|
||||
|
||||
bool mul_mat_l[GGML_TYPE_COUNT];
|
||||
@@ -1118,6 +1154,57 @@ void vk_command_pool::destroy(vk::Device& device) {
|
||||
cmd_buffers.clear();
|
||||
}
|
||||
|
||||
static void ggml_vk_print_device_fault_info(const vk_device& device) {
|
||||
if (!device->device_fault || !device->pfn_vkGetDeviceFaultInfoEXT) {
|
||||
return;
|
||||
}
|
||||
|
||||
VkDeviceFaultCountsEXT fault_counts {};
|
||||
fault_counts.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_COUNTS_EXT;
|
||||
VkResult res = device->pfn_vkGetDeviceFaultInfoEXT(device->device, &fault_counts, nullptr);
|
||||
if (res != VK_SUCCESS) {
|
||||
GGML_LOG_ERROR("ggml_vulkan: vkGetDeviceFaultInfoEXT (counts) failed: %d\n", res);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<VkDeviceFaultAddressInfoEXT> address_infos(fault_counts.addressInfoCount);
|
||||
std::vector<VkDeviceFaultVendorInfoEXT> vendor_infos(fault_counts.vendorInfoCount);
|
||||
|
||||
VkDeviceFaultInfoEXT fault_info {};
|
||||
fault_info.sType = VK_STRUCTURE_TYPE_DEVICE_FAULT_INFO_EXT;
|
||||
fault_info.pAddressInfos = address_infos.data();
|
||||
fault_info.pVendorInfos = vendor_infos.data();
|
||||
|
||||
res = device->pfn_vkGetDeviceFaultInfoEXT(device->device, &fault_counts, &fault_info);
|
||||
if (res != VK_SUCCESS) {
|
||||
GGML_LOG_ERROR("ggml_vulkan: vkGetDeviceFaultInfoEXT (info) failed: %d\n", res);
|
||||
return;
|
||||
}
|
||||
|
||||
if (fault_counts.addressInfoCount == 0 && fault_counts.vendorInfoCount == 0 && fault_info.description[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fault_info.description[0] != '\0') {
|
||||
GGML_LOG_ERROR("ggml_vulkan: device fault on %s: %s\n", device->name.c_str(), fault_info.description);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < fault_counts.addressInfoCount; i++) {
|
||||
const auto& info = address_infos[i];
|
||||
GGML_LOG_CONT(" address fault %u: type=%d address=0x%llx precision=0x%llx\n",
|
||||
i, (int)info.addressType,
|
||||
(unsigned long long)info.reportedAddress,
|
||||
(unsigned long long)info.addressPrecision);
|
||||
}
|
||||
for (uint32_t i = 0; i < fault_counts.vendorInfoCount; i++) {
|
||||
const auto& info = vendor_infos[i];
|
||||
GGML_LOG_CONT(" vendor fault %u: %s (code=0x%llx data=0x%llx)\n",
|
||||
i, info.description,
|
||||
(unsigned long long)info.vendorFaultCode,
|
||||
(unsigned long long)info.vendorFaultData);
|
||||
}
|
||||
}
|
||||
|
||||
struct vk_buffer_struct {
|
||||
vk::Buffer buffer = VK_NULL_HANDLE;
|
||||
vk::DeviceMemory device_memory = VK_NULL_HANDLE;
|
||||
@@ -2059,6 +2146,36 @@ static uint64_t ggml_vk_get_node_flops(const ggml_tensor * node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ggml_vk_print_node_list(const ggml_cgraph * cgraph, int start, int end) {
|
||||
uint64_t total_flops = 0;
|
||||
int n_ops = 0;
|
||||
for (int j = start; j <= end && j < cgraph->n_nodes; j++) {
|
||||
uint64_t flops = ggml_vk_get_node_flops(cgraph->nodes[j]);
|
||||
total_flops += flops;
|
||||
n_ops++;
|
||||
if (flops > 0) {
|
||||
GGML_LOG_CONT(" node %d: %s (%s) [%.2f GFLOP]\n",
|
||||
j, cgraph->nodes[j]->name, ggml_op_name(cgraph->nodes[j]->op),
|
||||
flops / 1e9);
|
||||
} else {
|
||||
GGML_LOG_CONT(" node %d: %s (%s)\n",
|
||||
j, cgraph->nodes[j]->name, ggml_op_name(cgraph->nodes[j]->op));
|
||||
}
|
||||
}
|
||||
GGML_LOG_CONT(" total: %d ops, %.2f GFLOP\n", n_ops, total_flops / 1e9);
|
||||
}
|
||||
|
||||
static void ggml_vk_print_device_lost_info(const vk_device& device) {
|
||||
ggml_vk_print_device_fault_info(device);
|
||||
if (device->serialize_submissions && device->diag_cgraph != nullptr && device->diag_prev_start >= 0) {
|
||||
GGML_LOG_ERROR("ggml_vulkan: device lost on %s, likely caused by previous submission (nodes %d to %d):\n",
|
||||
device->name.c_str(), device->diag_prev_start, device->diag_prev_end);
|
||||
ggml_vk_print_node_list(device->diag_cgraph, device->diag_prev_start, device->diag_prev_end);
|
||||
} else {
|
||||
GGML_LOG_ERROR("ggml_vulkan: device lost on %s\n", device->name.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
class vk_perf_logger {
|
||||
public:
|
||||
void print_timings(bool force = false) {
|
||||
@@ -2471,17 +2588,27 @@ static void ggml_vk_wait_for_fence(ggml_backend_vk_context * ctx) {
|
||||
// Use waitForFences while most of the graph executes. Hopefully the CPU can sleep
|
||||
// during this wait.
|
||||
if (ctx->almost_ready_fence_pending) {
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->almost_ready_fence }, true, UINT64_MAX), "almost_ready_fence");
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->almost_ready_fence }, true, UINT64_MAX), "almost_ready_fence", ctx->device);
|
||||
ctx->device->device.resetFences({ ctx->almost_ready_fence });
|
||||
ctx->almost_ready_fence_pending = false;
|
||||
}
|
||||
|
||||
// Spin (w/pause) waiting for the graph to finish executing.
|
||||
vk::Result result;
|
||||
while ((result = ctx->device->device.getFenceStatus(ctx->fence)) != vk::Result::eSuccess) {
|
||||
for (;;) {
|
||||
try {
|
||||
result = ctx->device->device.getFenceStatus(ctx->fence);
|
||||
} catch (vk::DeviceLostError &) {
|
||||
ggml_vk_print_device_lost_info(ctx->device);
|
||||
GGML_LOG_ERROR("ggml_vulkan: getFenceStatus at %s:%d\n", __FILE__, __LINE__);
|
||||
throw;
|
||||
}
|
||||
if (result == vk::Result::eSuccess) {
|
||||
break;
|
||||
}
|
||||
if (result != vk::Result::eNotReady) {
|
||||
fprintf(stderr, "ggml_vulkan: error %s at %s:%d\n", to_string(result).c_str(), __FILE__, __LINE__);
|
||||
exit(1);
|
||||
GGML_LOG_ERROR("ggml_vulkan: error %s at %s:%d\n", to_string(result).c_str(), __FILE__, __LINE__);
|
||||
throw vk::SystemError(vk::make_error_code(result), "ggml_vulkan: getFenceStatus");
|
||||
}
|
||||
for (uint32_t i = 0; i < 100; ++i) {
|
||||
YIELD();
|
||||
@@ -3172,6 +3299,7 @@ static std::unique_ptr<vk_queue> ggml_vk_create_queue(vk_device& device, uint32_
|
||||
}
|
||||
|
||||
h->queue = device->device.getQueue2(queue_info2);
|
||||
h->device = device;
|
||||
q->handle = h;
|
||||
|
||||
q->cmd_pool.init(device, q.get());
|
||||
@@ -6117,6 +6245,8 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
#endif
|
||||
} else if (strcmp(VK_KHR_INTERNALLY_SYNCHRONIZED_QUEUES_EXTENSION_NAME, properties.extensionName) == 0) {
|
||||
internally_sync_support = true;
|
||||
} else if (strcmp("VK_EXT_device_fault", properties.extensionName) == 0) {
|
||||
device->device_fault = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6471,8 +6601,18 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
}
|
||||
#endif
|
||||
|
||||
VkPhysicalDeviceFaultFeaturesEXT fault_features {};
|
||||
fault_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FAULT_FEATURES_EXT;
|
||||
if (device->device_fault) {
|
||||
last_struct->pNext = (VkBaseOutStructure *)&fault_features;
|
||||
last_struct = (VkBaseOutStructure *)&fault_features;
|
||||
device_extensions.push_back("VK_EXT_device_fault");
|
||||
}
|
||||
|
||||
vkGetPhysicalDeviceFeatures2(device->physical_device, &device_features2);
|
||||
|
||||
device->device_fault = device->device_fault && fault_features.deviceFault;
|
||||
|
||||
device->has_internally_synchronized_queues = internally_synchronized_queues_features.internallySynchronizedQueues;
|
||||
|
||||
// Build queue create infos only after querying whether internally synchronized queues are enabled.
|
||||
@@ -6771,6 +6911,11 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
device_create_info.setPNext(&device_features2);
|
||||
device->device = device->physical_device.createDevice(device_create_info);
|
||||
|
||||
if (device->device_fault) {
|
||||
device->pfn_vkGetDeviceFaultInfoEXT = (PFN_vkGetDeviceFaultInfoEXT)
|
||||
vkGetDeviceProcAddr(device->device, "vkGetDeviceFaultInfoEXT");
|
||||
}
|
||||
|
||||
// Queues
|
||||
device->compute_queue = ggml_vk_create_queue(device, compute_queue_family_index, 0, { vk::PipelineStageFlagBits::eComputeShader | vk::PipelineStageFlagBits::eTransfer }, false);
|
||||
|
||||
@@ -6893,6 +7038,8 @@ static vk_device ggml_vk_get_device(size_t idx) {
|
||||
|
||||
device->idx = idx;
|
||||
|
||||
device->serialize_submissions = getenv("GGML_VK_SERIALIZE_SUBMISSIONS") != nullptr;
|
||||
|
||||
device->disable_fusion = getenv("GGML_VK_DISABLE_FUSION") != nullptr;
|
||||
|
||||
device->add_rms_fusion = !device->disable_fusion &&
|
||||
@@ -8319,7 +8466,7 @@ static void ggml_vk_buffer_write_2d(vk_buffer& dst, size_t offset, const void *
|
||||
}
|
||||
|
||||
ggml_vk_submit(subctx, dst->device->fence);
|
||||
VK_CHECK(dst->device->device.waitForFences({ dst->device->fence }, true, UINT64_MAX), "vk_buffer_write_2d waitForFences");
|
||||
VK_CHECK(dst->device->device.waitForFences({ dst->device->fence }, true, UINT64_MAX), "vk_buffer_write_2d waitForFences", dst->device);
|
||||
dst->device->device.resetFences({ dst->device->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(dst->device);
|
||||
}
|
||||
@@ -8431,7 +8578,7 @@ static void ggml_vk_buffer_read_2d(vk_buffer& src, size_t offset, void * dst, si
|
||||
ggml_vk_ctx_end(subctx);
|
||||
ggml_vk_submit(subctx, src->device->fence);
|
||||
VK_CHECK(src->device->device.waitForFences({ src->device->fence }, true, UINT64_MAX),
|
||||
"vk_buffer_read_2d uma waitForFences");
|
||||
"vk_buffer_read_2d uma waitForFences", src->device);
|
||||
src->device->device.resetFences({ src->device->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(src->device);
|
||||
|
||||
@@ -8452,7 +8599,7 @@ static void ggml_vk_buffer_read_2d(vk_buffer& src, size_t offset, void * dst, si
|
||||
ggml_vk_ctx_end(subctx);
|
||||
|
||||
ggml_vk_submit(subctx, src->device->fence);
|
||||
VK_CHECK(src->device->device.waitForFences({ src->device->fence }, true, UINT64_MAX), "vk_buffer_read_2d waitForFences");
|
||||
VK_CHECK(src->device->device.waitForFences({ src->device->fence }, true, UINT64_MAX), "vk_buffer_read_2d waitForFences", src->device);
|
||||
src->device->device.resetFences({ src->device->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(src->device);
|
||||
|
||||
@@ -8487,7 +8634,7 @@ static void ggml_vk_buffer_copy(vk_buffer& dst, size_t dst_offset, vk_buffer& sr
|
||||
ggml_vk_buffer_copy_async(subctx, dst, dst_offset, src, src_offset, size);
|
||||
ggml_vk_ctx_end(subctx);
|
||||
ggml_vk_submit(subctx, src->device->fence);
|
||||
VK_CHECK(src->device->device.waitForFences({ src->device->fence }, true, UINT64_MAX), "vk_buffer_copy waitForFences");
|
||||
VK_CHECK(src->device->device.waitForFences({ src->device->fence }, true, UINT64_MAX), "vk_buffer_copy waitForFences", src->device);
|
||||
src->device->device.resetFences({ src->device->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(src->device);
|
||||
} else {
|
||||
@@ -8531,7 +8678,7 @@ static void ggml_vk_buffer_memset(vk_buffer& dst, size_t offset, uint32_t c, siz
|
||||
ggml_vk_ctx_end(subctx);
|
||||
|
||||
ggml_vk_submit(subctx, dst->device->fence);
|
||||
VK_CHECK(dst->device->device.waitForFences({ dst->device->fence }, true, UINT64_MAX), "vk_memset waitForFences");
|
||||
VK_CHECK(dst->device->device.waitForFences({ dst->device->fence }, true, UINT64_MAX), "vk_memset waitForFences", dst->device);
|
||||
dst->device->device.resetFences({ dst->device->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(dst->device);
|
||||
}
|
||||
@@ -14266,7 +14413,7 @@ static void ggml_vk_test_matmul(ggml_backend_vk_context * ctx, size_t m, size_t
|
||||
|
||||
auto begin = std::chrono::high_resolution_clock::now();
|
||||
ggml_vk_submit(subctx, ctx->fence);
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX), "ggml_vk_test_matmul waitForFences");
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX), "ggml_vk_test_matmul waitForFences", ctx->device);
|
||||
ctx->device->device.resetFences({ ctx->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(ctx->device);
|
||||
|
||||
@@ -14468,7 +14615,7 @@ static void ggml_vk_test_dequant(ggml_backend_vk_context * ctx, size_t ne, ggml_
|
||||
auto begin = std::chrono::high_resolution_clock::now();
|
||||
|
||||
ggml_vk_submit(subctx, ctx->fence);
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX), "ggml_vk_test_dequant waitForFences");
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX), "ggml_vk_test_dequant waitForFences", ctx->device);
|
||||
ctx->device->device.resetFences({ ctx->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(ctx->device);
|
||||
|
||||
@@ -14754,7 +14901,7 @@ static void ggml_vk_test_dequant_matmul(ggml_backend_vk_context * ctx, size_t m,
|
||||
auto begin = std::chrono::high_resolution_clock::now();
|
||||
|
||||
ggml_vk_submit(subctx, ctx->fence);
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX), "ggml_vk_test_dequant waitForFences");
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX), "ggml_vk_test_dequant waitForFences", ctx->device);
|
||||
ctx->device->device.resetFences({ ctx->fence });
|
||||
ggml_vk_queue_command_pools_cleanup(ctx->device);
|
||||
|
||||
@@ -15553,7 +15700,9 @@ static void ggml_vk_compute_forward(ggml_backend_vk_context * ctx, ggml_cgraph *
|
||||
memset(mset.dst, mset.val, mset.n);
|
||||
}
|
||||
|
||||
if (almost_ready && !ctx->almost_ready_fence_pending) {
|
||||
if (ctx->device->serialize_submissions) {
|
||||
ggml_vk_submit(subctx, ctx->fence);
|
||||
} else if (almost_ready && !ctx->almost_ready_fence_pending) {
|
||||
ggml_vk_submit(subctx, ctx->almost_ready_fence);
|
||||
ctx->almost_ready_fence_pending = true;
|
||||
} else {
|
||||
@@ -16164,12 +16313,20 @@ static void ggml_vk_synchronize(ggml_backend_vk_context * ctx) {
|
||||
memcpy(cpy.dst, cpy.src, cpy.n);
|
||||
}
|
||||
|
||||
ggml_vk_submit(compute_ctx, {});
|
||||
if (ctx->device->serialize_submissions) {
|
||||
ggml_vk_submit(compute_ctx, ctx->fence);
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX), "synchronize waitForFences", ctx->device);
|
||||
ctx->device->device.resetFences({ ctx->fence });
|
||||
} else {
|
||||
ggml_vk_submit(compute_ctx, {});
|
||||
}
|
||||
ctx->submit_pending = true;
|
||||
}
|
||||
|
||||
if (ctx->submit_pending) {
|
||||
if (ctx->device->async_use_transfer_queue && ctx->transfer_semaphore_last_submitted < ctx->transfer_semaphore.value) {
|
||||
if (ctx->device->serialize_submissions) {
|
||||
ctx->submit_pending = false;
|
||||
} else if (ctx->device->async_use_transfer_queue && ctx->transfer_semaphore_last_submitted < ctx->transfer_semaphore.value) {
|
||||
vk::TimelineSemaphoreSubmitInfo tl_info{
|
||||
1, &ctx->transfer_semaphore.value,
|
||||
0, nullptr,
|
||||
@@ -16186,7 +16343,9 @@ static void ggml_vk_synchronize(ggml_backend_vk_context * ctx) {
|
||||
} else {
|
||||
ctx->device->compute_queue->handle->submit({}, ctx->fence);
|
||||
}
|
||||
ggml_vk_wait_for_fence(ctx);
|
||||
if (!ctx->device->serialize_submissions) {
|
||||
ggml_vk_wait_for_fence(ctx);
|
||||
}
|
||||
ctx->submit_pending = false;
|
||||
if (cmd_buf) {
|
||||
cmd_buf->in_use = false;
|
||||
@@ -16758,6 +16917,10 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
|
||||
VK_LOG_DEBUG("ggml_backend_vk_graph_compute(" << cgraph->n_nodes << " nodes)");
|
||||
ggml_backend_vk_context * ctx = (ggml_backend_vk_context *)backend->context;
|
||||
|
||||
ctx->device->diag_cgraph = nullptr;
|
||||
ctx->device->diag_prev_start = -1;
|
||||
ctx->device->diag_prev_end = -1;
|
||||
|
||||
if (vk_instance.debug_utils_support) {
|
||||
vk::DebugUtilsLabelEXT dul = {};
|
||||
dul.pLabelName = "ggml_backend_vk_graph_compute";
|
||||
@@ -16849,6 +17012,36 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
|
||||
}
|
||||
uint64_t flops_per_submit = std::min(flops_cap, ctx->last_total_flops / 40u);
|
||||
|
||||
auto const submit_after = [&](int start, int end) {
|
||||
if (ctx->device->serialize_submissions) {
|
||||
try {
|
||||
auto res = ctx->device->device.waitForFences({ ctx->fence }, true, UINT64_MAX);
|
||||
if (res != vk::Result::eSuccess) {
|
||||
GGML_LOG_ERROR("ggml_vulkan: waitForFences error during serialized submission\n");
|
||||
throw vk::SystemError(vk::make_error_code(res), "ggml_vulkan: waitForFences during serialized submission");
|
||||
}
|
||||
} catch (vk::DeviceLostError &) {
|
||||
ggml_vk_print_device_fault_info(ctx->device);
|
||||
GGML_LOG_ERROR("ggml_vulkan: device lost on %s waiting for submission (nodes %d to %d):\n",
|
||||
ctx->device->name.c_str(), start, end);
|
||||
ggml_vk_print_node_list(cgraph, start, end);
|
||||
throw;
|
||||
}
|
||||
ctx->device->device.resetFences({ ctx->fence });
|
||||
ctx->submit_pending = false;
|
||||
ctx->device->diag_cgraph = cgraph;
|
||||
ctx->device->diag_prev_start = start;
|
||||
ctx->device->diag_prev_end = end;
|
||||
}
|
||||
first_node_in_batch = true;
|
||||
submitted_nodes = 0;
|
||||
batch_flops = 0;
|
||||
if (submit_count < 3) {
|
||||
flops_per_submit *= 2;
|
||||
}
|
||||
submit_count++;
|
||||
};
|
||||
|
||||
for (int i = 0; i < cgraph->n_nodes; i++) {
|
||||
if (first_node_in_batch) {
|
||||
submit_node_idx = i;
|
||||
@@ -16856,8 +17049,20 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
|
||||
|
||||
{
|
||||
auto node_flops = ggml_vk_get_node_flops(cgraph->nodes[i]);
|
||||
batch_flops += node_flops;
|
||||
total_flops += node_flops;
|
||||
|
||||
// Flush the current batch before recording a node that would push it over the flop threshold
|
||||
if (flops_per_submit != 0 && submitted_nodes > 0 && batch_flops + node_flops >= flops_per_submit) {
|
||||
vk_context flush_ctx = ggml_vk_get_compute_ctx(ctx);
|
||||
ggml_vk_ctx_end(flush_ctx);
|
||||
flush_ctx->exit_tensor_idx = -1;
|
||||
ctx->compute_ctx.reset();
|
||||
ggml_vk_compute_forward(ctx, cgraph, cgraph->nodes[submit_node_idx], submit_node_idx, false);
|
||||
submit_after(submit_node_idx, i - 1);
|
||||
submit_node_idx = i;
|
||||
}
|
||||
|
||||
batch_flops += node_flops;
|
||||
}
|
||||
|
||||
// op_srcs_fused_elementwise indicates whether an op's srcs all contribute to
|
||||
@@ -17111,13 +17316,7 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
|
||||
}
|
||||
|
||||
if (submit && enqueued) {
|
||||
first_node_in_batch = true;
|
||||
submitted_nodes = 0;
|
||||
batch_flops = 0;
|
||||
if (submit_count < 3) {
|
||||
flops_per_submit *= 2;
|
||||
}
|
||||
submit_count++;
|
||||
submit_after(submit_node_idx, i + (int)ctx->num_additional_fused_ops);
|
||||
}
|
||||
i += ctx->num_additional_fused_ops;
|
||||
ctx->num_additional_fused_ops = 0;
|
||||
@@ -17133,13 +17332,13 @@ static ggml_status ggml_backend_vk_graph_compute(ggml_backend_t backend, ggml_cg
|
||||
ggml_vk_ctx_end(compute_ctx);
|
||||
|
||||
ggml_vk_submit(compute_ctx, ctx->device->fence);
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->device->fence }, true, UINT64_MAX), "GGML_VULKAN_PERF waitForFences");
|
||||
VK_CHECK(ctx->device->device.waitForFences({ ctx->device->fence }, true, UINT64_MAX), "GGML_VULKAN_PERF waitForFences", ctx->device);
|
||||
ctx->device->device.resetFences({ ctx->device->fence });
|
||||
ctx->compute_ctx.reset();
|
||||
|
||||
// Get the results and pass them to the logger
|
||||
std::vector<uint64_t> timestamps(cgraph->n_nodes + 1);
|
||||
VK_CHECK(ctx->device->device.getQueryPoolResults(ctx->query_pool, 0, ctx->query_idx, (cgraph->n_nodes + 1)*sizeof(uint64_t), timestamps.data(), sizeof(uint64_t), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait), "get timestamp results");
|
||||
VK_CHECK(ctx->device->device.getQueryPoolResults(ctx->query_pool, 0, ctx->query_idx, (cgraph->n_nodes + 1)*sizeof(uint64_t), timestamps.data(), sizeof(uint64_t), vk::QueryResultFlagBits::e64 | vk::QueryResultFlagBits::eWait), "get timestamp results", ctx->device);
|
||||
if (!vk_perf_logger_concurrent) {
|
||||
// Log each op separately
|
||||
for (int i = 1; i < ctx->query_idx; i++) {
|
||||
@@ -18366,7 +18565,7 @@ static void ggml_backend_vk_device_event_synchronize(ggml_backend_dev_t dev, ggm
|
||||
vk::Semaphore sem = vkev->tl_semaphore.s;
|
||||
uint64_t val = vkev->tl_semaphore.value;
|
||||
vk::SemaphoreWaitInfo swi{vk::SemaphoreWaitFlags{}, sem, val};
|
||||
VK_CHECK(device->device.waitSemaphores(swi, UINT64_MAX), "event_synchronize");
|
||||
VK_CHECK(device->device.waitSemaphores(swi, UINT64_MAX), "event_synchronize", device);
|
||||
|
||||
// Reset and move submitted events
|
||||
for (auto& event : vkev->events_submitted) {
|
||||
|
||||
@@ -7200,6 +7200,10 @@ void ggml_build_forward_expand(struct ggml_cgraph * cgraph, struct ggml_tensor *
|
||||
ggml_build_forward_impl(cgraph, tensor, true, true);
|
||||
}
|
||||
|
||||
void ggml_build_forward_order(struct ggml_cgraph * cgraph, struct ggml_tensor * tensor) {
|
||||
ggml_build_forward_impl(cgraph, tensor, true, false);
|
||||
}
|
||||
|
||||
void ggml_build_backward_expand(
|
||||
struct ggml_context * ctx,
|
||||
struct ggml_cgraph * cgraph,
|
||||
|
||||
@@ -59,11 +59,29 @@ def byteswap_q6_k(tensor, block_offs):
|
||||
delta.byteswap(inplace=True)
|
||||
|
||||
|
||||
def byteswap_q1_0(tensor, block_offs):
|
||||
# Each block_q1_0 consists of an f16 delta followed by 16 int8 quantizations.
|
||||
|
||||
# Byte-Swap f16 sized delta field
|
||||
delta = tensor.data[block_offs:block_offs + 2].view(dtype=np.uint16)
|
||||
delta.byteswap(inplace=True)
|
||||
|
||||
|
||||
def byteswap_tq2_0(tensor, block_offs):
|
||||
# Each block_tq2_0 consists of 64 int8 values followed by 1 f16 value.
|
||||
|
||||
# Byte-Swap f16 sized field
|
||||
delta = tensor.data[block_offs + 64:block_offs + 66].view(dtype=np.uint16)
|
||||
delta.byteswap(inplace=True)
|
||||
|
||||
|
||||
byteswap_tensors = {
|
||||
gguf.GGMLQuantizationType.Q1_0: byteswap_q1_0,
|
||||
gguf.GGMLQuantizationType.Q4_0: byteswap_q4_0,
|
||||
gguf.GGMLQuantizationType.Q8_0: byteswap_q8_0,
|
||||
gguf.GGMLQuantizationType.Q4_K: byteswap_q4_k,
|
||||
gguf.GGMLQuantizationType.Q6_K: byteswap_q6_k,
|
||||
gguf.GGMLQuantizationType.TQ2_0: byteswap_tq2_0,
|
||||
gguf.GGMLQuantizationType.MXFP4: byteswap_noop,
|
||||
gguf.GGMLQuantizationType.NVFP4: byteswap_noop,
|
||||
}
|
||||
|
||||
@@ -24,10 +24,31 @@ vendor = {
|
||||
"https://raw.githubusercontent.com/sheredom/subprocess.h/8671cee1fc09f11a70ce3782a0ee13177c3aa387/subprocess.h": "vendor/sheredom/subprocess.h",
|
||||
}
|
||||
|
||||
# TODO @ngxson : this is temporary, to be removed in the future
|
||||
patches = [
|
||||
# https://github.com/sheredom/subprocess.h/pull/102
|
||||
"vendor/sheredom/patch-bsd.patch",
|
||||
# https://github.com/sheredom/subprocess.h/pull/101
|
||||
"vendor/sheredom/patch-windows-quote-backslash.patch",
|
||||
# https://github.com/sheredom/subprocess.h/pull/104
|
||||
# note: must be applied after patch-bsd.patch, they touch adjacent lines
|
||||
"vendor/sheredom/patch-glibc-older-than-2.29.patch",
|
||||
]
|
||||
|
||||
for url, filename in vendor.items():
|
||||
print(f"downloading {url} to {filename}") # noqa: NP100
|
||||
urllib.request.urlretrieve(url, filename)
|
||||
|
||||
for patch in patches:
|
||||
print(f"applying {patch}") # noqa: NP100
|
||||
try:
|
||||
subprocess.check_call([
|
||||
"git", "apply", "--directory", os.path.dirname(patch), patch
|
||||
])
|
||||
except Exception as e:
|
||||
print(f"Error: {e}") # noqa: NP100
|
||||
sys.exit(1)
|
||||
|
||||
print("Splitting httplib.h...") # noqa: NP100
|
||||
try:
|
||||
subprocess.check_call([
|
||||
|
||||
@@ -648,10 +648,12 @@ const char * llama_grammar_parser::parse_sequence(
|
||||
} else {
|
||||
throw std::runtime_error(std::string("expecting ',' at ") + pos);
|
||||
}
|
||||
bool has_max = max_times != UINT64_MAX;
|
||||
if (min_times > MAX_REPETITION_THRESHOLD || (has_max && max_times > MAX_REPETITION_THRESHOLD)) {
|
||||
if (min_times > MAX_REPETITION_THRESHOLD) {
|
||||
throw std::runtime_error(std::string("number of repetitions exceeds sane defaults, please reduce the number of repetitions"));
|
||||
}
|
||||
if (max_times != UINT64_MAX && max_times > MAX_REPETITION_THRESHOLD) {
|
||||
max_times = UINT64_MAX;
|
||||
}
|
||||
handle_repetitions(min_times, max_times);
|
||||
} else {
|
||||
break;
|
||||
|
||||
@@ -1249,7 +1249,13 @@ struct ggml_tensor * llama_model_loader::create_tensor(
|
||||
for (size_t dim = 0; dim < GGML_MAX_DIMS; dim++) {
|
||||
t_meta.ne[dim] = dim < ne.size() ? ne.begin()[dim] : 1;
|
||||
GGML_ASSERT(t_meta.ne[dim] >= 1);
|
||||
t_meta.nb[dim] = dim == 0 ? ggml_type_size(type) : t_meta.ne[dim-1]*t_meta.nb[dim-1];
|
||||
if (dim == 0) {
|
||||
t_meta.nb[dim] = ggml_type_size(type);
|
||||
} else if (dim == 1) {
|
||||
t_meta.nb[dim] = ggml_row_size(type, t_meta.ne[dim-1]);
|
||||
} else {
|
||||
t_meta.nb[dim] = t_meta.nb[dim-1]*t_meta.ne[dim-1];
|
||||
}
|
||||
GGML_ASSERT(t_meta.nb[dim] >= 1);
|
||||
}
|
||||
ggml_set_name(&t_meta, tn.str().c_str());
|
||||
@@ -1272,10 +1278,18 @@ struct ggml_tensor * llama_model_loader::create_tensor(
|
||||
if (flags & TENSOR_ALLOW_RESHAPE) {
|
||||
for (size_t dim = 0; dim < GGML_MAX_DIMS; dim++) {
|
||||
t_meta.ne[dim] = dim < ne.size() ? ne.begin()[dim] : 1;
|
||||
t_meta.nb[dim] = dim == 0 ? ggml_type_size(t_meta.type) : t_meta.ne[dim-1]*t_meta.nb[dim-1];
|
||||
if (dim == 0) {
|
||||
t_meta.nb[dim] = ggml_type_size(t_meta.type);
|
||||
} else if (dim == 1) {
|
||||
t_meta.nb[dim] = ggml_row_size(t_meta.type, t_meta.ne[dim-1]);
|
||||
} else {
|
||||
t_meta.nb[dim] = t_meta.ne[dim-1]*t_meta.nb[dim-1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GGML_ASSERT(ggml_nbytes(&t_meta) == ggml_nbytes(cur));
|
||||
|
||||
ggml_backend_buffer_type_t buft = buft_for_tensor(&t_meta);
|
||||
if (buft == nullptr) {
|
||||
return nullptr;
|
||||
|
||||
@@ -2890,6 +2890,21 @@ void llama_model_base::create_tensor_qkv(llama_layer & layer, int bid,
|
||||
int64_t n_embd_, int64_t n_embd_q_, int64_t n_embd_k_, int64_t n_embd_v_,
|
||||
int flags) {
|
||||
const int64_t n_embd_qkv = n_embd_q_ + n_embd_k_ + n_embd_v_;
|
||||
|
||||
if (flags & TENSOR_SKIP) {
|
||||
const int skip = TENSOR_NOT_REQUIRED | TENSOR_SKIP;
|
||||
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", bid), {n_embd_, n_embd_qkv}, skip | TENSOR_SKIP_IF_VIRTUAL);
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_QKV, "bias", bid), {n_embd_qkv}, skip | TENSOR_SKIP_IF_VIRTUAL);
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_Q, "weight", bid), {n_embd_, n_embd_q_}, skip);
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_K, "weight", bid), {n_embd_, n_embd_k_}, skip);
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_V, "weight", bid), {n_embd_, n_embd_v_}, skip);
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_Q, "bias", bid), {n_embd_q_}, skip);
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_K, "bias", bid), {n_embd_k_}, skip);
|
||||
create_tensor(tn(LLM_TENSOR_ATTN_V, "bias", bid), {n_embd_v_}, skip);
|
||||
return;
|
||||
}
|
||||
|
||||
layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", bid), {n_embd_, n_embd_qkv}, TENSOR_NOT_REQUIRED | TENSOR_SKIP_IF_VIRTUAL);
|
||||
if (layer.wqkv) {
|
||||
layer.wqkv_b = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "bias", bid), {n_embd_qkv}, TENSOR_NOT_REQUIRED | TENSOR_SKIP_IF_VIRTUAL);
|
||||
|
||||
@@ -1668,9 +1668,18 @@ static std::vector<const backend_test_case *> collect_tests_to_run(const std::st
|
||||
}
|
||||
} else {
|
||||
for (const auto & test : BACKEND_TESTS) {
|
||||
if (test.enabled_by_default) {
|
||||
selected.push_back(&test);
|
||||
if (!test.enabled_by_default) {
|
||||
continue;
|
||||
}
|
||||
#ifdef GGML_USE_HIP
|
||||
// TODO: remove this when https://github.com/ggml-org/llama.cpp/pull/26592 is merged
|
||||
if (test.name == "penalties" || test.name == "set_sampler" ||
|
||||
test.name == "mixed" || test.name == "top_p") {
|
||||
fprintf(stderr, "Skipping test '%s' on HIP backend (no backend TOP_K support)\n", test.name.c_str());
|
||||
continue;
|
||||
}
|
||||
#endif // GGML_USE_HIP
|
||||
selected.push_back(&test);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,53 @@ int main()
|
||||
root ::= "a"{,10}"
|
||||
)""");
|
||||
|
||||
verify_failure(R"""(
|
||||
root ::= "a"{5000}
|
||||
)""");
|
||||
|
||||
verify_failure(R"""(
|
||||
root ::= "a"{5000,}
|
||||
)""");
|
||||
|
||||
verify_failure(R"""(
|
||||
root ::= "a"{5000,6000}
|
||||
)""");
|
||||
|
||||
verify_parsing(R"""(
|
||||
root ::= "a"{0,5000}
|
||||
)""", {
|
||||
{"root", 0},
|
||||
{"root_1", 1},
|
||||
}, {
|
||||
// root (index 0)
|
||||
{LLAMA_GRETYPE_RULE_REF, /* root_1 */ 1},
|
||||
{LLAMA_GRETYPE_END, 0},
|
||||
// root_1 (index 1)
|
||||
{LLAMA_GRETYPE_CHAR, 'a'},
|
||||
{LLAMA_GRETYPE_RULE_REF, /* root_1 */ 1},
|
||||
{LLAMA_GRETYPE_ALT, 0},
|
||||
{LLAMA_GRETYPE_END, 0},
|
||||
});
|
||||
|
||||
verify_parsing(R"""(
|
||||
root ::= "a"{3,5000}
|
||||
)""", {
|
||||
{"root", 0},
|
||||
{"root_1", 1},
|
||||
}, {
|
||||
// root (index 0)
|
||||
{LLAMA_GRETYPE_CHAR, 'a'},
|
||||
{LLAMA_GRETYPE_CHAR, 'a'},
|
||||
{LLAMA_GRETYPE_CHAR, 'a'},
|
||||
{LLAMA_GRETYPE_RULE_REF, /* root_1 */ 1},
|
||||
{LLAMA_GRETYPE_END, 0},
|
||||
// root_1 (index 1)
|
||||
{LLAMA_GRETYPE_CHAR, 'a'},
|
||||
{LLAMA_GRETYPE_RULE_REF, /* root_1 */ 1},
|
||||
{LLAMA_GRETYPE_ALT, 0},
|
||||
{LLAMA_GRETYPE_END, 0},
|
||||
});
|
||||
|
||||
verify_parsing(R"""(
|
||||
root ::= "a"
|
||||
)""", {
|
||||
|
||||
@@ -432,11 +432,19 @@ static bool arch_supported(const llm_arch arch) {
|
||||
|
||||
// FIXME: these hit scheduler/view-backed-output issues with WebGPU on CI.
|
||||
#ifdef GGML_USE_WEBGPU
|
||||
if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA || arch == LLM_ARCH_MINIMAX_M3) {
|
||||
if (arch == LLM_ARCH_DEEPSEEK32 || arch == LLM_ARCH_GLM_DSA) {
|
||||
return false;
|
||||
}
|
||||
#endif // GGML_USE_WEBGPU
|
||||
|
||||
// FIXME: jamba produces incorrect output (~0.55 NMSE vs CPU) on the HIP
|
||||
// backend on RDNA3.5 (gfx1151); the SSM kernels need investigation.
|
||||
#ifdef GGML_USE_HIP
|
||||
if (arch == LLM_ARCH_JAMBA) {
|
||||
return false;
|
||||
}
|
||||
#endif // GGML_USE_HIP
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include "mtmd.h"
|
||||
@@ -62,6 +64,72 @@ int main(void) {
|
||||
}
|
||||
}
|
||||
|
||||
// test chunk save/load round-trip
|
||||
for (size_t i = 0; i < n_chunks; i++) {
|
||||
const mtmd_input_chunk * chunk = mtmd_input_chunks_get(chunks, i);
|
||||
assert(chunk != NULL);
|
||||
enum mtmd_input_chunk_type type = mtmd_input_chunk_get_type(chunk);
|
||||
|
||||
// query the required buffer size (out_buf == NULL)
|
||||
size_t expected_len = 0;
|
||||
int32_t rc = mtmd_input_chunk_save(chunk, NULL, 0, &expected_len);
|
||||
printf(" Chunk %zu: save query rc = %d, expected_len = %zu\n", i, rc, expected_len);
|
||||
assert(rc == 0);
|
||||
assert(expected_len > 0);
|
||||
|
||||
// saving into a too-small buffer must fail, not crash
|
||||
char tiny_buf[1];
|
||||
rc = mtmd_input_chunk_save(chunk, tiny_buf, sizeof(tiny_buf), NULL);
|
||||
printf(" Chunk %zu: save into too-small buffer rc = %d (expect non-zero)\n", i, rc);
|
||||
assert(rc != 0);
|
||||
|
||||
// save into a properly-sized buffer
|
||||
char * buf = (char *) malloc(expected_len);
|
||||
assert(buf != NULL);
|
||||
rc = mtmd_input_chunk_save(chunk, buf, expected_len, NULL);
|
||||
assert(rc == 0);
|
||||
|
||||
// loading from a truncated buffer must fail gracefully, not crash
|
||||
if (expected_len > 1) {
|
||||
mtmd_input_chunk * bad = mtmd_input_chunk_load(buf, expected_len - 1);
|
||||
printf(" Chunk %zu: load from truncated buffer = %p (expect NULL)\n", i, (void *) bad);
|
||||
assert(bad == NULL);
|
||||
}
|
||||
|
||||
// load it back
|
||||
mtmd_input_chunk * loaded = mtmd_input_chunk_load(buf, expected_len);
|
||||
assert(loaded != NULL);
|
||||
|
||||
// metadata must match the original chunk
|
||||
assert(mtmd_input_chunk_get_type(loaded) == type);
|
||||
assert(mtmd_input_chunk_get_n_tokens(loaded) == mtmd_input_chunk_get_n_tokens(chunk));
|
||||
assert(mtmd_input_chunk_get_n_pos(loaded) == mtmd_input_chunk_get_n_pos(chunk));
|
||||
|
||||
if (type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
|
||||
size_t n_tok_orig, n_tok_loaded;
|
||||
const llama_token * tok_orig = mtmd_input_chunk_get_tokens_text(chunk, &n_tok_orig);
|
||||
const llama_token * tok_loaded = mtmd_input_chunk_get_tokens_text(loaded, &n_tok_loaded);
|
||||
printf(" Chunk %zu: loaded %zu text tokens (orig %zu), first token %d (orig %d)\n",
|
||||
i, n_tok_loaded, n_tok_orig,
|
||||
n_tok_loaded > 0 ? tok_loaded[0] : -1,
|
||||
n_tok_orig > 0 ? tok_orig[0] : -1);
|
||||
assert(n_tok_orig == n_tok_loaded);
|
||||
for (size_t j = 0; j < n_tok_orig; j++) {
|
||||
assert(tok_orig[j] == tok_loaded[j]);
|
||||
}
|
||||
} else if (type == MTMD_INPUT_CHUNK_TYPE_IMAGE || type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
|
||||
const char * id_orig = mtmd_input_chunk_get_id(chunk);
|
||||
const char * id_loaded = mtmd_input_chunk_get_id(loaded);
|
||||
printf(" Chunk %zu: loaded id '%s' (orig '%s')\n", i, id_loaded, id_orig);
|
||||
assert(id_orig != NULL && id_loaded != NULL);
|
||||
assert(strcmp(id_orig, id_loaded) == 0);
|
||||
}
|
||||
|
||||
mtmd_input_chunk_free(loaded);
|
||||
free(buf);
|
||||
}
|
||||
printf("Chunk save/load round-trip OK\n");
|
||||
|
||||
// Free the chunks
|
||||
mtmd_input_chunks_free(chunks);
|
||||
|
||||
|
||||
@@ -591,6 +591,8 @@ struct clip_image_u8 {
|
||||
}
|
||||
};
|
||||
|
||||
struct mtmd_serialization; // forward declaration
|
||||
|
||||
// For images, buf.size() == nx*ny*3
|
||||
// Memory layout: RGBRGBRGB...
|
||||
// For seq, buf.size() == nx*ny*3*nt
|
||||
@@ -671,6 +673,9 @@ struct clip_image_f32 {
|
||||
return buf.empty();
|
||||
}
|
||||
|
||||
void serialize(struct mtmd_serialization & ser) const;
|
||||
void deserialize(struct mtmd_serialization & ser);
|
||||
|
||||
private:
|
||||
std::vector<float> buf;
|
||||
int nx_ = 0;
|
||||
@@ -752,6 +757,9 @@ struct clip_image_f32_batch {
|
||||
}
|
||||
return new_batch;
|
||||
}
|
||||
|
||||
void serialize(struct mtmd_serialization & ser) const;
|
||||
void deserialize(struct mtmd_serialization & ser);
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
+11
-3
@@ -708,9 +708,10 @@ ggml_tensor * clip_graph::build_attn(
|
||||
ggml_tensor * sinks) const {
|
||||
// these nodes are added to the graph together so that they are not reordered
|
||||
// by doing so, the number of splits in the graph is reduced
|
||||
ggml_build_forward_expand(gf, q_cur);
|
||||
ggml_build_forward_expand(gf, k_cur);
|
||||
ggml_build_forward_expand(gf, v_cur);
|
||||
// the order is fixed without the compute flag, so an unselected branch stays out of the compute set
|
||||
ggml_build_forward_order(gf, q_cur);
|
||||
ggml_build_forward_order(gf, k_cur);
|
||||
ggml_build_forward_order(gf, v_cur);
|
||||
|
||||
ggml_tensor * q = ggml_permute(ctx0, q_cur, 0, 2, 1, 3);
|
||||
//cb(q, "q", il);
|
||||
@@ -1761,6 +1762,10 @@ struct clip_model_loader {
|
||||
// qwen2 encoder is GQA, requires KEY_N_HEAD_KV
|
||||
get_u32(string_format(KEY_N_HEAD_KV, "vision"), hparams.n_head_kv);
|
||||
}
|
||||
// unlimited-ocr shares the v1 projector but tiles up to 32
|
||||
get_u32(KEY_PREPROC_MIN_TILES, hparams.preproc_min_tiles, false);
|
||||
get_u32(KEY_PREPROC_MAX_TILES, hparams.preproc_max_tiles, false);
|
||||
GGML_ASSERT(hparams.preproc_min_tiles <= hparams.preproc_max_tiles);
|
||||
} break;
|
||||
case PROJECTOR_TYPE_HUNYUANVL:
|
||||
{
|
||||
@@ -1909,6 +1914,9 @@ struct clip_model_loader {
|
||||
if (hparams.image_max_pixels > 0) {
|
||||
LOG_INF("%s: image_max_pixels: %d%s\n", __func__, hparams.image_max_pixels, hparams.custom_image_max_tokens > 0 ? " (custom value)" : "");
|
||||
}
|
||||
if (hparams.preproc_max_tiles > 0) {
|
||||
LOG_INF("%s: preproc_tiles: %d - %d\n", __func__, hparams.preproc_min_tiles, hparams.preproc_max_tiles);
|
||||
}
|
||||
} else if (is_audio) {
|
||||
LOG_INF("\n--- audio hparams ---\n");
|
||||
LOG_INF("%s: n_mel_bins: %d\n", __func__, hparams.n_mel_bins);
|
||||
|
||||
@@ -253,6 +253,9 @@ ggml_cgraph * clip_graph_deepseekocr::build() {
|
||||
|
||||
bool is_overview = img.add_viewsep;
|
||||
int n_tiles_per_row = 0;
|
||||
// number of separate "row" images batched together in this graph call
|
||||
// (captured now, before n_batch below gets repurposed as the SAM/ViT batch size)
|
||||
const int n_rows_batch = n_batch;
|
||||
|
||||
// note: we expect either a batch of rows or a batch of overviews, but not a mix of both
|
||||
|
||||
@@ -272,16 +275,18 @@ ggml_cgraph * clip_graph_deepseekocr::build() {
|
||||
GGML_ASSERT(img.ny() % img.nx() == 0);
|
||||
n_tiles_per_row = img.ny() / img.nx();
|
||||
|
||||
// input shape: [tile_size, tile_size * n_tiles_per_row, 3]
|
||||
// we want to reshape it to [tile_size, tile_size, 3, n_tiles_per_row]
|
||||
inp_raw = ggml_reshape_4d(ctx0, inp_raw, img.nx(), img.nx(), n_tiles_per_row, 3);
|
||||
inp_raw = ggml_cont(ctx0, ggml_permute(ctx0, inp_raw, 0, 1, 3, 2));
|
||||
// each entry is one "row" image of shape [tile_size, tile_size * n_tiles_per_row, 3];
|
||||
// merge the tile axis into the batch axis, giving a combined SAM input of shape
|
||||
// [tile_size, tile_size, 3, n_tiles_per_row * n_rows_batch] (tile fast, row slow)
|
||||
inp_raw = ggml_reshape_4d(ctx0, inp_raw, img.nx() * img.nx(), n_tiles_per_row, 3, n_rows_batch);
|
||||
inp_raw = ggml_cont(ctx0, ggml_permute(ctx0, inp_raw, 0, 2, 1, 3));
|
||||
inp_raw = ggml_reshape_4d(ctx0, inp_raw, img.nx(), img.nx(), 3, n_tiles_per_row * n_rows_batch);
|
||||
}
|
||||
|
||||
ggml_tensor * sam_out = build_sam(inp_raw);
|
||||
|
||||
if (!is_overview) {
|
||||
n_batch = n_tiles_per_row;
|
||||
n_batch = n_tiles_per_row * n_rows_batch;
|
||||
}
|
||||
|
||||
const int clip_n_patches = sam_out->ne[0] * sam_out->ne[1];
|
||||
@@ -354,34 +359,36 @@ ggml_cgraph * clip_graph_deepseekocr::build() {
|
||||
const auto w = h;
|
||||
const auto n_dim = cur->ne[0];
|
||||
|
||||
ggml_tensor * imgnl = ggml_repeat_4d(ctx0, model.image_newline, n_dim, 1, h, 1);
|
||||
cur = ggml_reshape_3d(ctx0, cur, n_dim, w, h);
|
||||
cur = ggml_reshape_2d(ctx0, ggml_concat(ctx0, cur, imgnl, 1), n_dim, (w + 1) * h);
|
||||
cur = ggml_concat(ctx0, cur, model.view_seperator, 1); // (n_dim, h*(w+1) + 1)
|
||||
ggml_tensor * imgnl = ggml_repeat_4d(ctx0, model.image_newline, n_dim, 1, h, n_batch);
|
||||
cur = ggml_reshape_4d(ctx0, cur, n_dim, w, h, n_batch);
|
||||
cur = ggml_reshape_3d(ctx0, ggml_concat(ctx0, cur, imgnl, 1), n_dim, (w + 1) * h, n_batch);
|
||||
ggml_tensor * vs = ggml_repeat_4d(ctx0, model.view_seperator, n_dim, 1, n_batch, 1);
|
||||
cur = ggml_concat(ctx0, cur, vs, 1); // (n_dim, h*(w+1) + 1, n_batch)
|
||||
} else {
|
||||
// tile row: interleave tiles within each row, add newline per row
|
||||
const int grid_x = static_cast<int>(std::sqrt(static_cast<float>(clip_n_patches)));
|
||||
const int grid_y = grid_x;
|
||||
const auto n_dim = cur->ne[0];
|
||||
const int grid_x = static_cast<int>(std::sqrt(static_cast<float>(clip_n_patches)));
|
||||
const int grid_y = grid_x;
|
||||
const auto n_dim = cur->ne[0];
|
||||
|
||||
// (n_dim, clip_n_patches, n_batch) -> (n_dim, grid_x, grid_y, n_batch)
|
||||
cur = ggml_reshape_4d(ctx0, cur, n_dim, grid_x, grid_y, n_batch);
|
||||
// merge n_dim into the grid_x axis, freeing the 4th axis for n_rows_batch
|
||||
// (n_dim, clip_n_patches, n_tiles_per_row * n_rows_batch) -> (n_dim*grid_x, grid_y, n_tiles_per_row, n_rows_batch)
|
||||
cur = ggml_reshape_4d(ctx0, cur, n_dim * grid_x, grid_y, n_tiles_per_row, n_rows_batch);
|
||||
|
||||
// tiles: re-order from A.row0 A.row1 B.row0 B.row1 ...
|
||||
// to A.row0 B.row0 A.row1 B.row1 ...
|
||||
// then add nl: A.row0 B.row0 [nl] A.row1 B.row1 [nl] ...
|
||||
// interleave tiles: (n_dim, grid_x, grid_y, n_batch) -> (n_dim, grid_x, n_batch, grid_y)
|
||||
cur = ggml_cont(ctx0, ggml_permute(ctx0, cur, 0, 1, 3, 2));
|
||||
// interleave tiles: -> (n_dim*grid_x, n_tiles_per_row, grid_y, n_rows_batch)
|
||||
cur = ggml_cont(ctx0, ggml_permute(ctx0, cur, 0, 2, 1, 3));
|
||||
|
||||
// merge: (n_dim, grid_x, n_batch, grid_y) -> (n_dim, grid_x*n_batch, grid_y, 1)
|
||||
cur = ggml_reshape_4d(ctx0, cur, n_dim, grid_x * n_batch, grid_y, 1);
|
||||
// merge: -> (n_dim, grid_x*n_tiles_per_row, grid_y, n_rows_batch)
|
||||
cur = ggml_reshape_4d(ctx0, cur, n_dim, grid_x * n_tiles_per_row, grid_y, n_rows_batch);
|
||||
|
||||
// append newline per row: (n_dim, grid_x*n_batch+1, grid_y, 1)
|
||||
ggml_tensor * imgnl = ggml_repeat_4d(ctx0, model.image_newline, n_dim, 1, grid_y, 1);
|
||||
// append newline per row: (n_dim, grid_x*n_tiles_per_row+1, grid_y, n_rows_batch)
|
||||
ggml_tensor * imgnl = ggml_repeat_4d(ctx0, model.image_newline, n_dim, 1, grid_y, n_rows_batch);
|
||||
cur = ggml_concat(ctx0, cur, imgnl, 1);
|
||||
|
||||
// flatten: (n_dim, (grid_x*n_batch+1)*grid_y)
|
||||
cur = ggml_reshape_2d(ctx0, cur, n_dim, (grid_x * n_batch + 1) * grid_y);
|
||||
// flatten: (n_dim, (grid_x*n_tiles_per_row+1)*grid_y, n_rows_batch)
|
||||
cur = ggml_reshape_3d(ctx0, cur, n_dim, (grid_x * n_tiles_per_row + 1) * grid_y, n_rows_batch);
|
||||
}
|
||||
|
||||
cb(cur, "dsocr_output", -1);
|
||||
|
||||
@@ -14,8 +14,9 @@ ggml_cgraph * clip_graph_deepseekocr2::build() {
|
||||
{
|
||||
ggml_tensor * inp;
|
||||
|
||||
inp = ggml_reshape_2d(ctx0, sam_out, sam_out->ne[0] * sam_out->ne[1], sam_out->ne[2]); // H*W, C
|
||||
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 1, 0, 2, 3));
|
||||
// H*W, C, B
|
||||
inp = ggml_reshape_3d(ctx0, sam_out, sam_out->ne[0] * sam_out->ne[1], sam_out->ne[2], sam_out->ne[3]);
|
||||
inp = ggml_cont(ctx0, ggml_permute(ctx0, inp, 1, 0, 2, 3)); // C, H*W, B
|
||||
|
||||
auto num_image_tokens = inp->ne[1]; // H*W
|
||||
GGML_ASSERT(num_image_tokens == 144 || num_image_tokens == 256);
|
||||
@@ -32,8 +33,10 @@ ggml_cgraph * clip_graph_deepseekocr2::build() {
|
||||
num_queries = 144;
|
||||
}
|
||||
|
||||
// (B, num_image_tokens + num_queries, C)
|
||||
inp = ggml_concat(ctx0, inp, ggml_cast(ctx0, query_embed, inp->type), 1);
|
||||
// repeat the query embedding per batch item, then append: (C, num_image_tokens + num_queries, B)
|
||||
query_embed = ggml_cast(ctx0, query_embed, inp->type);
|
||||
query_embed = ggml_repeat_4d(ctx0, query_embed, query_embed->ne[0], num_queries, inp->ne[2], 1);
|
||||
inp = ggml_concat(ctx0, inp, query_embed, 1);
|
||||
|
||||
auto seq_len = inp->ne[1];
|
||||
|
||||
@@ -57,7 +60,7 @@ ggml_cgraph * clip_graph_deepseekocr2::build() {
|
||||
/* learned_pos_embd */ nullptr, add_rope, vit_opts);
|
||||
|
||||
cur = ggml_cont(ctx0,
|
||||
ggml_view_2d(ctx0, cur, cur->ne[0], num_queries, cur->nb[1],
|
||||
ggml_view_3d(ctx0, cur, cur->ne[0], num_queries, cur->ne[2], cur->nb[1], cur->nb[2],
|
||||
cur->nb[1] * (cur->ne[1] - num_queries))); // only take query tokens for output
|
||||
|
||||
ggml_build_forward_expand(gf, cur);
|
||||
@@ -71,7 +74,8 @@ ggml_cgraph * clip_graph_deepseekocr2::build() {
|
||||
|
||||
// view_seperator only after the global view
|
||||
if (img.add_viewsep) {
|
||||
cur = ggml_concat(ctx0, cur, model.view_seperator, 1); // (n_dim, 257)
|
||||
ggml_tensor * vs = ggml_repeat_4d(ctx0, model.view_seperator, model.view_seperator->ne[0], 1, cur->ne[2], 1);
|
||||
cur = ggml_concat(ctx0, cur, vs, 1); // (n_dim, 257, n_batch)
|
||||
}
|
||||
|
||||
cb(cur, "dsocr2_output", -1);
|
||||
|
||||
@@ -138,12 +138,13 @@ struct clip_graph_deepseekocr : clip_graph {
|
||||
clip_graph_deepseekocr(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph(ctx, img) {}
|
||||
ggml_cgraph * build() override;
|
||||
ggml_tensor * build_sam(ggml_tensor * inp); // build the SAM model
|
||||
// bool support_batch() const override { return true; } // TODO: support batch for DeepSeek-OCR v1
|
||||
bool support_batch() const override { return true; }
|
||||
};
|
||||
|
||||
struct clip_graph_deepseekocr2 : clip_graph_deepseekocr {
|
||||
clip_graph_deepseekocr2(clip_ctx * ctx, const clip_image_f32 & img) : clip_graph_deepseekocr(ctx, img) {}
|
||||
ggml_cgraph * build() override; // reuses build_sam() from base
|
||||
bool support_batch() const override { return true; }
|
||||
};
|
||||
|
||||
struct clip_graph_conformer : clip_graph {
|
||||
|
||||
@@ -22,8 +22,123 @@
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <climits>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
// remember to bump this if the serialization format changes
|
||||
#define MTMD_SERIALIZATION_VERSION 1
|
||||
|
||||
struct mtmd_serialization {
|
||||
// note: using 64-bit here for future-proofing
|
||||
uint64_t version = MTMD_SERIALIZATION_VERSION;
|
||||
std::vector<char> data;
|
||||
size_t read_pos = 0; // cursor used when reading
|
||||
|
||||
// for writing
|
||||
mtmd_serialization(uint64_t version) : version(version) {
|
||||
write(version);
|
||||
}
|
||||
|
||||
// for reading
|
||||
mtmd_serialization(uint64_t version, const char * buf, size_t len) {
|
||||
// copy buf to data
|
||||
data.assign(buf, buf + len);
|
||||
uint64_t ver_in = read<uint64_t>();
|
||||
if (ver_in != version) {
|
||||
throw std::runtime_error("version mismatch");
|
||||
}
|
||||
this->version = ver_in;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void write(T value) {
|
||||
static_assert(std::is_trivially_copyable<T>::value && !std::is_same<T, bool>::value,
|
||||
"T must be trivially copyable and not bool");
|
||||
const char * p = reinterpret_cast<const char *>(&value);
|
||||
data.insert(data.end(), p, p + sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T read() {
|
||||
static_assert(std::is_trivially_copyable<T>::value && !std::is_same<T, bool>::value,
|
||||
"T must be trivially copyable and not bool");
|
||||
if (read_pos + sizeof(T) > data.size()) {
|
||||
throw std::runtime_error("read OOB");
|
||||
}
|
||||
T value;
|
||||
std::memcpy(&value, data.data() + read_pos, sizeof(T));
|
||||
read_pos += sizeof(T);
|
||||
return value;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
template <>
|
||||
void mtmd_serialization::write<bool>(bool value) {
|
||||
write<uint8_t>(value ? 1 : 0);
|
||||
}
|
||||
template <>
|
||||
bool mtmd_serialization::read<bool>() {
|
||||
return read<uint8_t>() != 0;
|
||||
}
|
||||
|
||||
template <>
|
||||
void mtmd_serialization::write<std::string>(std::string value) {
|
||||
write<uint64_t>(value.size());
|
||||
data.insert(data.end(), value.begin(), value.end());
|
||||
}
|
||||
template <>
|
||||
std::string mtmd_serialization::read<std::string>() {
|
||||
uint64_t len = read<uint64_t>();
|
||||
if (read_pos + len > data.size()) {
|
||||
throw std::runtime_error("read_string OOB");
|
||||
}
|
||||
std::string str(data.data() + read_pos, len);
|
||||
read_pos += len;
|
||||
return str;
|
||||
}
|
||||
|
||||
// only mtmd.cpp needs these, so they're implemented here rather than in clip-impl.h
|
||||
void clip_image_f32::serialize(mtmd_serialization & ser) const {
|
||||
// remember to bump MTMD_SERIALIZATION_VERSION if this is changed
|
||||
// note: buf is intentionally NOT serialized; the loaded clip_image_f32 will always be a placeholder
|
||||
ser.write(add_viewsep);
|
||||
ser.write(add_newline);
|
||||
ser.write((int32_t)nx_);
|
||||
ser.write((int32_t)ny_);
|
||||
}
|
||||
void clip_image_f32::deserialize(mtmd_serialization & ser) {
|
||||
add_viewsep = ser.read<bool>();
|
||||
add_newline = ser.read<bool>();
|
||||
nx_ = ser.read<int32_t>();
|
||||
ny_ = ser.read<int32_t>();
|
||||
buf.clear(); // always a placeholder after loading
|
||||
}
|
||||
|
||||
void clip_image_f32_batch::serialize(mtmd_serialization & ser) const {
|
||||
// remember to bump MTMD_SERIALIZATION_VERSION if this is changed
|
||||
ser.write(is_audio);
|
||||
ser.write<uint64_t>(entries.size());
|
||||
for (const auto & entry : entries) {
|
||||
entry.serialize(ser);
|
||||
}
|
||||
}
|
||||
void clip_image_f32_batch::deserialize(mtmd_serialization & ser) {
|
||||
is_audio = ser.read<bool>();
|
||||
uint64_t n = ser.read<uint64_t>();
|
||||
constexpr size_t min_entry_bytes = sizeof(uint8_t) * 2 + sizeof(int32_t) * 2;
|
||||
if (n > (ser.data.size() - ser.read_pos) / min_entry_bytes) {
|
||||
throw std::runtime_error("entries count exceeds buffer size");
|
||||
}
|
||||
entries.clear();
|
||||
entries.reserve(n);
|
||||
for (uint64_t i = 0; i < n; i++) {
|
||||
clip_image_f32 entry;
|
||||
entry.deserialize(ser);
|
||||
entries.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
|
||||
// for still image data, layout is RGBRGBRGB...
|
||||
// length of data must be nx * ny * 3 bytes
|
||||
//
|
||||
@@ -83,6 +198,7 @@ enum mtmd_pos_type {
|
||||
MTMD_POS_TYPE_NORMAL, // number of positions equals to number of tokens
|
||||
MTMD_POS_TYPE_MROPE, // qwen-vl mrope style, each image takes max(t,h,w) position indexes
|
||||
MTMD_POS_TYPE_HUNYUANVL, // HunyuanVL mrope + BOI/EOI/newline layout with XD-RoPE dim-3
|
||||
MTMD_POS_TYPE_COUNT, // for validation
|
||||
};
|
||||
|
||||
struct mtmd_image_tokens {
|
||||
@@ -136,6 +252,30 @@ struct mtmd_image_tokens {
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
void serialize(mtmd_serialization & ser) const {
|
||||
// remember to bump MTMD_SERIALIZATION_VERSION if this is changed
|
||||
ser.write(nx);
|
||||
ser.write(ny);
|
||||
ser.write((uint32_t)pos);
|
||||
ser.write(image_idx);
|
||||
ser.write(n_temporal_merge);
|
||||
ser.write(id);
|
||||
batch_f32.serialize(ser);
|
||||
}
|
||||
void deserialize(mtmd_serialization & ser) {
|
||||
nx = ser.read<uint32_t>();
|
||||
ny = ser.read<uint32_t>();
|
||||
uint32_t pos_raw = ser.read<uint32_t>();
|
||||
if (pos_raw >= MTMD_POS_TYPE_COUNT) {
|
||||
throw std::runtime_error("invalid pos type");
|
||||
}
|
||||
pos = (mtmd_pos_type)pos_raw;
|
||||
image_idx = ser.read<uint32_t>();
|
||||
n_temporal_merge = ser.read<uint32_t>();
|
||||
id = ser.read<std::string>();
|
||||
batch_f32.deserialize(ser);
|
||||
}
|
||||
};
|
||||
using mtmd_image_tokens_ptr = std::unique_ptr<mtmd_image_tokens>;
|
||||
|
||||
@@ -161,6 +301,18 @@ struct mtmd_audio_tokens {
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
void serialize(mtmd_serialization & ser) const {
|
||||
// remember to bump MTMD_SERIALIZATION_VERSION if this is changed
|
||||
ser.write(n_tokens);
|
||||
ser.write(id);
|
||||
batch_f32.serialize(ser);
|
||||
}
|
||||
void deserialize(mtmd_serialization & ser) {
|
||||
n_tokens = ser.read<uint32_t>();
|
||||
id = ser.read<std::string>();
|
||||
batch_f32.deserialize(ser);
|
||||
}
|
||||
};
|
||||
using mtmd_audio_tokens_ptr = std::unique_ptr<mtmd_audio_tokens>;
|
||||
|
||||
@@ -192,6 +344,66 @@ struct mtmd_input_chunk {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void serialize(mtmd_serialization & ser) const {
|
||||
// remember to bump MTMD_SERIALIZATION_VERSION if this is changed
|
||||
ser.write((uint32_t)type);
|
||||
|
||||
ser.write<uint64_t>(tokens_text.size());
|
||||
for (llama_token tok : tokens_text) {
|
||||
ser.write((int32_t)tok);
|
||||
}
|
||||
|
||||
ser.write(tokens_image != nullptr);
|
||||
if (tokens_image) {
|
||||
tokens_image->serialize(ser);
|
||||
}
|
||||
|
||||
ser.write(tokens_audio != nullptr);
|
||||
if (tokens_audio) {
|
||||
tokens_audio->serialize(ser);
|
||||
}
|
||||
}
|
||||
void deserialize(mtmd_serialization & ser) {
|
||||
uint32_t type_raw = ser.read<uint32_t>();
|
||||
if (type_raw >= MTMD_INPUT_CHUNK_TYPE_COUNT) {
|
||||
throw std::runtime_error("invalid chunk type");
|
||||
}
|
||||
type = (mtmd_input_chunk_type)type_raw;
|
||||
|
||||
uint64_t n_tokens_text = ser.read<uint64_t>();
|
||||
// reject before resize() so a tiny corrupted/malicious buffer can't force a huge allocation
|
||||
if (n_tokens_text > (ser.data.size() - ser.read_pos) / sizeof(int32_t)) {
|
||||
throw std::runtime_error("tokens_text length exceeds buffer size");
|
||||
}
|
||||
tokens_text.resize(n_tokens_text);
|
||||
for (uint64_t i = 0; i < n_tokens_text; i++) {
|
||||
tokens_text[i] = (llama_token)ser.read<int32_t>();
|
||||
}
|
||||
|
||||
if (ser.read<bool>()) {
|
||||
tokens_image = std::make_unique<mtmd_image_tokens>();
|
||||
tokens_image->deserialize(ser);
|
||||
} else {
|
||||
tokens_image.reset();
|
||||
}
|
||||
|
||||
if (ser.read<bool>()) {
|
||||
tokens_audio = std::make_unique<mtmd_audio_tokens>();
|
||||
tokens_audio->deserialize(ser);
|
||||
} else {
|
||||
tokens_audio.reset();
|
||||
}
|
||||
|
||||
// catch buffers where the declared type doesn't match which payload is actually present,
|
||||
// so a mismatched chunk can't slip through and null-deref/abort later in an accessor
|
||||
if (type == MTMD_INPUT_CHUNK_TYPE_IMAGE && !tokens_image) {
|
||||
throw std::runtime_error("type is IMAGE but tokens_image is missing");
|
||||
}
|
||||
if (type == MTMD_INPUT_CHUNK_TYPE_AUDIO && !tokens_audio) {
|
||||
throw std::runtime_error("type is AUDIO but tokens_audio is missing");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct mtmd_input_chunks {
|
||||
@@ -2043,6 +2255,42 @@ void mtmd_input_chunk_free(mtmd_input_chunk * chunk) {
|
||||
}
|
||||
}
|
||||
|
||||
int32_t mtmd_input_chunk_save(const mtmd_input_chunk * chunk, char * out_buf, size_t out_len, size_t * expected_out_len) {
|
||||
try {
|
||||
mtmd_serialization ser(MTMD_SERIALIZATION_VERSION);
|
||||
chunk->serialize(ser);
|
||||
|
||||
if (expected_out_len) {
|
||||
*expected_out_len = ser.data.size();
|
||||
}
|
||||
if (!out_buf) {
|
||||
// caller is only querying the required size
|
||||
return 0;
|
||||
}
|
||||
if (out_len < ser.data.size()) {
|
||||
LOG_ERR("%s: out_buf is too small, need %zu bytes, got %zu\n", __func__, ser.data.size(), out_len);
|
||||
return -1;
|
||||
}
|
||||
std::memcpy(out_buf, ser.data.data(), ser.data.size());
|
||||
return 0;
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("%s: %s\n", __func__, e.what());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
mtmd_input_chunk * mtmd_input_chunk_load(const char * buf, size_t len) {
|
||||
try {
|
||||
mtmd_serialization ser(MTMD_SERIALIZATION_VERSION, buf, len);
|
||||
mtmd::input_chunk_ptr chunk(new mtmd_input_chunk());
|
||||
chunk->deserialize(ser);
|
||||
return chunk.release();
|
||||
} catch (const std::exception & e) {
|
||||
LOG_ERR("%s: %s\n", __func__, e.what());
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// mtmd_image_tokens
|
||||
|
||||
size_t mtmd_image_tokens_get_n_tokens(const mtmd_image_tokens * image_tokens) {
|
||||
|
||||
@@ -55,6 +55,7 @@ enum mtmd_input_chunk_type {
|
||||
MTMD_INPUT_CHUNK_TYPE_TEXT,
|
||||
MTMD_INPUT_CHUNK_TYPE_IMAGE,
|
||||
MTMD_INPUT_CHUNK_TYPE_AUDIO,
|
||||
MTMD_INPUT_CHUNK_TYPE_COUNT, // for validation
|
||||
};
|
||||
|
||||
// opaque types
|
||||
@@ -232,6 +233,15 @@ MTMD_API llama_pos mtmd_input_chunk_get_n_pos (const mtmd
|
||||
MTMD_API mtmd_input_chunk * mtmd_input_chunk_copy(const mtmd_input_chunk * chunk);
|
||||
MTMD_API void mtmd_input_chunk_free(mtmd_input_chunk * chunk);
|
||||
|
||||
// save/load an input chunk to/from a buffer (useful for KV save/load)
|
||||
// important: only chunk's metadata will be saved, the actual image/audio data will not be saved
|
||||
// the loaded chunk will always be a placeholder, cannot be used for mtmd_encode() or mtmd_batch_encode()
|
||||
// out_buf can be nullptr (to query expected_out_len)
|
||||
// returns 0 on success, non-zero on failure
|
||||
MTMD_API int32_t mtmd_input_chunk_save(const mtmd_input_chunk * chunk, char * out_buf, size_t out_len, size_t * expected_out_len);
|
||||
// returns nullptr on failure
|
||||
MTMD_API mtmd_input_chunk * mtmd_input_chunk_load(const char * buf, size_t len);
|
||||
|
||||
|
||||
// mtmd_image_tokens
|
||||
//
|
||||
|
||||
@@ -1076,6 +1076,10 @@ In *router mode* the query param `?model={model_id}` has to be set. This endpoin
|
||||
| `llamacpp:n_tokens_max` | Counter | High watermark of the context size observed. |
|
||||
| `llamacpp:n_decode_total` | Counter | Total Number of llama_decode() calls. |
|
||||
| `llamacpp:n_busy_slots_per_decode` | Gauge | Average number of busy slots per llama_decode() call. |
|
||||
| `llamacpp:spec_decode_num_draft_tokens_total` | Counter | Total draft tokens generated (0 when spec-decode is off). |
|
||||
| `llamacpp:spec_decode_num_accepted_tokens_total` | Counter | Total draft tokens accepted by the target model (0 when spec-decode is off). |
|
||||
| `llamacpp:spec_decode_num_drafts_total` | Counter | Total speculative decoding verification steps (0 when spec-decode is off). |
|
||||
| `llamacpp:spec_decode_num_accepted_tokens_per_pos_total` | Counter | Accepted tokens per draft position (labeled `position="N"`; absent when spec-decode is off or before the first completed speculative request). |
|
||||
|
||||
### POST `/slots/{id_slot}?action=save`: Save the prompt cache of the specified slot to a file.
|
||||
|
||||
|
||||
@@ -845,6 +845,11 @@ struct server_metrics {
|
||||
uint64_t n_decode_total = 0;
|
||||
uint64_t n_busy_slots_total = 0;
|
||||
|
||||
uint64_t n_draft_tokens_total = 0;
|
||||
uint64_t n_draft_accepted_total = 0;
|
||||
uint64_t n_draft_verif_steps_total = 0;
|
||||
std::vector<uint64_t> n_accepted_per_pos_total;
|
||||
|
||||
void init() {
|
||||
t_start = ggml_time_us();
|
||||
}
|
||||
@@ -863,6 +868,17 @@ struct server_metrics {
|
||||
n_tokens_predicted += slot.n_decoded;
|
||||
t_tokens_generation += slot.t_token_generation;
|
||||
t_tokens_generation_total += slot.t_token_generation;
|
||||
|
||||
n_draft_tokens_total += slot.n_draft_total;
|
||||
n_draft_accepted_total += slot.n_draft_accepted;
|
||||
n_draft_verif_steps_total += slot.n_draft_verif_steps;
|
||||
|
||||
if (n_accepted_per_pos_total.size() < slot.n_accepted_per_pos.size()) {
|
||||
n_accepted_per_pos_total.resize(slot.n_accepted_per_pos.size(), 0);
|
||||
}
|
||||
for (size_t i = 0; i < slot.n_accepted_per_pos.size(); i++) {
|
||||
n_accepted_per_pos_total[i] += slot.n_accepted_per_pos[i];
|
||||
}
|
||||
}
|
||||
|
||||
void on_decoded(const std::vector<server_slot> & slots) {
|
||||
@@ -2552,6 +2568,11 @@ private:
|
||||
res->n_decode_total = metrics.n_decode_total;
|
||||
res->n_busy_slots_total = metrics.n_busy_slots_total;
|
||||
|
||||
res->n_draft_tokens_total = metrics.n_draft_tokens_total;
|
||||
res->n_draft_accepted_total = metrics.n_draft_accepted_total;
|
||||
res->n_draft_verif_steps_total = metrics.n_draft_verif_steps_total;
|
||||
res->n_accepted_per_pos_total = metrics.n_accepted_per_pos_total;
|
||||
|
||||
if (task.metrics_reset_bucket) {
|
||||
metrics.reset_bucket();
|
||||
}
|
||||
@@ -4440,6 +4461,18 @@ void server_routes::init_routes() {
|
||||
{"name", "n_tokens_max"},
|
||||
{"help", "Largest observed n_tokens."},
|
||||
{"value", res_task->n_tokens_max}
|
||||
}, {
|
||||
{"name", "spec_decode_num_draft_tokens_total"},
|
||||
{"help", "Total draft tokens generated"},
|
||||
{"value", res_task->n_draft_tokens_total}
|
||||
}, {
|
||||
{"name", "spec_decode_num_accepted_tokens_total"},
|
||||
{"help", "Total draft tokens accepted by the target model"},
|
||||
{"value", res_task->n_draft_accepted_total}
|
||||
}, {
|
||||
{"name", "spec_decode_num_drafts_total"},
|
||||
{"help", "Total speculative decoding verification steps"},
|
||||
{"value", res_task->n_draft_verif_steps_total}
|
||||
}}},
|
||||
{"gauge", {{
|
||||
{"name", "prompt_tokens_seconds"},
|
||||
@@ -4481,6 +4514,17 @@ void server_routes::init_routes() {
|
||||
}
|
||||
}
|
||||
|
||||
// labeled counter: one time series per draft position
|
||||
if (!res_task->n_accepted_per_pos_total.empty()) {
|
||||
prometheus << "# HELP llamacpp:spec_decode_num_accepted_tokens_per_pos_total"
|
||||
" Accepted tokens per draft position\n"
|
||||
<< "# TYPE llamacpp:spec_decode_num_accepted_tokens_per_pos_total counter\n";
|
||||
for (size_t i = 0; i < res_task->n_accepted_per_pos_total.size(); i++) {
|
||||
prometheus << "llamacpp:spec_decode_num_accepted_tokens_per_pos_total{position=\""
|
||||
<< i << "\"} " << res_task->n_accepted_per_pos_total[i] << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
res->headers["Process-Start-Time-Unix"] = std::to_string(res_task->t_start);
|
||||
res->content_type = "text/plain; version=0.0.4";
|
||||
res->status = 200;
|
||||
|
||||
@@ -2079,9 +2079,8 @@ server_http_proxy::server_http_proxy(
|
||||
return has_next; // false if EOF or pipe broken
|
||||
};
|
||||
|
||||
// wire up the HTTP client
|
||||
// note: do NOT capture `this` pointer, as it may be destroyed before the thread ends
|
||||
httplib::ResponseHandler response_handler = [pipe, cli](const httplib::Response & response) {
|
||||
// build the header message forwarded to the reader thread, stripping internal proxy headers
|
||||
auto make_header_msg = [](const httplib::Response & response) {
|
||||
msg_t msg;
|
||||
msg.status = response.status;
|
||||
for (const auto & [key, value] : response.headers) {
|
||||
@@ -2095,7 +2094,17 @@ server_http_proxy::server_http_proxy(
|
||||
}
|
||||
msg.headers[key] = value;
|
||||
}
|
||||
return pipe->write(std::move(msg)); // send headers first
|
||||
return msg;
|
||||
};
|
||||
|
||||
// true once response_handler has already forwarded the headers
|
||||
auto headers_sent = std::make_shared<std::atomic<bool>>(false);
|
||||
|
||||
// wire up the HTTP client
|
||||
// note: do NOT capture `this` pointer, as it may be destroyed before the thread ends
|
||||
httplib::ResponseHandler response_handler = [pipe, headers_sent, make_header_msg](const httplib::Response & response) {
|
||||
headers_sent->store(true);
|
||||
return pipe->write(make_header_msg(response)); // send headers first
|
||||
};
|
||||
httplib::ContentReceiverWithProgress content_receiver = [pipe](const char * data, size_t data_length, size_t, size_t) {
|
||||
// send data chunks
|
||||
@@ -2169,13 +2178,16 @@ server_http_proxy::server_http_proxy(
|
||||
|
||||
// start the proxy thread
|
||||
SRV_DBG("start proxy thread %s %s\n", req.method.c_str(), req.path.c_str());
|
||||
this->thread = std::thread([cli, pipe, req]() {
|
||||
this->thread = std::thread([cli, pipe, req, headers_sent, make_header_msg]() {
|
||||
auto result = cli->send(std::move(req));
|
||||
if (result.error() != httplib::Error::Success) {
|
||||
auto err_str = httplib::to_string(result.error());
|
||||
SRV_ERR("http client error: %s\n", err_str.c_str());
|
||||
pipe->write({{}, 500, "", ""}); // header
|
||||
pipe->write({{}, 0, "proxy error: " + err_str, ""}); // body
|
||||
} else if (!headers_sent->load()) {
|
||||
// httplib skips response_handler for bodyless statuses like 204, send headers here instead
|
||||
pipe->write(make_header_msg(*result));
|
||||
}
|
||||
pipe->close_write(); // signal EOF to reader
|
||||
SRV_DBG("%s", "client request thread ended\n");
|
||||
|
||||
@@ -1560,6 +1560,11 @@ json server_task_result_metrics::to_json() {
|
||||
{ "n_decode_total", n_decode_total },
|
||||
{ "n_busy_slots_total", n_busy_slots_total },
|
||||
|
||||
{ "n_draft_tokens_total", n_draft_tokens_total },
|
||||
{ "n_draft_accepted_total", n_draft_accepted_total },
|
||||
{ "n_draft_verif_steps_total", n_draft_verif_steps_total },
|
||||
{ "n_accepted_per_pos_total", n_accepted_per_pos_total },
|
||||
|
||||
{ "slots", slots_data },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -532,6 +532,11 @@ struct server_task_result_metrics : server_task_result {
|
||||
uint64_t n_decode_total = 0;
|
||||
uint64_t n_busy_slots_total = 0;
|
||||
|
||||
uint64_t n_draft_tokens_total = 0;
|
||||
uint64_t n_draft_accepted_total = 0;
|
||||
uint64_t n_draft_verif_steps_total = 0;
|
||||
std::vector<uint64_t> n_accepted_per_pos_total;
|
||||
|
||||
// while we can also use std::vector<server_slot> this requires copying the slot object which can be quite messy
|
||||
// therefore, we use json to temporarily store the slot.to_json() result
|
||||
json slots_data = json::array();
|
||||
|
||||
+154
-93
@@ -30,20 +30,14 @@ namespace fs = std::filesystem;
|
||||
// internal helpers
|
||||
//
|
||||
|
||||
#if defined(_WIN32)
|
||||
// A chunk can end in the middle of a multi-byte sequence, so the incomplete
|
||||
// tail is dropped before validating what precedes it.
|
||||
static bool is_utf8_text(const std::string & text) {
|
||||
return is_valid_utf8(text.substr(0, validate_utf8(text)));
|
||||
}
|
||||
|
||||
// A child process writes its output in the OEM code page, which is not UTF-8
|
||||
// on a western Windows install, so accented text reaches the JSON layer as
|
||||
// invalid bytes and is replaced there. Text that already decodes as UTF-8 is
|
||||
// returned untouched, so a child that emits UTF-8 is never decoded twice.
|
||||
// run() spawns without a console, so the console code page does not apply.
|
||||
// a child process writes in the OEM code page, so accented output would reach
|
||||
// the JSON layer as invalid bytes. run() spawns without a console, so the
|
||||
// console code page never applies
|
||||
static std::string console_output_to_utf8(const std::string & text) {
|
||||
if (text.empty() || is_utf8_text(text)) {
|
||||
#if defined(_WIN32)
|
||||
// a chunk can end mid sequence, so the incomplete tail is dropped first
|
||||
if (text.empty() || is_valid_utf8(text.substr(0, validate_utf8(text)))) {
|
||||
// never decode twice a child that already emits UTF-8
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -64,12 +58,10 @@ static std::string console_output_to_utf8(const std::string & text) {
|
||||
std::string utf8(utf8_len, '\0');
|
||||
WideCharToMultiByte(CP_UTF8, 0, wide.data(), wide_len, utf8.data(), utf8_len, nullptr, nullptr);
|
||||
return utf8;
|
||||
}
|
||||
#else
|
||||
static std::string console_output_to_utf8(const std::string & text) {
|
||||
return text;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
json server_tool::to_json() const {
|
||||
return {
|
||||
@@ -94,14 +86,30 @@ enum class list_kind {
|
||||
all, // both
|
||||
};
|
||||
|
||||
// a narrow path uses the active code page on Windows, so every crossing between
|
||||
// a std::string (always UTF-8 here) and fs::path is converted explicitly
|
||||
static fs::path path_from_utf8(const std::string & s) {
|
||||
return fs::u8path(s);
|
||||
}
|
||||
|
||||
// '/' separators on every platform: Windows accepts them, the web UI needs them
|
||||
static std::string path_to_utf8(const fs::path & p) {
|
||||
const auto s = p.generic_u8string();
|
||||
return std::string(s.begin(), s.end());
|
||||
}
|
||||
|
||||
// home directory, read once at first use (getenv is not thread safe against setenv)
|
||||
static const std::string & home_dir() {
|
||||
static const std::string home = [] {
|
||||
const char * h = getenv("HOME");
|
||||
#ifdef _WIN32
|
||||
if (h == nullptr) h = getenv("USERPROFILE");
|
||||
#endif
|
||||
// the narrow getenv would return the profile path in the active code page
|
||||
const wchar_t * w = _wgetenv(L"HOME");
|
||||
if (w == nullptr) w = _wgetenv(L"USERPROFILE");
|
||||
return w ? path_to_utf8(fs::path(w)) : std::string();
|
||||
#else
|
||||
const char * h = getenv("HOME");
|
||||
return h ? std::string(h) : std::string();
|
||||
#endif
|
||||
}();
|
||||
return home;
|
||||
}
|
||||
@@ -140,11 +148,14 @@ public:
|
||||
std::string rel; // '/'-separated, relative to `base`
|
||||
bool is_dir = false;
|
||||
};
|
||||
// entries relative to `base`; sets `err` if `base` isn't a directory
|
||||
struct list_result {
|
||||
std::vector<list_entry> entries;
|
||||
std::string err; // set when `base` is not a directory
|
||||
bool truncated = false; // set when the walk could not see everything
|
||||
};
|
||||
// entries relative to `base`, which must already be resolved (absolute)
|
||||
// max_depth == 0 means unlimited, 1 means direct children of `base` only
|
||||
// `base` must already be resolved (absolute); `caller_path` is the path the
|
||||
// caller passed, used only for error messages
|
||||
virtual std::vector<list_entry> list_entries(const std::string & base, const std::string & caller_path, int max_depth, list_kind kind, std::string & err, bool & truncated) const = 0;
|
||||
virtual list_result list_entries(const std::string & base, int max_depth, list_kind kind) const = 0;
|
||||
// on_chunk, if set, is called with each chunk of output as it is read (before truncation cuts in);
|
||||
// returning false terminates the process early (e.g. the client disconnected)
|
||||
virtual exec_result run(
|
||||
@@ -162,37 +173,47 @@ public:
|
||||
// expands a leading `~`, then resolves `path` against `cwd` (or the server
|
||||
// working directory when `cwd` is unset); the result is always absolute
|
||||
std::string resolve(const std::string & path) const override {
|
||||
std::string p = expand_home(path);
|
||||
if (fs::path(p).is_absolute()) {
|
||||
return p;
|
||||
const std::string p = expand_home(path);
|
||||
|
||||
fs::path full = path_from_utf8(p);
|
||||
if (!full.is_absolute()) {
|
||||
if (cwd.empty()) {
|
||||
std::error_code ec;
|
||||
const fs::path cur = fs::current_path(ec);
|
||||
if (ec) return p;
|
||||
full = cur / full;
|
||||
} else {
|
||||
full = path_from_utf8(cwd) / full;
|
||||
}
|
||||
}
|
||||
if (cwd.empty()) {
|
||||
std::error_code ec;
|
||||
fs::path cur = fs::current_path(ec);
|
||||
if (ec) return p;
|
||||
return (cur / p).string();
|
||||
|
||||
// drop "." and ".." so they never reach git or the client
|
||||
full = full.lexically_normal();
|
||||
// a trailing ".." normalizes to a path that ends with a separator
|
||||
if (!full.has_filename() && full != full.root_path()) {
|
||||
full = full.parent_path();
|
||||
}
|
||||
return (fs::path(cwd) / p).string();
|
||||
return path_to_utf8(full);
|
||||
}
|
||||
|
||||
bool is_directory(const std::string & path) const override {
|
||||
std::error_code ec;
|
||||
return fs::is_directory(resolve(path), ec) && !ec;
|
||||
return fs::is_directory(path_from_utf8(resolve(path)), ec) && !ec;
|
||||
}
|
||||
|
||||
bool is_regular_file(const std::string & path) const override {
|
||||
std::error_code ec;
|
||||
return fs::is_regular_file(resolve(path), ec) && !ec;
|
||||
return fs::is_regular_file(path_from_utf8(resolve(path)), ec) && !ec;
|
||||
}
|
||||
|
||||
bool file_size(const std::string & path, uintmax_t & out_size) const override {
|
||||
std::error_code ec;
|
||||
out_size = fs::file_size(resolve(path), ec);
|
||||
out_size = fs::file_size(path_from_utf8(resolve(path)), ec);
|
||||
return !ec;
|
||||
}
|
||||
|
||||
bool read_file(const std::string & path, std::string & out) const override {
|
||||
std::ifstream f(resolve(path), std::ios::binary);
|
||||
std::ifstream f(path_from_utf8(resolve(path)), std::ios::binary);
|
||||
if (!f) return false;
|
||||
std::ostringstream ss;
|
||||
ss << f.rdbuf();
|
||||
@@ -202,7 +223,7 @@ public:
|
||||
|
||||
bool write_file(const std::string & path, const std::string & content) const override {
|
||||
std::error_code ec;
|
||||
fs::path fpath(resolve(path));
|
||||
fs::path fpath = path_from_utf8(resolve(path));
|
||||
if (fpath.has_parent_path()) {
|
||||
fs::create_directories(fpath.parent_path(), ec);
|
||||
if (ec) return false;
|
||||
@@ -213,13 +234,13 @@ public:
|
||||
return (bool) f;
|
||||
}
|
||||
|
||||
std::vector<list_entry> list_entries(const std::string & base, const std::string & caller_path, int max_depth, list_kind kind, std::string & err, bool & truncated) const override {
|
||||
err.clear();
|
||||
truncated = false;
|
||||
list_result list_entries(const std::string & base, int max_depth, list_kind kind) const override {
|
||||
list_result out;
|
||||
|
||||
std::error_code ec;
|
||||
if (!fs::is_directory(base, ec) || ec) {
|
||||
err = "path does not exist or is not a directory: " + caller_path;
|
||||
return {};
|
||||
out.err = "path does not exist or is not a directory";
|
||||
return out;
|
||||
}
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(SERVER_TOOL_LIST_ENTRIES_TIMEOUT);
|
||||
@@ -231,7 +252,6 @@ public:
|
||||
SERVER_TOOL_GIT_LS_FILES_MAX_OUTPUT, SERVER_TOOL_LIST_ENTRIES_TIMEOUT);
|
||||
|
||||
if (res.exit_code == 0 && !res.timed_out) {
|
||||
std::vector<list_entry> result;
|
||||
std::istringstream iss(res.output);
|
||||
std::string line;
|
||||
while (std::getline(iss, line)) {
|
||||
@@ -239,15 +259,16 @@ public:
|
||||
if (line.empty()) continue;
|
||||
std::replace(line.begin(), line.end(), '\\', '/');
|
||||
if (max_depth > 0 && entry_depth(line) > max_depth) continue;
|
||||
if (is_regular_file((fs::path(base) / line).string())) {
|
||||
result.push_back({line, false});
|
||||
if (is_regular_file(path_to_utf8(path_from_utf8(base) / path_from_utf8(line)))) {
|
||||
out.entries.push_back({line, false});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
return list_entries_fallback(base, max_depth, kind, deadline, truncated);
|
||||
out.entries = list_entries_fallback(base, max_depth, kind, deadline, out.truncated);
|
||||
return out;
|
||||
}
|
||||
|
||||
exec_result run(
|
||||
@@ -326,6 +347,42 @@ public:
|
||||
private:
|
||||
std::string cwd;
|
||||
|
||||
// a link can point back to an ancestor and loop forever, so it is never walked
|
||||
static bool is_link(const fs::directory_entry & entry) {
|
||||
std::error_code ec;
|
||||
if (entry.is_symlink(ec) || ec) {
|
||||
return true;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
// a junction looks like a plain directory to std::filesystem, so read the reparse tag
|
||||
WIN32_FIND_DATAW data;
|
||||
const HANDLE h = FindFirstFileW(entry.path().c_str(), &data);
|
||||
if (h == INVALID_HANDLE_VALUE) {
|
||||
return false;
|
||||
}
|
||||
FindClose(h);
|
||||
if ((data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) {
|
||||
return false;
|
||||
}
|
||||
// other reparse points (cloud placeholder, dedup stub) are real directories
|
||||
return data.dwReserved0 == IO_REPARSE_TAG_SYMLINK || data.dwReserved0 == IO_REPARSE_TAG_MOUNT_POINT;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// NTFS is case insensitive, so Build and build are the same directory
|
||||
static std::string get_effective_name(const std::string & fname) {
|
||||
#if defined(_WIN32)
|
||||
std::string lowered = fname;
|
||||
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
|
||||
[](unsigned char c) { return (char) std::tolower(c); });
|
||||
return lowered;
|
||||
#else
|
||||
return fname;
|
||||
#endif
|
||||
}
|
||||
|
||||
static const std::unordered_set<std::string> & junk_dir_names() {
|
||||
static const std::unordered_set<std::string> names = {
|
||||
".git", ".svn", ".hg", "node_modules", "__pycache__",
|
||||
@@ -337,46 +394,53 @@ private:
|
||||
std::vector<list_entry> list_entries_fallback(const std::string & base, int max_depth, list_kind kind,
|
||||
std::chrono::steady_clock::time_point deadline, bool & truncated) const {
|
||||
std::vector<list_entry> result;
|
||||
std::error_code ec;
|
||||
|
||||
std::vector<std::tuple<fs::path, fs::path, int>> stack;
|
||||
stack.emplace_back(fs::path(base), fs::path(), 0);
|
||||
stack.emplace_back(path_from_utf8(base), fs::path(), 0);
|
||||
|
||||
while (!stack.empty()) {
|
||||
auto [dir, rel_dir, depth] = stack.back();
|
||||
if (std::chrono::steady_clock::now() >= deadline) {
|
||||
truncated = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
auto [dir, rel_dir, depth] = std::move(stack.back());
|
||||
stack.pop_back();
|
||||
|
||||
// the throwing increment would escape the tool on a directory that
|
||||
// goes away mid walk, so step the iterator explicitly
|
||||
std::error_code ec;
|
||||
// step the iterator by hand: the throwing increment escapes on a directory that goes away
|
||||
fs::directory_iterator it(dir, fs::directory_options::skip_permission_denied, ec);
|
||||
// permission errors are skipped above, so this is a subtree the caller never sees
|
||||
if (ec) {
|
||||
truncated = true;
|
||||
continue;
|
||||
}
|
||||
for (const fs::directory_iterator end; it != end; it.increment(ec)) {
|
||||
if (ec) break;
|
||||
if (ec) {
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
if (std::chrono::steady_clock::now() >= deadline) {
|
||||
truncated = true;
|
||||
return result;
|
||||
}
|
||||
const fs::directory_entry & entry = *it;
|
||||
std::string fname = entry.path().filename().string();
|
||||
const fs::path fname = entry.path().filename();
|
||||
std::error_code tec;
|
||||
if (entry.is_directory(tec)) {
|
||||
std::string rel = (rel_dir / fname).string();
|
||||
std::replace(rel.begin(), rel.end(), '\\', '/');
|
||||
const bool is_dir = entry.is_directory(tec);
|
||||
if (tec) continue;
|
||||
if (is_dir) {
|
||||
if (kind == list_kind::dirs || kind == list_kind::all) {
|
||||
result.push_back({rel, true});
|
||||
result.push_back({path_to_utf8(rel_dir / fname), true});
|
||||
}
|
||||
// junk directories stay selectable but are never walked: they
|
||||
// hold nothing worth searching and can be enormous
|
||||
if (junk_dir_names().count(fname) > 0) continue;
|
||||
// do not descend into symlinks: a link can point back to an
|
||||
// ancestor and loop forever
|
||||
if (!entry.is_symlink(tec) && (max_depth == 0 || depth + 1 < max_depth)) {
|
||||
// junk directories stay selectable but are never walked: they can be enormous
|
||||
if (junk_dir_names().count(get_effective_name(path_to_utf8(fname))) > 0) continue;
|
||||
if (!is_link(entry) && (max_depth == 0 || depth + 1 < max_depth)) {
|
||||
stack.emplace_back(entry.path(), rel_dir / fname, depth + 1);
|
||||
}
|
||||
} else if (entry.is_regular_file(tec)) {
|
||||
std::string rel = (rel_dir / fname).string();
|
||||
std::replace(rel.begin(), rel.end(), '\\', '/');
|
||||
if (kind == list_kind::files || kind == list_kind::all) {
|
||||
result.push_back({rel, false});
|
||||
result.push_back({path_to_utf8(rel_dir / fname), false});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,7 +458,7 @@ static std::unique_ptr<tools_io> make_tools_io(const json & params) {
|
||||
// no '/' in pattern -> match basename at any depth; else match full relative path
|
||||
static bool path_glob_match(const std::string & pattern, const std::string & rel_path) {
|
||||
if (pattern.find('/') == std::string::npos) {
|
||||
return glob_match(pattern, fs::path(rel_path).filename().string());
|
||||
return glob_match(pattern, path_to_utf8(path_from_utf8(rel_path).filename()));
|
||||
}
|
||||
if (pattern == "**" || pattern.rfind("**/", 0) == 0 || pattern.rfind('/', 0) == 0) {
|
||||
return glob_match(pattern, rel_path);
|
||||
@@ -491,7 +555,7 @@ struct server_tool_read_file : server_tool {
|
||||
// file_glob_search: find files matching a glob pattern under a base directory
|
||||
//
|
||||
|
||||
static constexpr size_t SERVER_TOOL_FILE_SEARCH_MAX_RESULTS = 100;
|
||||
static constexpr int SERVER_TOOL_FILE_SEARCH_MAX_RESULTS = 100;
|
||||
static constexpr const char * SERVER_TOOL_FILE_SEARCH_TYPE_FILE = "file";
|
||||
static constexpr const char * SERVER_TOOL_FILE_SEARCH_TYPE_DIR = "dir";
|
||||
static constexpr const char * SERVER_TOOL_FILE_SEARCH_TYPE_ALL = "all";
|
||||
@@ -525,7 +589,7 @@ struct server_tool_file_glob_search : server_tool {
|
||||
{"exclude", {{"type", "string"}, {"description", "Glob pattern for files to exclude"}}},
|
||||
{"type", {{"type", "string"}, {"description", "Entry type to return: \"file\" (default), \"dir\" or \"all\""}}},
|
||||
{"max_depth", {{"type", "integer"}, {"description", "Maximum depth to descend into subdirectories (default: 0 = unlimited; 1 = direct children only)"}}},
|
||||
{"limit", {{"type", "integer"}, {"description", string_format("Maximum number of results to return (default %zu; values below 1 fall back to the default)", SERVER_TOOL_FILE_SEARCH_MAX_RESULTS)}}},
|
||||
{"limit", {{"type", "integer"}, {"description", string_format("Maximum number of results to return, capped at %d (default %d)", SERVER_TOOL_FILE_SEARCH_MAX_RESULTS, SERVER_TOOL_FILE_SEARCH_MAX_RESULTS)}}},
|
||||
}},
|
||||
{"required", json::array({"path"})},
|
||||
}},
|
||||
@@ -536,17 +600,18 @@ struct server_tool_file_glob_search : server_tool {
|
||||
json invoke(json params, server_tool::stream *) const override {
|
||||
auto io = make_tools_io(params);
|
||||
|
||||
std::string base = io->resolve(params.at("path").get<std::string>());
|
||||
// normalize to forward slashes so the web UI (which assumes '/') can
|
||||
// join the relative entries into absolute paths on Windows too
|
||||
std::replace(base.begin(), base.end(), '\\', '/');
|
||||
const std::string path = params.at("path").get<std::string>();
|
||||
|
||||
std::string base = io->resolve(path);
|
||||
std::string include = json_value(params, "include", std::string("**"));
|
||||
std::string exclude = json_value(params, "exclude", std::string(""));
|
||||
std::string type = json_value(params, "type", std::string("file"));
|
||||
int max_depth = std::max(0, json_value(params, "max_depth", 0));
|
||||
int limit = json_value(params, "limit", (int) SERVER_TOOL_FILE_SEARCH_MAX_RESULTS);
|
||||
if (limit < 1) limit = SERVER_TOOL_FILE_SEARCH_MAX_RESULTS;
|
||||
limit = std::min(limit, (int) SERVER_TOOL_FILE_SEARCH_MAX_RESULTS);
|
||||
const int limit_req = json_value(params, "limit", SERVER_TOOL_FILE_SEARCH_MAX_RESULTS);
|
||||
if (limit_req < 1) {
|
||||
return {{"error", "invalid limit: " + std::to_string(limit_req) + " (expected 1 or more)"}};
|
||||
}
|
||||
const int limit = std::min(limit_req, SERVER_TOOL_FILE_SEARCH_MAX_RESULTS);
|
||||
|
||||
list_kind kind;
|
||||
if (type == SERVER_TOOL_FILE_SEARCH_TYPE_FILE) {
|
||||
@@ -559,15 +624,13 @@ struct server_tool_file_glob_search : server_tool {
|
||||
return {{"error", "invalid type: " + type + " (expected \"file\", \"dir\" or \"all\")"}};
|
||||
}
|
||||
|
||||
std::string err;
|
||||
bool truncated = false;
|
||||
auto entries = io->list_entries(base, params.at("path").get<std::string>(), max_depth, kind, err, truncated);
|
||||
if (!err.empty()) {
|
||||
return {{"error", err}};
|
||||
const auto listing = io->list_entries(base, max_depth, kind);
|
||||
if (!listing.err.empty()) {
|
||||
return {{"error", listing.err + ": " + path}};
|
||||
}
|
||||
|
||||
std::vector<tools_io::list_entry> matches;
|
||||
for (const auto & entry : entries) {
|
||||
for (const auto & entry : listing.entries) {
|
||||
if (!path_glob_match(include, entry.rel)) continue;
|
||||
if (!exclude.empty() && path_glob_match(exclude, entry.rel)) continue;
|
||||
matches.push_back(entry);
|
||||
@@ -592,8 +655,8 @@ struct server_tool_file_glob_search : server_tool {
|
||||
"[%zu results limit reached (%zu total matches). Refine the glob pattern to narrow the search.]\n",
|
||||
shown, total);
|
||||
}
|
||||
if (truncated) {
|
||||
output_text << "[search timed out, results truncated]\n";
|
||||
if (listing.truncated) {
|
||||
output_text << "[results truncated: time budget or unreadable directory]\n";
|
||||
}
|
||||
|
||||
// `base` is always absolute (resolve falls back to the server cwd), so
|
||||
@@ -688,16 +751,14 @@ struct server_tool_grep_search : server_tool {
|
||||
if (io->is_regular_file(abs_path)) {
|
||||
files.emplace_back(abs_path, path);
|
||||
} else if (io->is_directory(abs_path)) {
|
||||
std::string err;
|
||||
bool truncated = false;
|
||||
auto candidates = io->list_entries(abs_path, path, 0, list_kind::files, err, truncated);
|
||||
if (!err.empty()) {
|
||||
return {{"error", err}};
|
||||
const auto listing = io->list_entries(abs_path, 0, list_kind::files);
|
||||
if (!listing.err.empty()) {
|
||||
return {{"error", listing.err + ": " + path}};
|
||||
}
|
||||
for (const auto & entry : candidates) {
|
||||
for (const auto & entry : listing.entries) {
|
||||
if (!path_glob_match(include, entry.rel)) continue;
|
||||
if (!exclude.empty() && path_glob_match(exclude, entry.rel)) continue;
|
||||
files.emplace_back((fs::path(abs_path) / entry.rel).string(), entry.rel);
|
||||
files.emplace_back(path_to_utf8(path_from_utf8(abs_path) / path_from_utf8(entry.rel)), entry.rel);
|
||||
}
|
||||
} else {
|
||||
return {{"error", "path does not exist: " + path}};
|
||||
@@ -1306,7 +1367,7 @@ struct server_tool_get_info : server_tool {
|
||||
std::string cwd = json_value(params, "cwd", std::string());
|
||||
if (cwd.empty()) {
|
||||
std::error_code ec;
|
||||
cwd = fs::current_path(ec).string();
|
||||
cwd = path_to_utf8(fs::current_path(ec));
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import pytest
|
||||
from utils import *
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
server = ServerPreset.tinyllama2()
|
||||
|
||||
@@ -39,3 +41,31 @@ def test_mcp_proxy_custom_port():
|
||||
res = server.make_request("GET", f"/cors-proxy?url=http://{server.server_host}:{server.server_port}/models")
|
||||
assert res.status_code == 200
|
||||
assert "data" in res.body
|
||||
|
||||
|
||||
def test_mcp_proxy_no_content():
|
||||
# note: see issue #26598
|
||||
class NoContentHandler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
target = ThreadingHTTPServer(("127.0.0.1", 0), NoContentHandler)
|
||||
target_thread = threading.Thread(target=target.serve_forever, daemon=True)
|
||||
target_thread.start()
|
||||
|
||||
try:
|
||||
global server
|
||||
server.ui_mcp_proxy = True
|
||||
server.start()
|
||||
|
||||
res = server.make_request("POST", f"/cors-proxy?url=http://127.0.0.1:{target.server_port}/", data={})
|
||||
assert res.status_code == 204
|
||||
assert res.body in (None, b"", "")
|
||||
finally:
|
||||
target.shutdown()
|
||||
target.server_close()
|
||||
|
||||
|
||||
@@ -214,6 +214,27 @@ def test_tools_builtin_file_glob_search_max_depth_and_limit(tmp_path):
|
||||
assert "Total matches: 3" in res["plain_text_response"]
|
||||
|
||||
|
||||
def test_tools_builtin_file_glob_search_junk_dirs(tmp_path):
|
||||
global server
|
||||
server.start()
|
||||
|
||||
(tmp_path / "build" / "nested").mkdir(parents=True)
|
||||
(tmp_path / "build" / "artifact.txt").write_text("built")
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "main.cpp").write_text("int main() {}")
|
||||
|
||||
# a junk directory stays selectable as a working directory
|
||||
res = call_tool("file_glob_search", {"path": str(tmp_path), "type": "dir", "max_depth": 1})
|
||||
assert "build" in [e["path"] for e in res["entries"]]
|
||||
|
||||
# but it is never walked, so nothing inside it shows up
|
||||
res = call_tool("file_glob_search", {"path": str(tmp_path), "type": "all"})
|
||||
paths = [e["path"] for e in res["entries"]]
|
||||
assert "src/main.cpp" in paths
|
||||
assert "build/artifact.txt" not in paths
|
||||
assert "build/nested" not in paths
|
||||
|
||||
|
||||
def test_tools_builtin_file_glob_search_rejects_invalid_type(tmp_path):
|
||||
global server
|
||||
server.start()
|
||||
|
||||
@@ -11,7 +11,8 @@ const config: StorybookConfig = {
|
||||
'@chromatic-com/storybook',
|
||||
'@storybook/addon-vitest',
|
||||
'@storybook/addon-a11y',
|
||||
'@storybook/addon-docs'
|
||||
'@storybook/addon-docs',
|
||||
'@storybook/addon-mcp'
|
||||
],
|
||||
framework: '@storybook/sveltekit',
|
||||
viteFinal: async (config) => {
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import * as a11yAddonAnnotations from '@storybook/addon-a11y/preview';
|
||||
import { setProjectAnnotations } from '@storybook/sveltekit';
|
||||
import * as previewAnnotations from './preview';
|
||||
import { beforeAll } from 'vitest';
|
||||
|
||||
const project = setProjectAnnotations([a11yAddonAnnotations, previewAnnotations]);
|
||||
|
||||
beforeAll(async () => {
|
||||
if (project.beforeAll) {
|
||||
await project.beforeAll();
|
||||
}
|
||||
});
|
||||
Generated
+877
-592
File diff suppressed because it is too large
Load Diff
+21
-18
@@ -27,20 +27,20 @@
|
||||
"cleanup": "rm -rf .svelte-kit build node_modules test-results dist dev-dist debug-storybook.log static/pwa-*.png static/maskable-icon-*.png static/apple-touch-icon-*.png static/apple-splash-*.png static/favicon*.ico"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "5.0.0",
|
||||
"@chromatic-com/storybook": "5.2.1",
|
||||
"@eslint/compat": "1.4.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@internationalized/date": "3.12.2",
|
||||
"@lucide/svelte": "1.25.0",
|
||||
"@modelcontextprotocol/sdk": "1.26.0",
|
||||
"@modelcontextprotocol/sdk": "1.30.0",
|
||||
"@playwright/test": "1.56.1",
|
||||
"@storybook/addon-a11y": "10.2.4",
|
||||
"@storybook/addon-docs": "10.2.4",
|
||||
"@storybook/addon-svelte-csf": "5.0.10",
|
||||
"@storybook/addon-vitest": "10.2.4",
|
||||
"@storybook/sveltekit": "10.2.4",
|
||||
"@storybook/addon-a11y": "10.5.6",
|
||||
"@storybook/addon-docs": "10.5.6",
|
||||
"@storybook/addon-svelte-csf": "5.1.2",
|
||||
"@storybook/addon-vitest": "10.5.6",
|
||||
"@storybook/sveltekit": "10.5.6",
|
||||
"@sveltejs/adapter-static": "3.0.10",
|
||||
"@sveltejs/kit": "2.60.1",
|
||||
"@sveltejs/kit": "2.70.2",
|
||||
"@sveltejs/vite-plugin-svelte": "6.2.1",
|
||||
"@tailwindcss/forms": "0.5.10",
|
||||
"@tailwindcss/typography": "0.5.16",
|
||||
@@ -48,16 +48,16 @@
|
||||
"@types/node": "24.13.0",
|
||||
"@vite-pwa/assets-generator": "1.0.2",
|
||||
"@vite-pwa/sveltekit": "1.1.0",
|
||||
"@vitest/browser": "4.1.8",
|
||||
"@vitest/browser-playwright": "4.1.8",
|
||||
"@vitest/coverage-v8": "4.1.8",
|
||||
"@vitest/browser": "4.1.10",
|
||||
"@vitest/browser-playwright": "4.1.10",
|
||||
"@vitest/coverage-v8": "4.1.10",
|
||||
"bits-ui": "2.18.1",
|
||||
"clsx": "2.1.1",
|
||||
"dexie": "4.4.3",
|
||||
"dompurify": "3.4.11",
|
||||
"dompurify": "3.4.13",
|
||||
"eslint": "9.39.4",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-storybook": "10.4.2",
|
||||
"eslint-plugin-storybook": "10.5.6",
|
||||
"eslint-plugin-svelte": "3.19.0",
|
||||
"fflate": "0.8.3",
|
||||
"globals": "16.5.0",
|
||||
@@ -82,7 +82,7 @@
|
||||
"remark-math": "6.0.0",
|
||||
"remark-rehype": "11.1.2",
|
||||
"sass": "1.100.0",
|
||||
"storybook": "10.4.2",
|
||||
"storybook": "10.5.6",
|
||||
"svelte": "5.56.1",
|
||||
"svelte-check": "4.6.0",
|
||||
"svelte-sonner": "1.1.1",
|
||||
@@ -95,13 +95,16 @@
|
||||
"unified": "11.0.5",
|
||||
"unist-util-visit": "5.1.0",
|
||||
"uuid": "13.0.2",
|
||||
"vite": "7.3.5",
|
||||
"vite": "7.3.6",
|
||||
"vite-plugin-devtools-json": "0.2.1",
|
||||
"vitest": "4.1.8",
|
||||
"vitest": "4.1.10",
|
||||
"vitest-browser-svelte": "2.1.1",
|
||||
"workbox-window": "7.4.1"
|
||||
"workbox-window": "7.4.1",
|
||||
"@storybook/addon-mcp": "0.7.0"
|
||||
},
|
||||
"overrides": {
|
||||
"cookie": "1.1.1"
|
||||
"cookie": "1.1.1",
|
||||
"sharp": "0.35.3",
|
||||
"valibot": "1.4.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,8 +87,9 @@
|
||||
let searchSeq = 0;
|
||||
|
||||
// Cache of the last file_glob_search result per (parent, include, max_depth),
|
||||
// so repeated queries in the same directory don't re-walk the tree. Entries
|
||||
// expire after a short TTL.
|
||||
// so repeated queries in the same directory don't re-walk the tree. Entering
|
||||
// a directory hits it every time: the children listed for an exactly typed
|
||||
// segment are what the next keystroke, the trailing slash, asks for again.
|
||||
const SEARCH_CACHE_TTL_MS = 2000;
|
||||
const searchCache = new SvelteMap<string, { results: GlobEntry[]; base: string; at: number }>();
|
||||
|
||||
@@ -161,7 +162,11 @@
|
||||
if (typeof res.error === 'string') return { base: '', entries: [], error: res.error };
|
||||
const base = typeof res.base === 'string' ? res.base : '';
|
||||
const entries = Array.isArray(res.entries) ? (res.entries as GlobEntry[]) : [];
|
||||
searchCache.set(key, { results: entries, base, at: Date.now() });
|
||||
const now = Date.now();
|
||||
for (const [k, v] of searchCache) {
|
||||
if (now - v.at >= SEARCH_CACHE_TTL_MS) searchCache.delete(k);
|
||||
}
|
||||
searchCache.set(key, { results: entries, base, at: now });
|
||||
return { base, entries };
|
||||
}
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ const SETTINGS_REGISTRY: Record<string, SettingsSectionEntry> = {
|
||||
key: SETTINGS_KEYS.SHOW_MESSAGE_STATS,
|
||||
label: 'Show message generation statistics',
|
||||
help: 'Display generation statistics (tokens/second, token count, duration) below each assistant message.',
|
||||
defaultValue: false,
|
||||
defaultValue: true,
|
||||
type: SettingsFieldType.CHECKBOX,
|
||||
section: SETTINGS_SECTION_SLUGS.DISPLAY
|
||||
},
|
||||
|
||||
@@ -76,8 +76,7 @@ export default defineConfig({
|
||||
extends: './vite.config.ts',
|
||||
test: {
|
||||
name: 'ui',
|
||||
browser: { ...browserBaseConfig, instances: [{ browser: 'chromium', headless: true }] },
|
||||
setupFiles: ['./.storybook/vitest.setup.ts']
|
||||
browser: { ...browserBaseConfig, instances: [{ browser: 'chromium', headless: true }] }
|
||||
},
|
||||
plugins: [
|
||||
storybookTest({
|
||||
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
Fix build on NetBSD, which provides posix_spawn_file_actions_addchdir()
|
||||
but not the _np() variant.
|
||||
|
||||
Upstream PR: https://github.com/sheredom/subprocess.h/pull/102
|
||||
Applied locally by scripts/sync_vendor.py until it is merged upstream.
|
||||
|
||||
diff --git a/subprocess.h b/subprocess.h
|
||||
index 5e809023a4..74a4e006c7 100644
|
||||
--- a/subprocess.h
|
||||
+++ b/subprocess.h
|
||||
@@ -1205,7 +1205,7 @@ cleanup:
|
||||
|
||||
// Set working directory
|
||||
if (process_cwd) {
|
||||
-#if defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000
|
||||
+#if defined(__NetBSD__) || (defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000)
|
||||
posix_error = posix_spawn_file_actions_addchdir(&actions, process_cwd);
|
||||
#else
|
||||
#if defined(__APPLE__) && defined(__clang__)
|
||||
@@ -0,0 +1,47 @@
|
||||
Fix building against glibc older than 2.29, which has no
|
||||
posix_spawn_file_actions_addchdir_np (the symbol is genuinely absent from
|
||||
libc.so, so no feature-test macro helps). Affects manylinux2014 (glibc 2.17)
|
||||
and manylinux_2_28, and was reported on RHEL 8.1. A requested process_cwd now
|
||||
fails with ENOSYS there instead of failing the build.
|
||||
|
||||
Upstream PR: https://github.com/sheredom/subprocess.h/pull/104
|
||||
Applied locally by scripts/sync_vendor.py until it is merged upstream.
|
||||
|
||||
(the README.md and test/ changes from the PR are omitted, we only vendor
|
||||
subprocess.h; rebased on top of patch-bsd.patch, so apply it after that one)
|
||||
|
||||
diff --git a/subprocess.h b/subprocess.h
|
||||
index 1ef424a..c363393 100644
|
||||
--- a/subprocess.h
|
||||
+++ b/subprocess.h
|
||||
@@ -274,6 +274,21 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process);
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
+/* Whether subprocess_create_ex can honour process_cwd. glibc only gained
|
||||
+ posix_spawn_file_actions_addchdir_np in 2.29. Define this yourself to
|
||||
+ override the detection, for instance on musl older than 1.1.24. */
|
||||
+#if !defined(SUBPROCESS_HAVE_CWD)
|
||||
+#if defined(__GLIBC__)
|
||||
+#if __GLIBC_PREREQ(2, 29)
|
||||
+#define SUBPROCESS_HAVE_CWD 1
|
||||
+#else
|
||||
+#define SUBPROCESS_HAVE_CWD 0
|
||||
+#endif
|
||||
+#else
|
||||
+#define SUBPROCESS_HAVE_CWD 1
|
||||
+#endif
|
||||
+#endif
|
||||
+
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <wchar.h>
|
||||
@@ -1219,6 +1234,8 @@ cleanup:
|
||||
if (process_cwd) {
|
||||
#if defined(__NetBSD__) || (defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000)
|
||||
posix_error = posix_spawn_file_actions_addchdir(&actions, process_cwd);
|
||||
+#elif !SUBPROCESS_HAVE_CWD
|
||||
+ posix_error = ENOSYS;
|
||||
#else
|
||||
#if defined(__APPLE__) && defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
Fix Windows command line quoting of backslash runs: a trailing backslash, or
|
||||
backslashes preceding a double quote, were not doubled, so CommandLineToArgvW
|
||||
in the child parsed them as escapes and mangled the argument list.
|
||||
|
||||
Upstream PR: https://github.com/sheredom/subprocess.h/pull/101
|
||||
Applied locally by scripts/sync_vendor.py until it is merged upstream.
|
||||
|
||||
(the test/ changes from the PR are omitted, we only vendor subprocess.h)
|
||||
|
||||
diff --git a/subprocess.h b/subprocess.h
|
||||
index 5e80902..b06ad4d 100644
|
||||
--- a/subprocess.h
|
||||
+++ b/subprocess.h
|
||||
@@ -653,6 +653,7 @@ int subprocess_create_ex(const char *const commandLine[], int options,
|
||||
int wide_len;
|
||||
int i, j;
|
||||
int need_quoting;
|
||||
+ subprocess_size_t bs_run;
|
||||
unsigned long flags = 0;
|
||||
unsigned long last_error = 0;
|
||||
int result = subprocess_error_unknown;
|
||||
@@ -906,25 +907,29 @@ int subprocess_create_ex(const char *const commandLine[], int options,
|
||||
len++;
|
||||
|
||||
// Quote the argument if it has a space in it
|
||||
- if (strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL ||
|
||||
- commandLine[i][0] == SUBPROCESS_NULL)
|
||||
+ need_quoting = strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL ||
|
||||
+ commandLine[i][0] == SUBPROCESS_NULL;
|
||||
+ if (need_quoting)
|
||||
len += 2;
|
||||
|
||||
+ bs_run = 0;
|
||||
for (j = 0; '\0' != commandLine[i][j]; j++) {
|
||||
- switch (commandLine[i][j]) {
|
||||
- default:
|
||||
- break;
|
||||
- case '\\':
|
||||
- if (commandLine[i][j + 1] == '"') {
|
||||
- len++;
|
||||
- }
|
||||
+ len++;
|
||||
|
||||
- break;
|
||||
- case '"':
|
||||
- len++;
|
||||
- break;
|
||||
+ if ('\\' == commandLine[i][j]) {
|
||||
+ bs_run++;
|
||||
+ } else {
|
||||
+ if ('"' == commandLine[i][j]) {
|
||||
+ // Duplicate the preceding run and escape the quote.
|
||||
+ len += bs_run + 1;
|
||||
+ }
|
||||
+ bs_run = 0;
|
||||
}
|
||||
- len++;
|
||||
+ }
|
||||
+
|
||||
+ if (need_quoting) {
|
||||
+ // Duplicate trailing slashes before the generated closing quote.
|
||||
+ len += bs_run;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,22 +954,29 @@ int subprocess_create_ex(const char *const commandLine[], int options,
|
||||
commandLineCombined[len++] = '"';
|
||||
}
|
||||
|
||||
- for (j = 0; '\0' != commandLine[i][j]; j++) {
|
||||
- switch (commandLine[i][j]) {
|
||||
- default:
|
||||
- break;
|
||||
- case '\\':
|
||||
- if (commandLine[i][j + 1] == '"') {
|
||||
- commandLineCombined[len++] = '\\';
|
||||
- }
|
||||
+ for (j = 0; '\0' != commandLine[i][j];) {
|
||||
+ bs_run = 0;
|
||||
+ while ('\\' == commandLine[i][j]) {
|
||||
+ bs_run++;
|
||||
+ j++;
|
||||
+ }
|
||||
+
|
||||
+ if ('"' == commandLine[i][j]) {
|
||||
+ // 2n + 1 slashes preserve n slashes and escape the quote.
|
||||
+ bs_run = (bs_run * 2) + 1;
|
||||
+ } else if ('\0' == commandLine[i][j] && need_quoting) {
|
||||
+ // 2n slashes preserve n slashes before the closing quote.
|
||||
+ bs_run *= 2;
|
||||
+ }
|
||||
|
||||
- break;
|
||||
- case '"':
|
||||
+ while (bs_run > 0) {
|
||||
commandLineCombined[len++] = '\\';
|
||||
- break;
|
||||
+ bs_run--;
|
||||
}
|
||||
|
||||
- commandLineCombined[len++] = commandLine[i][j];
|
||||
+ if ('\0' != commandLine[i][j]) {
|
||||
+ commandLineCombined[len++] = commandLine[i][j++];
|
||||
+ }
|
||||
}
|
||||
if (need_quoting) {
|
||||
commandLineCombined[len++] = '"';
|
||||
Vendored
+59
-30
@@ -274,6 +274,21 @@ subprocess_weak int subprocess_alive(struct subprocess_s *const process);
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
/* Whether subprocess_create_ex can honour process_cwd. glibc only gained
|
||||
posix_spawn_file_actions_addchdir_np in 2.29. Define this yourself to
|
||||
override the detection, for instance on musl older than 1.1.24. */
|
||||
#if !defined(SUBPROCESS_HAVE_CWD)
|
||||
#if defined(__GLIBC__)
|
||||
#if __GLIBC_PREREQ(2, 29)
|
||||
#define SUBPROCESS_HAVE_CWD 1
|
||||
#else
|
||||
#define SUBPROCESS_HAVE_CWD 0
|
||||
#endif
|
||||
#else
|
||||
#define SUBPROCESS_HAVE_CWD 1
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <wchar.h>
|
||||
@@ -653,6 +668,7 @@ int subprocess_create_ex(const char *const commandLine[], int options,
|
||||
int wide_len;
|
||||
int i, j;
|
||||
int need_quoting;
|
||||
subprocess_size_t bs_run;
|
||||
unsigned long flags = 0;
|
||||
unsigned long last_error = 0;
|
||||
int result = subprocess_error_unknown;
|
||||
@@ -906,25 +922,29 @@ int subprocess_create_ex(const char *const commandLine[], int options,
|
||||
len++;
|
||||
|
||||
// Quote the argument if it has a space in it
|
||||
if (strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL ||
|
||||
commandLine[i][0] == SUBPROCESS_NULL)
|
||||
need_quoting = strpbrk(commandLine[i], "\t\v ") != SUBPROCESS_NULL ||
|
||||
commandLine[i][0] == SUBPROCESS_NULL;
|
||||
if (need_quoting)
|
||||
len += 2;
|
||||
|
||||
bs_run = 0;
|
||||
for (j = 0; '\0' != commandLine[i][j]; j++) {
|
||||
switch (commandLine[i][j]) {
|
||||
default:
|
||||
break;
|
||||
case '\\':
|
||||
if (commandLine[i][j + 1] == '"') {
|
||||
len++;
|
||||
}
|
||||
|
||||
break;
|
||||
case '"':
|
||||
len++;
|
||||
break;
|
||||
}
|
||||
len++;
|
||||
|
||||
if ('\\' == commandLine[i][j]) {
|
||||
bs_run++;
|
||||
} else {
|
||||
if ('"' == commandLine[i][j]) {
|
||||
// Duplicate the preceding run and escape the quote.
|
||||
len += bs_run + 1;
|
||||
}
|
||||
bs_run = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (need_quoting) {
|
||||
// Duplicate trailing slashes before the generated closing quote.
|
||||
len += bs_run;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,22 +969,29 @@ int subprocess_create_ex(const char *const commandLine[], int options,
|
||||
commandLineCombined[len++] = '"';
|
||||
}
|
||||
|
||||
for (j = 0; '\0' != commandLine[i][j]; j++) {
|
||||
switch (commandLine[i][j]) {
|
||||
default:
|
||||
break;
|
||||
case '\\':
|
||||
if (commandLine[i][j + 1] == '"') {
|
||||
commandLineCombined[len++] = '\\';
|
||||
}
|
||||
|
||||
break;
|
||||
case '"':
|
||||
commandLineCombined[len++] = '\\';
|
||||
break;
|
||||
for (j = 0; '\0' != commandLine[i][j];) {
|
||||
bs_run = 0;
|
||||
while ('\\' == commandLine[i][j]) {
|
||||
bs_run++;
|
||||
j++;
|
||||
}
|
||||
|
||||
commandLineCombined[len++] = commandLine[i][j];
|
||||
if ('"' == commandLine[i][j]) {
|
||||
// 2n + 1 slashes preserve n slashes and escape the quote.
|
||||
bs_run = (bs_run * 2) + 1;
|
||||
} else if ('\0' == commandLine[i][j] && need_quoting) {
|
||||
// 2n slashes preserve n slashes before the closing quote.
|
||||
bs_run *= 2;
|
||||
}
|
||||
|
||||
while (bs_run > 0) {
|
||||
commandLineCombined[len++] = '\\';
|
||||
bs_run--;
|
||||
}
|
||||
|
||||
if ('\0' != commandLine[i][j]) {
|
||||
commandLineCombined[len++] = commandLine[i][j++];
|
||||
}
|
||||
}
|
||||
if (need_quoting) {
|
||||
commandLineCombined[len++] = '"';
|
||||
@@ -1205,8 +1232,10 @@ cleanup:
|
||||
|
||||
// Set working directory
|
||||
if (process_cwd) {
|
||||
#if defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000
|
||||
#if defined(__NetBSD__) || (defined(__APPLE__) && MAC_OS_X_VERSION_MIN_REQUIRED >= 260000)
|
||||
posix_error = posix_spawn_file_actions_addchdir(&actions, process_cwd);
|
||||
#elif !SUBPROCESS_HAVE_CWD
|
||||
posix_error = ENOSYS;
|
||||
#else
|
||||
#if defined(__APPLE__) && defined(__clang__)
|
||||
#pragma clang diagnostic push
|
||||
|
||||
Reference in New Issue
Block a user