Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
/tests/test_metal_tp_spec
/tests/test_metal_tp_cancel
/tests/test_metal_moe_prefill
/tests/test_metal_slab_residency
/speed-bench/metal_slab_residency_bench
/tests/test_metal_ssd_experts
/tests/test_metal_command_memory
/tests/test_metal_dense_mpp
Expand Down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ tests/test_metal_moe_prefill: tests/test_metal_moe_prefill.o $(CORE_OBJS)
test-metal-moe-prefill: tests/test_metal_moe_prefill
./tests/test_metal_moe_prefill

tests/test_metal_slab_residency: tests/test_metal_slab_residency.m ds4_metal.m ds4_gpu.h ds4_image.o $(METAL_SRCS)
$(CC) $(OBJCFLAGS) -I. -o $@ $< ds4_image.o $(METAL_LDLIBS)

.PHONY: test-metal-slab-residency
test-metal-slab-residency: tests/test_metal_slab_residency
MTL_DEBUG_LAYER=1 ./tests/test_metal_slab_residency

speed-bench/metal_slab_residency_bench: speed-bench/metal_slab_residency_bench.m
$(CC) $(OBJCFLAGS) -o $@ $< $(METAL_LDLIBS)

.PHONY: metal-slab-residency-bench
metal-slab-residency-bench: speed-bench/metal_slab_residency_bench

tests/test_qwen4_moe_mm_specialize.o: tests/test_qwen4_moe_mm_specialize.c ds4_gpu.h
$(CC) $(CFLAGS) -fno-fast-math -I. -c -o $@ $<

Expand Down Expand Up @@ -1023,6 +1036,7 @@ clean:
rm -f tests/test_web_recovery
rm -f tests/test_metal_ssd_experts
rm -f tests/test_metal_command_memory
rm -f tests/test_metal_slab_residency speed-bench/metal_slab_residency_bench
rm -f tests/test_deepseek41_metal
rm -f tests/test_deepseek41_cuda
rm -f tests/test_cuda_q8_rows
Expand Down
53 changes: 53 additions & 0 deletions ds4_metal.m
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,7 @@ static void ds4_gpu_print_device_summary(void) {
static id<MTLBuffer> g_stream_expert_cache_up_addr_buffers[DS4_METAL_STREAM_EXPERT_CACHE_MAX_LAYER];
static id<MTLBuffer> g_stream_expert_cache_down_addr_buffers[DS4_METAL_STREAM_EXPERT_CACHE_MAX_LAYER];
static id<MTLBuffer> g_stream_expert_cache_slabs[DS4_METAL_STREAM_EXPERT_CACHE_MAX_SLABS];
static id g_stream_slab_residency_set;
static uint32_t g_stream_expert_cache_slab_start_slot[DS4_METAL_STREAM_EXPERT_CACHE_MAX_SLABS];
static uint32_t g_stream_expert_cache_slab_slot_count[DS4_METAL_STREAM_EXPERT_CACHE_MAX_SLABS];
static uint32_t g_stream_expert_cache_slab_slots_used[DS4_METAL_STREAM_EXPERT_CACHE_MAX_SLABS];
Expand Down Expand Up @@ -4421,6 +4422,13 @@ void ds4_gpu_print_memory_report(const char *label) {
(unsigned long long)g_stream_expert_cache_buffer_allocs,
(unsigned long long)g_stream_expert_cache_buffer_reuses);
}
if (g_stream_slab_residency_set) {
uint64_t bytes = 0;
for (uint32_t i = 0; i < g_stream_expert_cache_slab_count; i++)
bytes += [g_stream_expert_cache_slabs[i] length];
fprintf(stderr, "ds4: streaming slab residency: %u slabs, %.2f GiB allocations\n",
g_stream_expert_cache_slab_count, ds4_gpu_gib(bytes));
}
if (g_stream_expert_cache_mlock_bytes != 0 ||
g_stream_expert_cache_mlock_failures != 0) {
fprintf(stderr,
Expand Down Expand Up @@ -13877,6 +13885,17 @@ static uint64_t ds4_gpu_stream_expert_slab_target_bytes(void) {
return target;
}

static void ds4_gpu_stream_slab_residency_clear(void) {
#if TARGET_OS_OSX
if (@available(macOS 15.0, *)) {
if (g_stream_slab_residency_set) {
[g_queue removeResidencySet:g_stream_slab_residency_set];
g_stream_slab_residency_set = nil;
}
}
#endif
}

static id<MTLBuffer> ds4_gpu_stream_expert_alloc_slab_buffer(
uint64_t len,
NSString *label) {
Expand All @@ -13896,6 +13915,36 @@ static uint64_t ds4_gpu_stream_expert_slab_target_bytes(void) {
}
buffer.label = label;
g_stream_expert_cache_buffer_allocs++;
if (getenv("DS4_METAL_STREAMING_SLAB_RESIDENCY") &&
!g_stream_expert_cache_mlock_relief_applied) {
#if TARGET_OS_OSX
if (@available(macOS 15.0, *)) {
const BOOL fresh = g_stream_slab_residency_set == nil;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I find the fresh bool logic hard to read. It's basically an if/else on g_stream_slab_residency_set == nil with the else just adding the new buffer to the existing residency set.

if (fresh) {
MTLResidencySetDescriptor *desc = [[MTLResidencySetDescriptor alloc] init];
desc.label = @"ds4_streaming_expert_slabs";
desc.initialCapacity = 64;
NSError *error = nil;
g_stream_slab_residency_set = [g_device newResidencySetWithDescriptor:desc error:&error];
if (!g_stream_slab_residency_set) {
fprintf(stderr, "ds4: streaming slab residency set failed: %s\n",
[[error localizedDescription] UTF8String]);
return nil;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Somewhat debatable to me why can't we just return the usable buffer here.

}
}
/* Only owned cache slabs belong here, never disk-backed model
* views. Keep registration across slot reuse; clear it with the
* physical slab pool after outstanding GPU work has drained.
* Queue attachment covers each submission without a separate
* explicit requestResidency lifetime. */
[g_stream_slab_residency_set addAllocation:buffer];
[g_stream_slab_residency_set commit];
if (fresh) {
[g_queue addResidencySet:g_stream_slab_residency_set];
}
}
#endif
}
return buffer;
}

Expand Down Expand Up @@ -15184,6 +15233,7 @@ static void ds4_gpu_stream_expert_cache_clear_all(int reset_stats) {
}
g_stream_expert_cache_bytes = 0;
g_stream_expert_cache_entry_count = 0;
ds4_gpu_stream_slab_residency_clear();
for (uint32_t i = 0; i < g_stream_expert_cache_slab_count; i++) {
g_stream_expert_cache_slabs[i] = nil;
g_stream_expert_cache_slab_start_slot[i] = 0;
Expand Down Expand Up @@ -15725,6 +15775,9 @@ static uint32_t ds4_gpu_stream_expert_cache_release_mlock_margin(

if (released == 0) return 0;
g_stream_expert_cache_mlock_relief_applied = 1;
/* Let released slots become reclaimable instead of requesting the whole
* pool again on every submission. A cache rebuild can enable residency. */
ds4_gpu_stream_slab_residency_clear();

uint32_t cap = g_stream_expert_cache_entry_count;
const uint32_t locked_after =
Expand Down
21 changes: 21 additions & 0 deletions speed-bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,27 @@ python3 speed-bench/plot_speed.py speed-bench/m3_max.csv --title "M3 Max t/s"
The script uses only the Python standard library. By default it writes a file
next to the CSV using the `_ts.svg` suffix, such as `speed-bench/m3_max_ts.svg`.

### DeepSeek V4.1 Flash streaming slab residency

On macOS 15+, `DS4_METAL_STREAMING_SLAB_RESIDENCY=1` attaches owned expert-cache
slabs to the existing Metal queue. It is opt-in; disk-backed model views and
Engram tables are excluded. Memory-pressure relief detaches the set until the
cache is rebuilt. See [the M2 Ultra investigation](v41_slab_residency_m2_ultra.md).

A model-free reproducer checks every GPU result and toggles queue attachment
OFF/ON/OFF/ON. The following allocates and locks approximately 104 GiB; run it
alone on a host with enough available memory:

```sh
make metal-slab-residency-bench test-metal-slab-residency
DS4_SLAB_BENCH_ALTERNATE_SMALL=1 ./speed-bench/metal_slab_residency_bench \
toggle 26 4096 4080 24 6 > /tmp/slab-residency.csv
```

Arguments are mode, slab count, allocation MiB per slab, filled/locked MiB per
slab, iterations per phase and selected slabs per command buffer. Use eight
slabs instead of 26 for the 32 GiB control. No model file is opened.

### Metal decode schedule A/B

Build the balanced, same-engine Metal decode comparison with:
Expand Down
164 changes: 164 additions & 0 deletions speed-bench/metal_slab_residency_bench.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/* Model-free large-slab residency reproducer; see speed-bench/README.md. */
#import <Foundation/Foundation.h>
#import <Metal/Metal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <time.h>

static double now_ms(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return t.tv_sec * 1000.0 + t.tv_nsec / 1e6;
}

static uint64_t number(const char *text) {
char *end = NULL;
unsigned long long value = strtoull(text, &end, 10);
return text[0] && text[0] != '-' && end && !*end ? value : 0;
}

int main(int argc, char **argv) {
if (argc != 7) {
fprintf(stderr, "usage: %s none|queue|toggle SLABS SLAB_MIB FILLED_MIB ITERATIONS SELECTED\n",
argv[0]);
return 2;
}
const char *mode = argv[1];
const uint64_t count64 = number(argv[2]), mib = number(argv[3]);
const uint64_t filled_mib = number(argv[4]);
const uint64_t iterations64 = number(argv[5]), selected64 = number(argv[6]);
if ((strcmp(mode, "none") && strcmp(mode, "queue") && strcmp(mode, "toggle")) ||
!count64 || count64 > UINT32_MAX || !mib || mib > UINT64_MAX / 1048576 ||
filled_mib < 4 || filled_mib > mib || !iterations64 || iterations64 > UINT32_MAX ||
!selected64 || selected64 > count64 || selected64 > UINT32_MAX / 1024) return 2;
const uint32_t count = (uint32_t)count64, iterations = (uint32_t)iterations64;
const uint32_t selected = (uint32_t)selected64;
const uint64_t bytes = mib * 1048576, filled = filled_mib * 1048576;
const BOOL alternate = getenv("DS4_SLAB_BENCH_ALTERNATE_SMALL") != NULL;

if (@available(macOS 15.0, *)) {
@autoreleasepool {
id<MTLDevice> dev = MTLCreateSystemDefaultDevice();
id<MTLCommandQueue> queue = [dev newCommandQueue];
if (!dev || !queue || bytes > dev.maxBufferLength) return 1;
fprintf(stderr, "device=%s mode=%s slabs=%u slab_MiB=%llu filled_MiB=%llu "
"selected=%u alternate_small=%d\n", dev.name.UTF8String, mode,
count, mib, filled_mib, selected, alternate);
NSString *source = @"#include <metal_stdlib>\n"
"using namespace metal;\n"
"kernel void probe(device const ulong *a [[buffer(0)]], "
"device uint *out [[buffer(1)]], uint i [[thread_position_in_grid]]) { "
"device const uint *b = reinterpret_cast<device const uint *>(a[i/1024]); "
"out[i] = b[(i%1024)*1024]; }";
NSError *error = nil;
id<MTLLibrary> lib = [dev newLibraryWithSource:source options:nil error:&error];
if (!lib) {
fprintf(stderr, "Metal library: %s\n", error.description.UTF8String);
return 1;
}
id<MTLComputePipelineState> pipeline = [dev
newComputePipelineStateWithFunction:[lib newFunctionWithName:@"probe"] error:&error];
if (!pipeline) return 1;

NSMutableArray<id<MTLBuffer>> *pool = [NSMutableArray array];
for (uint32_t i = 0; i < count; i++) {
id<MTLBuffer> b = [dev newBufferWithLength:bytes options:MTLResourceStorageModeShared];
if (!b) return 1;
memset(b.contents, i % 127 + 1, filled);
/* Match the Q2 expert-slot lock granularity. No model file IO. */
const uint64_t slot_bytes = 9961472;
for (uint64_t off = 0; off < filled; off += slot_bytes) {
const uint64_t n = filled - off < slot_bytes ? filled - off : slot_bytes;
if (mlock((char *)b.contents + off, n)) {
perror("mlock");
return 1;
}
}
[pool addObject:b];
}
id<MTLBuffer> small = [dev newBufferWithLength:4194304 options:MTLResourceStorageModeShared];
id<MTLBuffer> addresses = [dev newBufferWithLength:selected * sizeof(uint64_t)
options:MTLResourceStorageModeShared];
id<MTLBuffer> out = [dev newBufferWithLength:selected * 1024 * sizeof(uint32_t)
options:MTLResourceStorageModeShared];
if (!small || !addresses || !out) return 1;
memset(small.contents, 1, 4194304);
id<MTLResidencySet> set = nil;
BOOL queued = NO;
if (strcmp(mode, "none")) {
MTLResidencySetDescriptor *desc = [MTLResidencySetDescriptor new];
desc.initialCapacity = count;
set = [dev newResidencySetWithDescriptor:desc error:&error];
if (!set) return 1;
for (id<MTLBuffer> b in pool) [set addAllocation:b];
[set commit];
if (!strcmp(mode, "queue")) {
[queue addResidencySet:set];
queued = YES;
}
}
printf("phase,iteration,kind,wall_ms,gpu_ms,driver_ms,checksum\n");
const uint32_t phases = !strcmp(mode, "toggle") ? 4 : 1;
for (uint32_t phase = 0; phase < phases; phase++) {
if (phases > 1) {
if (phase % 2) {
[queue addResidencySet:set];
queued = YES;
} else if (queued) {
[queue removeResidencySet:set];
queued = NO;
}
}
for (uint32_t it = 0; it < iterations; it++) {
@autoreleasepool {
const BOOL use_small = alternate && (it % 2);
const double begin = now_ms();
id<MTLCommandBuffer> cb = [queue commandBuffer];
id<MTLComputeCommandEncoder> enc = [cb computeCommandEncoder];
uint64_t expected = 0;
for (uint32_t j = 0; j < selected; j++) {
const uint32_t k = ((uint64_t)it * selected + j) % count;
id<MTLBuffer> b = use_small ? small : pool[k];
((uint64_t *)addresses.contents)[j] = b.gpuAddress;
[enc useResource:b usage:MTLResourceUsageRead];
expected += (uint64_t)(0x01010101u * (use_small ? 1 : k % 127 + 1)) * 1024;
}
[enc setComputePipelineState:pipeline];
[enc setBuffer:addresses offset:0 atIndex:0];
[enc setBuffer:out offset:0 atIndex:1];
[enc dispatchThreadgroups:MTLSizeMake(selected * 4, 1, 1)
threadsPerThreadgroup:MTLSizeMake(256, 1, 1)];
[enc endEncoding];
[cb commit];
[cb waitUntilCompleted];
const double end = now_ms();
if (cb.status != MTLCommandBufferStatusCompleted) {
fprintf(stderr, "command buffer: %s\n", cb.error.description.UTF8String);
return 1;
}
uint64_t checksum = 0;
for (uint32_t j = 0; j < selected * 1024; j++) checksum += ((uint32_t *)out.contents)[j];
if (checksum != expected) {
fprintf(stderr, "checksum mismatch\n");
return 1;
}
printf("%u,%u,%s,%.6f,%.6f,%.6f,%llu\n", phase, it,
use_small ? "small" : "slabs", end - begin,
(cb.GPUEndTime - cb.GPUStartTime) * 1000,
(cb.kernelEndTime - cb.kernelStartTime) * 1000, checksum);
}
}
fflush(stdout);
}
if (queued) [queue removeResidencySet:set];
for (id<MTLBuffer> b in pool) munlock(b.contents, filled);
}
} else {
fprintf(stderr, "requires macOS 15 or later\n");
return 1;
}
return 0;
}
5 changes: 5 additions & 0 deletions speed-bench/v41_slab_residency_m2_ultra.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
run,variant,ctx_tokens,prefill_tokens,prefill_tps,gen_tokens,gen_tps,gen_first_ms,gen_steady_tokens,gen_steady_tps,kvcache_bytes
bench-0-control,control,2048,2048,76.15,8,0.25,4103.550,7,0.25,0
bench-1-candidate,candidate,2048,2048,70.95,8,9.57,329.048,7,13.83,0
bench-2-candidate,candidate,2048,2048,69.16,8,9.67,316.315,7,13.71,0
bench-3-control,control,2048,2048,60.11,8,0.25,3935.309,7,0.25,0
Loading