char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Georgi Gerganovmetal : single-source fusion table + fusion debug rework (#28164)a2878d30d

char/aliceai-t5-moe
26.2 KiB771 linesraw
1#import "ggml-metal-context.h"
2
3#import "ggml-impl.h"
4#import "ggml-backend-impl.h"
5
6#import "ggml-metal-impl.h"
7#import "ggml-metal-common.h"
8#import "ggml-metal-ops.h"
9#import "ggml-metal-fusion.h"
10
11#import <Foundation/Foundation.h>
12
13#import <Metal/Metal.h>
14
15#undef MIN
16#undef MAX
17#define MIN(a, b) ((a) < (b) ? (a) : (b))
18#define MAX(a, b) ((a) > (b) ? (a) : (b))
19
20// max number of MTLCommandBuffer used to submit a graph for processing
21#define GGML_METAL_MAX_COMMAND_BUFFERS 8
22
23struct ggml_metal_command_buffer {
24    id<MTLCommandBuffer> obj;
25};
26
27struct ggml_metal {
28    char name[128];
29
30    ggml_metal_device_t  dev;
31    ggml_metal_library_t lib;
32
33    ggml_metal_event_t ev_cpy; // for async copies
34
35    dispatch_queue_t d_queue;
36
37    // additional, inference-time compiled pipelines
38    ggml_metal_pipelines_t pipelines_ext;
39
40    bool use_concurrency;
41    bool use_graph_optimize;
42
43    int debug_graph;
44
45    struct ggml_metal_fusion_info * finfo;
46
47    // capture state
48    int capture_compute;
49    bool capture_started;
50
51    id<MTLCaptureScope> capture_scope;
52
53    // command buffer state
54    int n_cb;           // number of extra threads used to submit the command buffers
55    int n_nodes_0;      // number of nodes submitted by the main thread
56    int n_nodes_1;      // remaining number of nodes submitted by the n_cb threads
57    int n_nodes_per_cb;
58
59    struct ggml_cgraph * gf;
60
61    // the callback given to the thread pool
62    void (^encode_async)(size_t ith);
63
64    // n_cb command buffers + 1 used by the main thread
65    struct ggml_metal_command_buffer cmd_bufs[GGML_METAL_MAX_COMMAND_BUFFERS + 1];
66
67    // extra command buffers for things like getting, setting and copying tensors
68    NSMutableArray * cmd_bufs_ext;
69
70    // buffers to release after async Metal operations complete
71    // if Metal released them, it would do so on a Metal-internal thread without an autorelease pool, which could cause leaks
72    NSMutableArray * buf_refs;
73
74    // the last command buffer queued into the Metal queue with operations relevant to the current Metal backend
75    id<MTLCommandBuffer> cmd_buf_last;
76
77    // abort ggml_metal_graph_compute if callback returns true
78    ggml_abort_callback abort_callback;
79    void *              abort_callback_data;
80
81    // error state - set when a command buffer fails during synchronize
82    // once set, graph_compute will return GGML_STATUS_FAILED until the backend is recreated
83    bool has_error;
84};
85
86ggml_metal_t ggml_metal_init(ggml_metal_device_t dev) {
87    GGML_LOG_INFO("%s: allocating\n", __func__);
88
89    @autoreleasepool {
90#if TARGET_OS_OSX && !GGML_METAL_NDEBUG
91        // Show all the Metal device instances in the system
92        NSArray * devices = MTLCopyAllDevices();
93        for (id<MTLDevice> device in devices) {
94            GGML_LOG_INFO("%s: found device: %s\n", __func__, [[device name] UTF8String]);
95        }
96        [devices release]; // since it was created by a *Copy* C method
97#endif
98
99        // init context
100        ggml_metal_t res = calloc(1, sizeof(struct ggml_metal));
101
102        id<MTLDevice> device = ggml_metal_device_get_obj(dev);
103
104        GGML_LOG_INFO("%s: picking default device: %s\n", __func__, [[device name] UTF8String]);
105
106        // TODO: would it be better to have one queue for the backend and one queue for the device?
107        //       the graph encoders and async ops would use the backend queue while the sync ops would use the device queue?
108        //res->queue = [device newCommandQueue]; [TAG_QUEUE_PER_BACKEND]
109        id<MTLCommandQueue> queue = ggml_metal_device_get_queue(dev);
110        if (queue == nil) {
111            GGML_LOG_ERROR("%s: error: failed to create command queue\n", __func__);
112            free(res);
113            return NULL;
114        }
115
116        res->dev = dev;
117        res->lib = ggml_metal_device_get_library(dev);
118        if (res->lib == NULL) {
119            GGML_LOG_WARN("%s: the device does not have a precompiled Metal library - this is unexpected\n", __func__);
120            GGML_LOG_WARN("%s: will try to compile it on the fly\n", __func__);
121
122            res->lib = ggml_metal_library_init(dev);
123            if (res->lib == NULL) {
124                GGML_LOG_ERROR("%s: error: failed to initialize the Metal library\n", __func__);
125
126                free(res);
127
128                return NULL;
129            }
130        }
131
132        res->ev_cpy = ggml_metal_device_event_init(dev);
133
134        const struct ggml_metal_device_props * props_dev = ggml_metal_device_get_props(dev);
135
136        snprintf(res->name, sizeof(res->name), "%s", props_dev->name);
137
138        res->d_queue = dispatch_queue_create("ggml-metal", DISPATCH_QUEUE_CONCURRENT);
139
140        res->use_concurrency = getenv("GGML_METAL_CONCURRENCY_DISABLE") == nil;
141
142        {
143            const char * val = getenv("GGML_METAL_GRAPH_DEBUG");
144            res->debug_graph = val ? atoi(val) : 0;
145        }
146
147        res->use_graph_optimize = true;
148
149        if (getenv("GGML_METAL_GRAPH_OPTIMIZE_DISABLE") != NULL) {
150            res->use_graph_optimize = false;
151        }
152
153        res->finfo = ggml_metal_device_get_fusion_info(dev);
154        if (ggml_metal_fusion_info_stats(res->finfo)) {
155            ggml_metal_fusion_info_labels_init(res->finfo);
156            res->n_cb = 0;
157        }
158
159        GGML_LOG_INFO("%s: use fusion         = %s\n", __func__, ggml_metal_fusion_info_enabled(res->finfo) ? "true" : "false");
160        GGML_LOG_INFO("%s: use concurrency    = %s\n", __func__, res->use_concurrency    ? "true" : "false");
161        GGML_LOG_INFO("%s: use graph optimize = %s\n", __func__, res->use_graph_optimize ? "true" : "false");
162
163        res->capture_compute = 0;
164        res->capture_started = false;
165        res->capture_scope = nil;
166
167        {
168            const char * val = getenv("GGML_METAL_CAPTURE_COMPUTE");
169            if (val) {
170                res->capture_compute = atoi(val);
171            }
172        }
173
174        res->has_error = false;
175
176        res->gf = nil;
177        res->encode_async = nil;
178        for (int i = 0; i < GGML_METAL_MAX_COMMAND_BUFFERS; ++i) {
179            res->cmd_bufs[i].obj = nil;
180        }
181
182        res->cmd_bufs_ext = [[NSMutableArray alloc] init];
183        res->buf_refs     = [[NSMutableArray alloc] init];
184
185        res->cmd_buf_last = nil;
186
187        res->pipelines_ext = ggml_metal_pipelines_init();
188
189        return res;
190    }
191}
192
193void ggml_metal_free(ggml_metal_t ctx) {
194    GGML_LOG_INFO("%s: deallocating\n", __func__);
195
196    for (int i = 0; i < GGML_METAL_MAX_COMMAND_BUFFERS; ++i) {
197        if (ctx->cmd_bufs[i].obj) {
198            [ctx->cmd_bufs[i].obj release];
199        }
200    }
201
202    for (int i = 0; i < (int) ctx->cmd_bufs_ext.count; ++i) {
203        if (ctx->cmd_bufs_ext[i]) {
204            [ctx->cmd_bufs_ext[i] release];
205        }
206    }
207
208    [ctx->cmd_bufs_ext removeAllObjects];
209    [ctx->cmd_bufs_ext release];
210
211    @autoreleasepool {
212        [ctx->buf_refs removeAllObjects];
213        [ctx->buf_refs release];
214    }
215
216    if (ctx->pipelines_ext) {
217        ggml_metal_pipelines_free(ctx->pipelines_ext);
218        ctx->pipelines_ext = nil;
219    }
220
221    if (ggml_metal_fusion_info_debug(ctx->finfo) > 0) {
222        GGML_LOG_DEBUG("%s: fusion stats:\n", __func__);
223
224        const int n_fusions = ggml_metal_fusion_info_n_fusions(ctx->finfo);
225        for (int i = 0; i < n_fusions; i++) {
226            const uint64_t count = ggml_metal_fusion_info_count(ctx->finfo, i);
227            if (count == 0) {
228                continue;
229            }
230
231            // note: cannot use ggml_log here
232            GGML_LOG_DEBUG("%s: - %s: %" PRIu64 "\n", __func__, ggml_metal_fusion_info_label(ctx->finfo, i), count);
233        }
234    }
235
236    Block_release(ctx->encode_async);
237
238    //[ctx->queue release]; // [TAG_QUEUE_PER_BACKEND]
239
240    dispatch_release(ctx->d_queue);
241
242    ggml_metal_device_event_free(ctx->dev, ctx->ev_cpy);
243
244    free(ctx);
245}
246
247const char * ggml_metal_get_name(ggml_metal_t ctx) {
248    return ctx->name;
249}
250
251void ggml_metal_synchronize(ggml_metal_t ctx) {
252    // wait for any backend operations to finish
253    if (ctx->cmd_buf_last) {
254        [ctx->cmd_buf_last waitUntilCompleted];
255        ctx->cmd_buf_last = nil;
256    }
257
258    // check status of all command buffers
259    {
260        const int n_cb = ctx->n_cb;
261
262        for (int cb_idx = 0; cb_idx <= n_cb; ++cb_idx) {
263            id<MTLCommandBuffer> cmd_buf = ctx->cmd_bufs[cb_idx].obj;
264            if (!cmd_buf) {
265                continue;
266            }
267
268            MTLCommandBufferStatus status = [cmd_buf status];
269            if (status != MTLCommandBufferStatusCompleted) {
270                GGML_LOG_ERROR("%s: error: command buffer %d failed with status %d\n", __func__, cb_idx, (int) status);
271                if (status == MTLCommandBufferStatusError) {
272                    GGML_LOG_ERROR("error: %s\n", [[cmd_buf error].localizedDescription UTF8String]);
273                }
274                ctx->has_error = true;
275                return;
276            }
277        }
278    }
279
280    // release any completed extra command buffers
281    if (ctx->cmd_bufs_ext.count > 0) {
282        for (size_t i = 0; i < ctx->cmd_bufs_ext.count; ++i) {
283            id<MTLCommandBuffer> cmd_buf = ctx->cmd_bufs_ext[i];
284
285            MTLCommandBufferStatus status = [cmd_buf status];
286            if (status != MTLCommandBufferStatusCompleted) {
287                GGML_LOG_ERROR("%s: error: command buffer %d failed with status %d\n", __func__, (int) i, (int) status);
288                if (status == MTLCommandBufferStatusError) {
289                    GGML_LOG_ERROR("error: %s\n", [[cmd_buf error].localizedDescription UTF8String]);
290                }
291
292                // release this and all remaining command buffers before returning
293                for (size_t j = i; j < ctx->cmd_bufs_ext.count; ++j) {
294                    [ctx->cmd_bufs_ext[j] release];
295                }
296                [ctx->cmd_bufs_ext removeAllObjects];
297
298                ctx->has_error = true;
299                return;
300            }
301
302            [cmd_buf release];
303        }
304
305        [ctx->cmd_bufs_ext removeAllObjects];
306    }
307
308    @autoreleasepool {
309        [ctx->buf_refs removeAllObjects];
310    }
311}
312
313static struct ggml_metal_buffer_id ggml_metal_get_buffer_id(const struct ggml_tensor * t) {
314    if (!t) {
315        return (struct ggml_metal_buffer_id) { nil, 0 };
316    }
317
318    ggml_backend_buffer_t buffer = t->view_src ? t->view_src->buffer : t->buffer;
319
320    return ggml_metal_buffer_get_id(buffer->context, t);
321}
322
323void ggml_metal_set_tensor_async(ggml_metal_t ctx, struct ggml_tensor * tensor, const void * data, size_t offset, size_t size) {
324    @autoreleasepool {
325        // wrap the source data into a Metal buffer
326        id<MTLDevice> device = ggml_metal_device_get_obj(ctx->dev);
327        id<MTLBuffer> buf_src = [device newBufferWithBytes:data
328                                                    length:size
329                                                   options:MTLResourceStorageModeShared];
330
331        GGML_ASSERT(buf_src);
332
333        struct ggml_metal_buffer_id bid_dst = ggml_metal_get_buffer_id(tensor);
334        if (bid_dst.metal == nil) {
335            GGML_ABORT("%s: failed to find buffer for tensor '%s'\n", __func__, tensor->name);
336        }
337
338        bid_dst.offs += offset;
339
340        // queue the copy operation into the queue of the Metal context
341        // this will be queued at the end, after any currently ongoing GPU operations
342        id<MTLCommandQueue> queue = ggml_metal_device_get_queue(ctx->dev);
343        id<MTLCommandBuffer> cmd_buf = [queue commandBuffer];
344        id<MTLBlitCommandEncoder> encoder = [cmd_buf blitCommandEncoder];
345
346        [encoder copyFromBuffer:buf_src
347                   sourceOffset:0
348                       toBuffer:bid_dst.metal
349              destinationOffset:bid_dst.offs
350                           size:size];
351
352        [encoder endEncoding];
353        [cmd_buf commit];
354
355        [ctx->buf_refs addObject:buf_src];
356        [buf_src release];
357
358        // do not wait here for completion
359        //[cmd_buf waitUntilCompleted];
360
361        // instead, remember a reference to the command buffer and wait for it later if needed
362        [ctx->cmd_bufs_ext addObject:cmd_buf];
363        ctx->cmd_buf_last = cmd_buf;
364
365        [cmd_buf retain];
366    }
367}
368
369void ggml_metal_get_tensor_async(ggml_metal_t ctx, const struct ggml_tensor * tensor, void * data, size_t offset, size_t size) {
370    @autoreleasepool {
371        id<MTLDevice> device = ggml_metal_device_get_obj(ctx->dev);
372        id<MTLBuffer> buf_dst = [device newBufferWithBytesNoCopy:data
373                                                          length:size
374                                                         options:MTLResourceStorageModeShared
375                                                     deallocator:nil];
376
377        GGML_ASSERT(buf_dst);
378
379        struct ggml_metal_buffer_id bid_src = ggml_metal_get_buffer_id(tensor);
380        if (bid_src.metal == nil) {
381            GGML_ABORT("%s: failed to find buffer for tensor '%s'\n", __func__, tensor->name);
382        }
383
384        bid_src.offs += offset;
385
386        // queue the copy operation into the queue of the Metal context
387        // this will be queued at the end, after any currently ongoing GPU operations
388        id<MTLCommandQueue> queue = ggml_metal_device_get_queue(ctx->dev);
389        id<MTLCommandBuffer> cmd_buf = [queue commandBuffer];
390        id<MTLBlitCommandEncoder> encoder = [cmd_buf blitCommandEncoder];
391
392        [encoder copyFromBuffer:bid_src.metal
393                   sourceOffset:bid_src.offs
394                       toBuffer:buf_dst
395              destinationOffset:0
396                           size:size];
397
398        [encoder endEncoding];
399        [cmd_buf commit];
400
401        [ctx->buf_refs addObject:buf_dst];
402        [buf_dst release];
403
404        // do not wait here for completion
405        //[cmd_buf waitUntilCompleted];
406
407        // instead, remember a reference to the command buffer and wait for it later if needed
408        [ctx->cmd_bufs_ext addObject:cmd_buf];
409        ctx->cmd_buf_last = cmd_buf;
410
411        [cmd_buf retain];
412    }
413}
414
415bool ggml_metal_cpy_tensor_async(ggml_metal_t ctx_src, ggml_metal_t ctx_dst, const struct ggml_tensor * src, struct ggml_tensor * dst) {
416    @autoreleasepool {
417        struct ggml_metal_buffer_id bid_src = ggml_metal_get_buffer_id(src);
418        struct ggml_metal_buffer_id bid_dst = ggml_metal_get_buffer_id(dst);
419
420        if (bid_src.metal == nil || bid_dst.metal == nil) {
421            return false;
422        }
423
424        // queue the copy operation into the Metal context
425        // this will be queued at the end, after any currently ongoing GPU operations
426        id<MTLCommandQueue> queue = ggml_metal_device_get_queue(ctx_src->dev);
427        id<MTLCommandBuffer> cmd_buf = [queue commandBuffer];
428        id<MTLBlitCommandEncoder> encoder = [cmd_buf blitCommandEncoder];
429
430        [encoder copyFromBuffer:bid_src.metal
431                   sourceOffset:bid_src.offs
432                       toBuffer:bid_dst.metal
433              destinationOffset:bid_dst.offs
434                           size:ggml_nbytes(src)];
435
436        [encoder endEncoding];
437
438        ggml_metal_event_t ev_cpy = ggml_metal_get_ev_cpy(ctx_src);
439        ggml_metal_event_encode_signal(ev_cpy, cmd_buf);
440
441        [cmd_buf commit];
442
443        // do not wait here for completion
444        //[cmd_buf waitUntilCompleted];
445
446        // instead, remember a reference to the command buffer and wait for it later if needed
447        [ctx_src->cmd_bufs_ext addObject:cmd_buf];
448        ctx_src->cmd_buf_last = cmd_buf;
449
450        [cmd_buf retain];
451
452        ggml_metal_event_wait(ctx_dst, ev_cpy);
453
454        return true;
455    }
456}
457
458enum ggml_status ggml_metal_graph_compute(ggml_metal_t ctx, struct ggml_cgraph * gf) {
459    if (ctx->has_error) {
460        GGML_LOG_ERROR("%s: backend is in error state from a previous command buffer failure - recreate the backend to recover\n", __func__);
461        return GGML_STATUS_FAILED;
462    }
463
464    // number of nodes encoded by the main thread (empirically determined)
465    const int n_main = MAX(64, 0.1*gf->n_nodes);
466
467    // number of threads in addition to the main thread
468    const int n_cb = ctx->n_cb;
469
470    // keep the memory wired
471    ggml_metal_device_rsets_keep_alive(ctx->dev);
472
473    // submit the ggml compute graph to the GPU by creating command buffers and encoding the ops in them
474    // the first n_nodes_0 are encoded and submitted for processing directly by the calling thread
475    // while these nodes are processing, we start n_cb threads to enqueue the rest of the nodes
476    // each thread creates it's own command buffer and enqueues the ops in parallel
477    //
478    // tests on M1 Pro and M2 Ultra using LLaMA models, show that optimal values for n_cb are 1 or 2
479
480    @autoreleasepool {
481        ctx->gf = gf;
482
483        if (ctx->n_cb == 0) {
484            // single-threaded encoding: the whole graph is encoded by one command buffer
485            ctx->n_nodes_0      = gf->n_nodes;
486            ctx->n_nodes_1      = 0;
487            ctx->n_nodes_per_cb = 0;
488        } else {
489            ctx->n_nodes_0      = MIN(n_main, gf->n_nodes);
490            ctx->n_nodes_1      = gf->n_nodes - ctx->n_nodes_0;
491
492            ctx->n_nodes_per_cb = (ctx->n_nodes_1 + ctx->n_cb - 1) / ctx->n_cb;
493        }
494
495        if (ctx->capture_compute >= 0) {
496            ctx->capture_compute--;
497        }
498
499        const bool use_capture = ctx->capture_compute == 0;
500        if (use_capture) {
501            ctx->capture_compute = -1;
502
503            // make sure all previous computations have finished before starting the capture
504            if (ctx->cmd_buf_last) {
505                [ctx->cmd_buf_last waitUntilCompleted];
506                ctx->cmd_buf_last = nil;
507            }
508
509            if (!ctx->capture_started) {
510                NSString * path = [NSString stringWithFormat:@"/tmp/perf-metal-%d.gputrace", getpid()];
511
512                GGML_LOG_WARN("%s: capturing graph in %s\n", __func__, [path UTF8String]);
513
514                // create capture scope
515                id<MTLDevice> device = ggml_metal_device_get_obj(ctx->dev);
516                ctx->capture_scope = [[MTLCaptureManager sharedCaptureManager] newCaptureScopeWithDevice:device];
517
518                MTLCaptureDescriptor * descriptor = [MTLCaptureDescriptor new];
519                descriptor.captureObject = ctx->capture_scope;
520                descriptor.destination = MTLCaptureDestinationGPUTraceDocument;
521                descriptor.outputURL = [NSURL fileURLWithPath:path];
522
523                NSError * error = nil;
524                if (![[MTLCaptureManager sharedCaptureManager] startCaptureWithDescriptor:descriptor error:&error]) {
525                    GGML_LOG_ERROR("%s: error: unable to start capture '%s'\n", __func__, [[error localizedDescription] UTF8String]);
526                } else {
527                    [ctx->capture_scope beginScope];
528                    ctx->capture_started = true;
529                }
530            }
531        }
532
533        // short-hand
534        id<MTLCommandQueue> queue = ggml_metal_device_get_queue(ctx->dev);
535
536        // the main thread commits the first few commands immediately
537        // cmd_buf[n_cb]
538        {
539            id<MTLCommandBuffer> cmd_buf = [queue commandBufferWithUnretainedReferences];
540            [cmd_buf retain];
541
542            if (ctx->cmd_bufs[n_cb].obj) {
543                [ctx->cmd_bufs[n_cb].obj release];
544            }
545            ctx->cmd_bufs[n_cb].obj = cmd_buf;
546
547            [cmd_buf enqueue];
548
549            ctx->encode_async(n_cb);
550        }
551
552        // remember the command buffer for the next iteration
553        ctx->cmd_buf_last = ctx->cmd_bufs[n_cb].obj;
554
555        // prepare the rest of the command buffers asynchronously (optional)
556        // cmd_buf[0.. n_cb)
557        for (int cb_idx = 0; cb_idx < n_cb; ++cb_idx) {
558            id<MTLCommandBuffer> cmd_buf = [queue commandBufferWithUnretainedReferences];
559            [cmd_buf retain];
560
561            if (ctx->cmd_bufs[cb_idx].obj) {
562                [ctx->cmd_bufs[cb_idx].obj release];
563            }
564            ctx->cmd_bufs[cb_idx].obj = cmd_buf;
565
566            // always enqueue the first two command buffers
567            // enqueue all of the command buffers if we don't need to abort
568            if (cb_idx < 2 || ctx->abort_callback == NULL) {
569                [cmd_buf enqueue];
570
571                // update the pointer to the last queued command buffer
572                // this is needed to implement synchronize()
573                ctx->cmd_buf_last = cmd_buf;
574            }
575        }
576
577        dispatch_apply(n_cb, ctx->d_queue, ctx->encode_async);
578
579        // for debugging: block until graph is computed
580        //[ctx->cmd_buf_last waitUntilCompleted];
581
582        // enter here only when capturing in order to wait for all computation to finish
583        // otherwise, we leave the graph to compute asynchronously
584        if (use_capture && ctx->capture_started) {
585            // wait for completion and check status of each command buffer
586            // needed to detect if the device ran out-of-memory for example (#1881)
587            {
588                id<MTLCommandBuffer> cmd_buf = ctx->cmd_bufs[n_cb].obj;
589                [cmd_buf waitUntilCompleted];
590
591                MTLCommandBufferStatus status = [cmd_buf status];
592                if (status != MTLCommandBufferStatusCompleted) {
593                    GGML_LOG_INFO("%s: command buffer %d failed with status %lu\n", __func__, n_cb, status);
594                    if (status == MTLCommandBufferStatusError) {
595                        GGML_LOG_INFO("error: %s\n", [[cmd_buf error].localizedDescription UTF8String]);
596                    }
597
598                    return GGML_STATUS_FAILED;
599                }
600            }
601
602            for (int i = 0; i < n_cb; ++i) {
603                id<MTLCommandBuffer> cmd_buf = ctx->cmd_bufs[i].obj;
604                [cmd_buf waitUntilCompleted];
605
606                MTLCommandBufferStatus status = [cmd_buf status];
607                if (status != MTLCommandBufferStatusCompleted) {
608                    GGML_LOG_INFO("%s: command buffer %d failed with status %lu\n", __func__, i, status);
609                    if (status == MTLCommandBufferStatusError) {
610                        GGML_LOG_INFO("error: %s\n", [[cmd_buf error].localizedDescription UTF8String]);
611                    }
612
613                    return GGML_STATUS_FAILED;
614                }
615
616                id<MTLCommandBuffer> next_buffer = (i + 1 < n_cb ? ctx->cmd_bufs[i + 1].obj : nil);
617                if (!next_buffer) {
618                    continue;
619                }
620
621                const bool next_queued = ([next_buffer status] != MTLCommandBufferStatusNotEnqueued);
622                if (next_queued) {
623                    continue;
624                }
625
626                if (ctx->abort_callback && ctx->abort_callback(ctx->abort_callback_data)) {
627                    GGML_LOG_INFO("%s: command buffer %d aborted", __func__, i);
628                    return GGML_STATUS_ABORTED;
629                }
630
631                [next_buffer commit];
632            }
633
634            [ctx->capture_scope endScope];
635            [[MTLCaptureManager sharedCaptureManager] stopCapture];
636
637            ctx->capture_started = false;
638        }
639    }
640
641    return GGML_STATUS_SUCCESS;
642}
643
644void ggml_metal_graph_optimize(ggml_metal_t ctx, struct ggml_cgraph * gf) {
645    //const int64_t t_start = ggml_time_us();
646
647    if (ctx->use_graph_optimize) {
648        ggml_graph_optimize(gf);
649    }
650
651    //printf("%s: graph optimize took %.3f ms\n", __func__, (ggml_time_us() - t_start) / 1000.0);
652}
653
654void ggml_metal_event_record(ggml_metal_t ctx, ggml_metal_event_t ev) {
655    @autoreleasepool {
656        id<MTLCommandQueue> queue = ggml_metal_device_get_queue(ctx->dev);
657        id<MTLCommandBuffer> cmd_buf = [queue commandBuffer];
658
659        ggml_metal_event_encode_signal(ev, cmd_buf);
660
661        [cmd_buf commit];
662
663        [ctx->cmd_bufs_ext addObject:cmd_buf];
664        ctx->cmd_buf_last = cmd_buf;
665
666        [cmd_buf retain];
667    }
668}
669
670void ggml_metal_event_wait(ggml_metal_t ctx, ggml_metal_event_t ev) {
671    @autoreleasepool {
672        id<MTLCommandQueue> queue = ggml_metal_device_get_queue(ctx->dev);
673        id<MTLCommandBuffer> cmd_buf = [queue commandBuffer];
674
675        ggml_metal_event_encode_wait(ev, cmd_buf);
676
677        [cmd_buf commit];
678
679        [ctx->cmd_bufs_ext addObject:cmd_buf];
680        ctx->cmd_buf_last = cmd_buf;
681
682        [cmd_buf retain];
683    }
684}
685
686ggml_metal_event_t ggml_metal_get_ev_cpy(ggml_metal_t ctx) {
687    return ctx->ev_cpy;
688}
689
690void ggml_metal_set_n_cb(ggml_metal_t ctx, int n_cb) {
691    // when fusion stats are collected the graph must be encoded by a single thread so the
692    // counters are race-free; override whatever the caller requested
693    if (ggml_metal_fusion_info_stats(ctx->finfo)) {
694        n_cb = 0;
695    }
696
697    if (ctx->n_cb != n_cb) {
698        ctx->n_cb = MIN(n_cb, GGML_METAL_MAX_COMMAND_BUFFERS);
699
700        if (ctx->n_cb > 2) {
701            GGML_LOG_WARN("%s: n_cb = %d, using n_cb > 2 is not recommended and can degrade the performance in some cases\n", __func__, n_cb);
702        }
703    }
704
705    if (ctx->encode_async) {
706        Block_release(ctx->encode_async);
707    }
708
709    ctx->encode_async = Block_copy(^(size_t iter) {
710        const int cb_idx = iter;
711        const int n_cb_l = ctx->n_cb;
712
713        const int n_nodes_0 = ctx->n_nodes_0;
714        const int n_nodes_1 = ctx->n_nodes_1;
715
716        const int n_nodes_per_cb = ctx->n_nodes_per_cb;
717
718        int idx_start = 0;
719        int idx_end   = n_nodes_0;
720
721        if (cb_idx < n_cb_l) {
722            idx_start = n_nodes_0 + (                                         (cb_idx + 0) * n_nodes_per_cb);
723            idx_end   = n_nodes_0 + (MIN((cb_idx == n_cb_l - 1) ? n_nodes_1 : (cb_idx + 1) * n_nodes_per_cb, n_nodes_1));
724        }
725
726        id<MTLCommandBuffer> cmd_buf = ctx->cmd_bufs[cb_idx].obj;
727
728        ggml_metal_op_t ctx_op = ggml_metal_op_init(
729            ctx->dev,
730            cmd_buf,
731            ctx->gf,
732            ctx->finfo,
733            idx_start,
734            idx_end,
735            ctx->use_concurrency,
736            ctx->capture_compute,
737            ctx->debug_graph);
738
739        for (int idx = 0; idx < ggml_metal_op_n_nodes(ctx_op); ++idx) {
740            const int res = ggml_metal_op_encode(ctx_op, idx);
741            if (res == 0) {
742                break;
743            }
744
745            idx += res - 1;
746        }
747
748        ggml_metal_op_free(ctx_op);
749
750        if (cb_idx < 2 || ctx->abort_callback == NULL) {
751            [cmd_buf commit];
752        }
753    });
754}
755
756void ggml_metal_set_abort_callback(ggml_metal_t ctx, ggml_abort_callback abort_callback, void * user_data) {
757    ctx->abort_callback = abort_callback;
758    ctx->abort_callback_data = user_data;
759}
760
761bool ggml_metal_supports_family(ggml_metal_t ctx, int family) {
762    GGML_ASSERT(ctx->dev != nil);
763
764    id<MTLDevice> device = ggml_metal_device_get_obj(ctx->dev);
765
766    return [device supportsFamily:(MTLGPUFamilyApple1 + family - 1)];
767}
768
769void ggml_metal_capture_next_compute(ggml_metal_t ctx) {
770    ctx->capture_compute = 1;
771}