Skip to content

Commit 0633bbc

Browse files
Kenoclaude
andcommitted
Add an asynchronous cancellation API for in-flight operations
Long-running BLAS calls (a large gemm can run for minutes) cannot currently be interrupted: callers embedding OpenBLAS (e.g. the Julia runtime responding to a user's ^C) can only wait for completion or kill the process. Add a minimal cooperative cancellation protocol: Every thread owns a pointer-sized generation slot in thread-local storage, whose stable address is returned by openblas_cancel_token(). Instrumented compute drivers advance the slot to a fresh even generation at operation entry on the issuing thread (forwarding the slot and generation to worker threads through blas_arg_t) and poll it at block granularity. openblas_cancel(token, loaded_token) - callable from any thread - sets the cancel bit (bit 0) iff the slot still holds loaded_token, so a canceller that loaded the value while an operation was in flight stops exactly that operation, while stale or racing requests either miss or dirty an already-dead generation, both harmless. There is no object lifecycle: nothing to allocate, bind, reset, or free. A cancelled operation returns quickly, leaving its output buffer in an unspecified partially-updated state that the caller must discard; every synchronization point in the threaded driver is still executed, so sibling threads never stall and the library remains consistent for subsequent calls. Coverage: the level-3 gemm/symm/hemm drivers (level3.c and level3_thread.c). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012nCkyKUguncLLJrH9K5o7m
1 parent 62bcfb0 commit 0633bbc

9 files changed

Lines changed: 342 additions & 19 deletions

File tree

cblas.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ typedef void (*openblas_dojob_callback)(int thread_num, void *jobdata, int dojob
5959
typedef void (*openblas_threads_callback)(int sync, openblas_dojob_callback dojob, int numjobs, size_t jobdata_elsize, void *jobdata, int dojob_data);
6060
void openblas_set_threads_callback_function(openblas_threads_callback callback);
6161

62+
/* Cooperative cancellation of in-flight operations.
63+
*
64+
* Every thread owns a pointer-sized generation slot;
65+
* openblas_cancel_token() returns its (stable) address for the calling
66+
* thread. Supported compute drivers (currently the level-3 GEMM drivers)
67+
* advance the slot to a fresh even generation at operation entry on the
68+
* issuing thread and poll it during the computation. To cancel the
69+
* operation in flight on a thread, load that thread's slot value, verify
70+
* the operation you mean to cancel is still the current one, and call
71+
* openblas_cancel(token, loaded_value): the cancel bit (bit 0) is set iff
72+
* the slot still holds the loaded value, so stale requests cannot affect
73+
* later operations. A cancelled operation leaves its output in an
74+
* unspecified, partially-updated state; the caller must discard the
75+
* result. The library itself stays consistent and can service further
76+
* calls. openblas_cancel_token() may return NULL if per-thread state
77+
* cannot be allocated; cancellation is then unavailable on that thread
78+
* (openblas_cancel(NULL, ...) is a harmless no-op). These symbols are
79+
* exported without any symbol prefix/suffix decoration. */
80+
size_t *openblas_cancel_token(void);
81+
void openblas_cancel(size_t *token, size_t loaded_token);
82+
6283
#ifdef OPENBLAS_OS_LINUX
6384
/* Sets thread affinity for OpenBLAS threads. `thread_idx` is in [0, openblas_get_num_threads()-1]. */
6485
int openblas_setaffinity(int thread_idx, size_t cpusetsize, cpu_set_t* cpu_set);

common.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,17 @@ int get_node_equal (void);
720720

721721
void goto_set_num_threads(int);
722722

723+
/* Cooperative cancellation of in-flight operations
724+
* (implemented in driver/others/openblas_cancel.c). These symbols are
725+
* exported without SYMBOLPREFIX/SYMBOLSUFFIX decoration. */
726+
size_t *openblas_cancel_token(void);
727+
void openblas_cancel(size_t *token, size_t loaded_token);
728+
729+
/* Internal helpers for the instrumented compute drivers. */
730+
size_t openblas_cancel_begin(void);
731+
size_t *openblas_cancel_self(void);
732+
int openblas_cancel_poll(size_t *slot, size_t gen);
733+
723734
void gotoblas_affinity_init(void);
724735
void gotoblas_affinity_quit(void);
725736
void gotoblas_dynamic_init(void);

common_macro.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2763,6 +2763,13 @@ typedef struct {
27632763
void * routine;
27642764
int routine_mode;
27652765

2766+
/* Generation slot of the thread that issued this operation and the
2767+
generation it runs as (see driver/others/openblas_cancel.c), or
2768+
NULL/0. Consulted only by the level-3 thread drivers; must be set
2769+
explicitly wherever it is to be observed. */
2770+
size_t * cancel_slot;
2771+
size_t cancel_gen;
2772+
27662773
} blas_arg_t;
27672774
#endif
27682775

driver/level3/level3.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,15 @@ int CNAME(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
214214
double total;
215215
#endif
216216

217+
/* Begin a cancellable generation on the calling thread and poll it at
218+
* block granularity; openblas_cancel() against this thread's slot makes
219+
* the operation return early, leaving C partially updated. (When this
220+
* driver runs on a BLAS worker thread on behalf of another operation,
221+
* the generation is the worker's own, which nobody cancels; the outer
222+
* operation's cancellation is handled by the threading driver.) */
223+
size_t cancel_gen = openblas_cancel_begin();
224+
size_t *cancel_slot = openblas_cancel_self();
225+
217226
k = K;
218227

219228
a = (IFLOAT *)A;
@@ -303,11 +312,15 @@ int CNAME(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
303312
#endif
304313

305314
for(js = n_from; js < n_to; js += GEMM_R){
315+
if (openblas_cancel_poll(cancel_slot, cancel_gen)) return 0;
316+
306317
min_j = n_to - js;
307318
if (min_j > GEMM_R) min_j = GEMM_R;
308319

309320
for(ls = 0; ls < k; ls += min_l){
310321

322+
if (openblas_cancel_poll(cancel_slot, cancel_gen)) return 0;
323+
311324
min_l = k - ls;
312325

313326
if (min_l >= GEMM_Q * 2) {
@@ -354,6 +367,8 @@ int CNAME(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
354367

355368
#else
356369
for(jjs = js; jjs < js + min_j; jjs += min_jj){
370+
if (openblas_cancel_poll(cancel_slot, cancel_gen)) return 0;
371+
357372
min_jj = min_j + js - jjs;
358373
#if defined(SKYLAKEX) || defined(COOPERLAKE) || defined(SAPPHIRERAPIDS)
359374
/* the current AVX512 s/d/c/z GEMM kernel requires n>=6*GEMM_UNROLL_N to achieve best performance */
@@ -391,6 +406,8 @@ int CNAME(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
391406
#endif
392407

393408
for(is = m_from + min_i; is < m_to; is += min_i){
409+
if (openblas_cancel_poll(cancel_slot, cancel_gen)) return 0;
410+
394411
min_i = m_to - is;
395412

396413
if (min_i >= GEMM_P * 2) {

driver/level3/level3_thread.c

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,17 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
250250
FLOAT *c;
251251
job_t *job = (job_t *)args -> common;
252252

253+
/* Cancellation token forwarded (by gemm_driver) from the thread that
254+
* issued this operation, or NULL. Once cancellation is observed the
255+
* remaining compute (copy/kernel calls) is skipped, but every
256+
* synchronization point - buffer publication and flag clearing - is
257+
* still executed, so sibling threads never deadlock and the operation
258+
* finishes quickly with C left in an unspecified, partially-updated
259+
* state. */
260+
size_t *cancel_slot = args -> cancel_slot;
261+
size_t cancel_gen = args -> cancel_gen;
262+
int cancelled = 0;
263+
253264
BLASLONG nthreads_m;
254265
BLASLONG mypos_m, mypos_n;
255266
BLASLONG divide_rate = DIVIDE_RATE;
@@ -314,8 +325,10 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
314325
n_to = range_n[mypos + 1];
315326
}
316327

317-
/* Multiply C by beta if needed */
318-
if (beta) {
328+
cancelled = openblas_cancel_poll(cancel_slot, cancel_gen);
329+
330+
/* Multiply C by beta if needed (skipped when cancelled) */
331+
if (beta && !cancelled) {
319332
#ifndef COMPLEX
320333
if (beta[0] != ONE)
321334
#else
@@ -342,6 +355,9 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
342355
/* Iterate through steps of k */
343356
for(ls = 0; ls < k; ls += min_l){
344357

358+
/* Poll for cancellation at block granularity */
359+
if (!cancelled) cancelled = openblas_cancel_poll(cancel_slot, cancel_gen);
360+
345361
/* Determine step size in k */
346362
min_l = k - ls;
347363
if (min_l >= GEMM_Q * 2) {
@@ -371,10 +387,12 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
371387
}
372388
}
373389

374-
/* Copy local region of A into workspace */
375-
START_RPCC();
376-
ICOPY_OPERATION(min_l, min_i, a, lda, ls, m_from, sa);
377-
STOP_RPCC(copy_A);
390+
/* Copy local region of A into workspace (skipped when cancelled) */
391+
if (!cancelled) {
392+
START_RPCC();
393+
ICOPY_OPERATION(min_l, min_i, a, lda, ls, m_from, sa);
394+
STOP_RPCC(copy_A);
395+
}
378396

379397
/* Copy local region of B into workspace and apply kernel */
380398
div_n = (n_to - n_from + divide_rate - 1) / divide_rate;
@@ -389,9 +407,13 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
389407

390408
#if defined(FUSED_GEMM) && !defined(TIMING)
391409

392-
/* Fused operation to copy region of B into workspace and apply kernel */
393-
FUSED_KERNEL_OPERATION(min_i, MIN(n_to, js + div_n) - js, min_l, alpha,
394-
sa, buffer[bufferside], b, ldb, c, ldc, m_from, js, ls);
410+
/* Fused operation to copy region of B into workspace and apply kernel
411+
* (skipped when cancelled; the buffer is still published below so
412+
* sibling threads do not stall) */
413+
if (!cancelled) cancelled = openblas_cancel_poll(cancel_slot, cancel_gen);
414+
if (!cancelled)
415+
FUSED_KERNEL_OPERATION(min_i, MIN(n_to, js + div_n) - js, min_l, alpha,
416+
sa, buffer[bufferside], b, ldb, c, ldc, m_from, js, ls);
395417

396418
#else
397419

@@ -410,6 +432,12 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
410432
*/
411433
if (min_jj > GEMM_UNROLL_N) min_jj = GEMM_UNROLL_N;
412434
#endif
435+
/* Poll for cancellation between blocks; when cancelled, skip the
436+
* copy and kernel but keep iterating so the buffer is still
437+
* published below and sibling threads do not stall */
438+
if (!cancelled) cancelled = openblas_cancel_poll(cancel_slot, cancel_gen);
439+
if (!cancelled) {
440+
413441
/* Copy part of local region of B into workspace */
414442
START_RPCC();
415443
OCOPY_OPERATION(min_l, min_jj, b, ldb, ls, jjs,
@@ -427,6 +455,8 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
427455
ops += 2 * min_i * min_jj * min_l;
428456
#endif
429457

458+
}
459+
430460
}
431461
#endif
432462

@@ -456,7 +486,10 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
456486
STOP_RPCC(waiting2);
457487
MB;
458488

459-
/* Apply kernel with local region of A and part of other region of B */
489+
/* Apply kernel with local region of A and part of other region of B
490+
* (skipped when cancelled; the flag is still cleared below) */
491+
if (!cancelled) cancelled = openblas_cancel_poll(cancel_slot, cancel_gen);
492+
if (!cancelled) {
460493
START_RPCC();
461494
KERNEL_OPERATION(min_i, MIN(range_n[current + 1] - js, div_n), min_l, alpha,
462495
sa, (IFLOAT *)job[current].working[mypos][CACHE_LINE_SIZE * bufferside],
@@ -466,6 +499,7 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
466499
#ifdef TIMING
467500
ops += 2 * min_i * MIN(range_n[current + 1] - js, div_n) * min_l;
468501
#endif
502+
}
469503
}
470504

471505
/* Clear synchronization flag if this thread is done with other region of B */
@@ -476,9 +510,12 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
476510
}
477511
} while (current != mypos);
478512

479-
/* Iterate through steps of m
513+
/* Iterate through steps of m
480514
* Note: First step has already been finished */
481515
for(is = m_from + min_i; is < m_to; is += min_i){
516+
/* Poll for cancellation between blocks */
517+
if (!cancelled) cancelled = openblas_cancel_poll(cancel_slot, cancel_gen);
518+
482519
min_i = m_to - is;
483520
if (min_i >= GEMM_P * 2) {
484521
min_i = GEMM_P;
@@ -487,10 +524,12 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
487524
min_i = (((min_i + 1) / 2 + GEMM_UNROLL_M - 1)/GEMM_UNROLL_M) * GEMM_UNROLL_M;
488525
}
489526

490-
/* Copy local region of A into workspace */
491-
START_RPCC();
492-
ICOPY_OPERATION(min_l, min_i, a, lda, ls, is, sa);
493-
STOP_RPCC(copy_A);
527+
/* Copy local region of A into workspace (skipped when cancelled) */
528+
if (!cancelled) {
529+
START_RPCC();
530+
ICOPY_OPERATION(min_l, min_i, a, lda, ls, is, sa);
531+
STOP_RPCC(copy_A);
532+
}
494533

495534
/* Get regions of B and apply kernel */
496535
current = mypos;
@@ -500,17 +539,20 @@ static int inner_thread(blas_arg_t *args, BLASLONG *range_m, BLASLONG *range_n,
500539
div_n = (range_n[current + 1] - range_n[current] + divide_rate - 1) / divide_rate;
501540
for (js = range_n[current], bufferside = 0; js < range_n[current + 1]; js += div_n, bufferside ++) {
502541

503-
/* Apply kernel with local region of A and part of region of B */
542+
/* Apply kernel with local region of A and part of region of B
543+
* (skipped when cancelled; the flag is still cleared below) */
544+
if (!cancelled) {
504545
START_RPCC();
505546
KERNEL_OPERATION(min_i, MIN(range_n[current + 1] - js, div_n), min_l, alpha,
506547
sa, (IFLOAT *)job[current].working[mypos][CACHE_LINE_SIZE * bufferside],
507548
c, ldc, is, js);
508549
STOP_RPCC(kernel);
509-
550+
510551
#ifdef TIMING
511552
ops += 2 * min_i * MIN(range_n[current + 1] - js, div_n) * min_l;
512553
#endif
513-
554+
}
555+
514556
/* Clear synchronization flag if this thread is done with region of B */
515557
if (is + min_i >= m_to) {
516558
WMB;
@@ -688,6 +730,12 @@ static int gemm_driver(blas_arg_t *args, BLASLONG *range_m, BLASLONG
688730
newarg.beta = args -> beta;
689731
newarg.nthreads = args -> nthreads;
690732
newarg.common = (void *)job;
733+
734+
/* Begin a cancellable generation on this (the calling, i.e. issuing)
735+
* thread and forward it to the worker threads through the job
736+
* arguments. */
737+
newarg.cancel_gen = openblas_cancel_begin();
738+
newarg.cancel_slot = openblas_cancel_self();
691739
#ifdef PARAMTEST
692740
newarg.gemm_p = args -> gemm_p;
693741
newarg.gemm_q = args -> gemm_q;
@@ -791,6 +839,11 @@ static int gemm_driver(blas_arg_t *args, BLASLONG *range_m, BLASLONG
791839
WMB;
792840
/* Execute parallel computation */
793841
exec_blas(nthreads, queue);
842+
843+
/* Skip the remaining column blocks once this operation has been
844+
* cancelled. Each exec_blas() round is a full barrier, so bailing
845+
* out between rounds leaves the library in a consistent state. */
846+
if (openblas_cancel_poll(newarg.cancel_slot, newarg.cancel_gen)) break;
794847
}
795848

796849
#ifdef USE_ALLOC_HEAP

driver/others/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ set(COMMON_SOURCES
4141
openblas_env.c
4242
openblas_get_num_procs.c
4343
openblas_get_num_threads.c
44+
openblas_cancel.c
4445
blas_server_callback.c
4546
)
4647

driver/others/Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
TOPDIR = ../..
22
include ../../Makefile.system
33

4-
COMMONOBJS = memory.$(SUFFIX) xerbla.$(SUFFIX) c_abs.$(SUFFIX) z_abs.$(SUFFIX) openblas_set_num_threads.$(SUFFIX) openblas_get_num_threads.$(SUFFIX) openblas_get_num_procs.$(SUFFIX) openblas_get_config.$(SUFFIX) openblas_get_parallel.$(SUFFIX) openblas_error_handle.$(SUFFIX) openblas_env.$(SUFFIX) blas_server_callback.$(SUFFIX)
4+
COMMONOBJS = memory.$(SUFFIX) xerbla.$(SUFFIX) c_abs.$(SUFFIX) z_abs.$(SUFFIX) openblas_set_num_threads.$(SUFFIX) openblas_get_num_threads.$(SUFFIX) openblas_get_num_procs.$(SUFFIX) openblas_get_config.$(SUFFIX) openblas_get_parallel.$(SUFFIX) openblas_error_handle.$(SUFFIX) openblas_env.$(SUFFIX) openblas_cancel.$(SUFFIX) blas_server_callback.$(SUFFIX)
55

66
#COMMONOBJS += slamch.$(SUFFIX) slamc3.$(SUFFIX) dlamch.$(SUFFIX) dlamc3.$(SUFFIX)
77

@@ -172,6 +172,9 @@ openblas_error_handle.$(SUFFIX) : openblas_error_handle.c
172172
openblas_env.$(SUFFIX) : openblas_env.c
173173
$(CC) $(CFLAGS) -c $< -o $(@F)
174174

175+
openblas_cancel.$(SUFFIX) : openblas_cancel.c ../../common.h
176+
$(CC) $(CFLAGS) -c $< -o $(@F)
177+
175178
blasL1thread.$(SUFFIX) : blas_l1_thread.c ../../common.h ../../common_thread.h
176179
$(CC) $(CFLAGS) -c $< -o $(@F)
177180

0 commit comments

Comments
 (0)