Commit ea24b952c for llama.cpp

commit ea24b952cd21a3c00ecffa23b328a6037a8692eb
Author: Yuri Khrustalev <ykhrustalev@users.noreply.github.com>
Date:   Mon Nov 2 07:08:21 2026 -0500

    ggml : use `vm_allocate` instead of `posix_memalign` on all Apple platforms (#31437)

    * ggml : use `vm_allocate` instead of `posix_memalign` on iOS

    * ggml : use vm_allocate on all Apple platforms, run test-host-buffer-release by default

diff --git a/ggml/src/ggml-metal/ggml-metal-device.m b/ggml/src/ggml-metal/ggml-metal-device.m
index 3d4ebf86f..19ed68f9a 100644
--- a/ggml/src/ggml-metal/ggml-metal-device.m
+++ b/ggml/src/ggml-metal/ggml-metal-device.m
@@ -2102,19 +2102,11 @@ static void ggml_metal_buffer_rset_free(ggml_metal_buffer_t buf) {
 static void * ggml_metal_host_malloc(size_t n) {
     void * data = NULL;

-#if TARGET_OS_OSX
     kern_return_t err = vm_allocate((vm_map_t) mach_task_self(), (void *) &data, n, VM_FLAGS_ANYWHERE);
     if (err != KERN_SUCCESS) {
         GGML_LOG_ERROR("%s: error: vm_allocate failed\n", __func__);
         return NULL;
     }
-#else
-    const int result = posix_memalign((void **) &data, sysconf(_SC_PAGESIZE), n);
-    if (result != 0) {
-        GGML_LOG_ERROR("%s: error: posix_memalign failed\n", __func__);
-        return NULL;
-    }
-#endif

     return data;
 }
@@ -2296,11 +2288,7 @@ void ggml_metal_buffer_free(ggml_metal_buffer_t buf) {
     }

     if (buf->is_shared && buf->owned) {
-#if TARGET_OS_OSX
         vm_deallocate((vm_map_t)mach_task_self(), (vm_address_t)buf->all_data, buf->all_size);
-#else
-        free(buf->all_data);
-#endif
     }

     free(buf);
diff --git a/ggml/src/ggml.c b/ggml/src/ggml.c
index 822c8ab64..a324ec951 100644
--- a/ggml/src/ggml.c
+++ b/ggml/src/ggml.c
@@ -357,7 +357,8 @@ void * ggml_aligned_malloc(size_t size) {
     void * aligned_memory = NULL;
   #ifdef GGML_USE_CPU_HBM
     int result = hbw_posix_memalign(&aligned_memory, alignment, size);
-  #elif TARGET_OS_OSX && !defined(GGML_TSAN_ENABLED)
+  #elif defined(__APPLE__) && !defined(GGML_TSAN_ENABLED)
+    // libmalloc can keep freed pages in the process footprint, vm_deallocate returns them at once
     GGML_UNUSED(alignment);
     kern_return_t alloc_status = vm_allocate((vm_map_t) mach_task_self(), (vm_address_t *) &aligned_memory, size, VM_FLAGS_ANYWHERE);
     int result = EFAULT;
@@ -404,7 +405,7 @@ void ggml_aligned_free(void * ptr, size_t size) {
     if (ptr != NULL) {
         hbw_free(ptr);
     }
-#elif TARGET_OS_OSX && !defined(GGML_TSAN_ENABLED)
+#elif defined(__APPLE__) && !defined(GGML_TSAN_ENABLED)
     if (ptr != NULL) {
         vm_deallocate((vm_map_t)mach_task_self(), (vm_address_t)ptr, size);
     }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 841339356..4224a15ae 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -364,6 +364,7 @@ llama_build_and_test(test-backend-sampler.cpp   LABEL "model behavior backend")

 if (APPLE)
     llama_build(test-rset-release.cpp)
+    llama_build_and_test(test-host-buffer-release.cpp LINK ggml)
 endif()

 if (NOT GGML_BACKEND_DL AND GGML_CPU)
diff --git a/tests/test-host-buffer-release.cpp b/tests/test-host-buffer-release.cpp
new file mode 100644
index 000000000..594157306
--- /dev/null
+++ b/tests/test-host-buffer-release.cpp
@@ -0,0 +1,47 @@
+// ref: https://github.com/ggml-org/llama.cpp/issues/30556
+
+#include "ggml-backend.h"
+
+#include <cstdint>
+#include <cstdio>
+#include <cstring>
+#include <mach/mach.h>
+
+static uint64_t phys_footprint() {
+    task_vm_info_data_t info;
+    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
+    const kern_return_t ret = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &info, &count);
+    GGML_ASSERT(ret == KERN_SUCCESS);
+    return info.phys_footprint;
+}
+
+static bool test_release(ggml_backend_buffer_type_t buft) {
+    const size_t size = 256ull*1024*1024;
+
+    ggml_backend_buffer_t buf = ggml_backend_buft_alloc_buffer(buft, size);
+    GGML_ASSERT(buf != nullptr);
+    ggml_backend_buffer_clear(buf, 1);
+
+    const uint64_t footprint_alloc = phys_footprint();
+    ggml_backend_buffer_free(buf);
+    const uint64_t footprint_free = phys_footprint();
+
+    printf("%s: phys_footprint %.1f MiB -> %.1f MiB after freeing %.1f MiB\n", ggml_backend_buft_name(buft), footprint_alloc/1048576.0, footprint_free/1048576.0, size/1048576.0);
+
+    // do not poll, the footprint must drop at free time
+    return footprint_free < footprint_alloc - 0.75*size;
+}
+
+int main() {
+    ggml_backend_load_all();
+
+    bool ok = test_release(ggml_backend_cpu_buffer_type());
+
+    // private Metal buffers are allocated by Metal, not by ggml_metal_host_malloc
+    ggml_backend_dev_t dev = ggml_backend_dev_by_name("MTL0");
+    if (dev && strcmp(ggml_backend_buft_name(ggml_backend_dev_buffer_type(dev)), "MTL0") == 0) {
+        ok = test_release(ggml_backend_dev_buffer_type(dev)) && ok;
+    }
+
+    return ok ? 0 : 1;
+}