Files
OpenBLAS/driver/others/openblas_cancel.c
Keno FischerandClaude Fable 5 7a07c8e1f4 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
2026-07-19 04:20:28 +00:00

189 lines
7.8 KiB
C

/*****************************************************************************
Copyright (c) 2026, The OpenBLAS Project
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
3. Neither the name of the OpenBLAS project nor the names of
its contributors may be used to endorse or promote products
derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**********************************************************************************/
#include <stddef.h>
#include "common.h"
/* Cooperative cancellation of in-flight OpenBLAS operations.
*
* Every thread owns a pointer-sized generation slot in thread-local
* storage:
*
* - openblas_cancel_token() returns the address of the calling thread's
* slot. The address is stable for the lifetime of the thread.
* - Instrumented compute drivers advance the slot to a fresh even value
* at operation entry (on the issuing thread) and poll it at block
* granularity: any change - the cancel bit or a later generation -
* makes the operation abandon its remaining work and return early.
* - openblas_cancel(token, loaded_token) requests cancellation of the
* operation that was in flight when the caller loaded *token: it
* atomically sets the cancel bit iff the slot still holds
* loaded_token. A racing or stale request (the operation completed;
* a new one may have started) either fails the compare-exchange or
* dirties an already-dead generation - both harmless. May be called
* from any thread.
*
* A cancelled operation returns quickly but leaves its output buffers in
* an unspecified, partially-updated state; the caller is responsible for
* discarding the result. The library itself remains in a consistent
* state and can service further calls.
*
* These symbols are deliberately exported without any SYMBOLPREFIX /
* SYMBOLSUFFIX decoration, like the other openblas_* utility functions
* that are not per-precision entry points.
*/
/* Storage for the per-thread generation slot. Compiler-emitted TLS is
* used where the target supports it (clang reports per-target support
* via __has_feature(tls); e.g. 32-bit iOS with an old deployment minimum
* lacks it). Where it does not, the slot falls back to a
* pthread_getspecific() lookup of a lazily heap-allocated slot: slightly
* more expensive at operation entry, but the compute drivers cache the
* slot's address, so the polling fast path is identical. */
#if defined(_MSC_VER) && !defined(__clang__)
#define OPENBLAS_CANCEL_TLS __declspec(thread)
#elif defined(__clang__)
#if __has_feature(tls)
#define OPENBLAS_CANCEL_TLS __thread
#endif
#elif defined(__GNUC__) || defined(__SUNPRO_C) || defined(__xlC__)
#define OPENBLAS_CANCEL_TLS __thread
#endif
#ifdef OPENBLAS_CANCEL_TLS
/* This thread's generation slot. Bit 0 is the cancel flag; the remaining
* bits count operations issued by this thread. */
static OPENBLAS_CANCEL_TLS size_t openblas_cancel_slot = 0;
/* Address of the calling thread's generation slot. */
static size_t *openblas_cancel_slot_addr(void) {
return (size_t *)&openblas_cancel_slot;
}
#else
#include <pthread.h>
#include <stdlib.h>
static pthread_key_t openblas_cancel_key;
static pthread_once_t openblas_cancel_key_once = PTHREAD_ONCE_INIT;
static int openblas_cancel_key_ok = 0;
static void openblas_cancel_key_init(void) {
openblas_cancel_key_ok = (pthread_key_create(&openblas_cancel_key, free) == 0);
}
/* Address of the calling thread's generation slot, allocated on first
* use and freed by the key destructor at thread exit. Returns NULL if
* per-thread state cannot be set up; cancellation is then unavailable
* on this thread (all other entry points accept a NULL slot). */
static size_t *openblas_cancel_slot_addr(void) {
size_t *slot;
pthread_once(&openblas_cancel_key_once, openblas_cancel_key_init);
if (!openblas_cancel_key_ok) return NULL;
slot = (size_t *)pthread_getspecific(openblas_cancel_key);
if (slot == NULL) {
slot = (size_t *)calloc(1, sizeof(size_t));
if (slot == NULL) return NULL;
if (pthread_setspecific(openblas_cancel_key, slot) != 0) {
free(slot);
return NULL;
}
}
return slot;
}
#endif
#if defined(__GNUC__) || defined(__clang__)
#define OPENBLAS_CANCEL_LOAD(PTR) __atomic_load_n((PTR), __ATOMIC_RELAXED)
#define OPENBLAS_CANCEL_STORE(PTR, VAL) __atomic_store_n((PTR), (VAL), __ATOMIC_RELEASE)
#define OPENBLAS_CANCEL_CAS(PTR, EXPP, VAL) \
__atomic_compare_exchange_n((PTR), (EXPP), (VAL), 0, __ATOMIC_RELEASE, __ATOMIC_RELAXED)
#else
/* Fallback for compilers without the GNU atomic builtins. The slot is
* only advanced by its owning thread; the cancel request degrades to a
* check-then-store, which can at worst dirty a dead generation. */
#define OPENBLAS_CANCEL_LOAD(PTR) (*(volatile size_t *)(PTR))
#define OPENBLAS_CANCEL_STORE(PTR, VAL) do { WMB; *(volatile size_t *)(PTR) = (VAL); } while (0)
static int openblas_cancel_cas_fallback(size_t *ptr, size_t *expp, size_t val) {
if (*(volatile size_t *)ptr != *expp) return 0;
WMB;
*(volatile size_t *)ptr = val;
return 1;
}
#define OPENBLAS_CANCEL_CAS(PTR, EXPP, VAL) openblas_cancel_cas_fallback((PTR), (EXPP), (VAL))
#endif
/* Address of the calling thread's generation slot (NULL if unavailable). */
size_t *openblas_cancel_token(void) {
return openblas_cancel_slot_addr();
}
/* Request cancellation of the operation that was in flight on the token's
* thread when the caller loaded LOADED_TOKEN from it. May be called from
* any thread. */
void openblas_cancel(size_t *token, size_t loaded_token) {
if (token == NULL) return;
OPENBLAS_CANCEL_CAS(token, &loaded_token, loaded_token | (size_t)1);
}
/* --- internal helpers (not part of the public API) --------------------- */
/* Begin an instrumented operation on the calling thread: advance the slot
* past any stale cancel bit to a fresh even generation and return it.
* Returns 0 if the thread has no slot (paired with a NULL slot from
* openblas_cancel_self, so polling never reports cancellation). */
size_t openblas_cancel_begin(void) {
size_t *slot = openblas_cancel_slot_addr();
size_t gen;
if (slot == NULL) return 0;
gen = (*slot | (size_t)1) + 1;
OPENBLAS_CANCEL_STORE(slot, gen);
return gen;
}
/* The calling thread's slot address (same as openblas_cancel_token, for
* internal use without going through the exported symbol). */
size_t *openblas_cancel_self(void) {
return openblas_cancel_slot_addr();
}
/* Cheap poll usable from compute drivers: nonzero once GEN is no longer
* the live value of SLOT (cancelled, or superseded). SLOT may be NULL. */
int openblas_cancel_poll(size_t *slot, size_t gen) {
if (slot == NULL) return 0;
return OPENBLAS_CANCEL_LOAD(slot) != gen;
}