Skip to content

Commit f6bd45e

Browse files
committed
[Fixup] Phase 1: enriched adstack overflow diagnostic - registry-id cmpxchg + kernel/task name in raise message
1 parent da060a2 commit f6bd45e

9 files changed

Lines changed: 274 additions & 30 deletions

File tree

quadrants/codegen/llvm/codegen_llvm.cpp

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1940,6 +1940,16 @@ std::string TaskCodeGenLLVM::init_offloaded_task_function(OffloadedStmt *stmt, s
19401940
func = llvm::Function::Create(task_function_type, llvm::Function::ExternalLinkage, task_kernel_name, module.get());
19411941

19421942
current_task = std::make_unique<OffloadedTask>(task_kernel_name);
1943+
// Pre-register the per-task AdStackSizingInfo so the registry id is assigned BEFORE codegen visits any
1944+
// `AdStackPushStmt`. Metadata (allocated_max_sizes) is filled in at `finalize_offloaded_task_function`
1945+
// time after the alloca scan completes; the registry call is idempotent on the same `ad_stack_ptr` so
1946+
// the second call updates the entry in place. Skipping registration when `prog == nullptr` (C++-only
1947+
// tests) leaves `registry_id == 0`, which the codegen-emitted cmpxchg short-circuits.
1948+
if (prog != nullptr) {
1949+
uint32_t id = prog->register_adstack_sizing_info(static_cast<const void *>(&current_task->ad_stack), kernel_name,
1950+
task_codegen_id, /*allocated_max_sizes=*/{});
1951+
current_task->ad_stack.registry_id = id;
1952+
}
19431953

19441954
for (auto &arg : func->args()) {
19451955
kernel_args.push_back(&arg);
@@ -2010,6 +2020,20 @@ void TaskCodeGenLLVM::finalize_offloaded_task_function() {
20102020
current_task->arr_writes.erase(std::unique(current_task->arr_writes.begin(), current_task->arr_writes.end()),
20112021
current_task->arr_writes.end());
20122022
}
2023+
// Register the per-task AdStackSizingInfo with the Program-side identity registry. The id is baked
2024+
// into the lazy-claim overflow path's `cmpxchg(0, id)` so the host raise site can name the offending
2025+
// kernel + task in its diagnostic message. Empty alloca list = no adstack pushes in this task; skip
2026+
// registration to keep the registry compact.
2027+
if (!current_task->ad_stack.allocas.empty() && prog != nullptr) {
2028+
std::vector<int> allocated_max_sizes;
2029+
allocated_max_sizes.reserve(current_task->ad_stack.allocas.size());
2030+
for (const auto &a : current_task->ad_stack.allocas) {
2031+
allocated_max_sizes.push_back(static_cast<int>(a.max_size_compile_time));
2032+
}
2033+
uint32_t id = prog->register_adstack_sizing_info(static_cast<const void *>(&current_task->ad_stack), kernel_name,
2034+
task_codegen_id, std::move(allocated_max_sizes));
2035+
current_task->ad_stack.registry_id = id;
2036+
}
20132037
}
20142038

20152039
// entry_block should jump to the body after all allocas are inserted
@@ -2464,10 +2488,12 @@ void TaskCodeGenLLVM::emit_ad_stack_row_claim_llvm() {
24642488
llvm::Value *clamped_row = builder->CreateSelect(cmp, clamp_upper, claimed_row);
24652489
builder->CreateStore(clamped_row, row_id_var);
24662490

2467-
// Overflow signal: on `claimed_row > clamp_upper`, atomically OR 1 into the pinned-host overflow flag. The
2468-
// condition is hoisted to a structured if so the not-overflowing fast path skips the atomic entirely - one
2469-
// function call to fetch the flag pointer plus one CreateICmpUGT comparison, the same compare we already
2470-
// emitted for the clamp.
2491+
// Overflow signal: on `claimed_row > clamp_upper`, atomically OR 1 into the pinned-host overflow flag and
2492+
// record the offending task identity in the companion `adstack_overflow_task_id_dev_ptr` slot via a
2493+
// `cmpxchg(0, registry_id)`. Only the FIRST overflowing thread's id sticks; subsequent threads observe
2494+
// a non-zero value and their cmpxchg fails harmlessly. The condition is hoisted to a structured if so
2495+
// the not-overflowing fast path skips both atomics entirely - one function call to fetch the pointers
2496+
// plus one CreateICmpUGT comparison (the same compare we already emitted for the clamp).
24712497
auto *current_function = builder->GetInsertBlock()->getParent();
24722498
auto *overflow_then_block = llvm::BasicBlock::Create(*llvm_context, "adstack_overflow_signal", current_function);
24732499
auto *overflow_merge_block = llvm::BasicBlock::Create(*llvm_context, "adstack_overflow_merge", current_function);
@@ -2479,6 +2505,17 @@ void TaskCodeGenLLVM::emit_ad_stack_row_claim_llvm() {
24792505
llvm::Value *one_i64 = llvm::ConstantInt::get(i64ty_local, 1);
24802506
builder->CreateAtomicRMW(llvm::AtomicRMWInst::Or, flag_ptr, one_i64, llvm::MaybeAlign(),
24812507
llvm::AtomicOrdering::Monotonic);
2508+
// Record the registry id (0 means "not registered"; skip the cmpxchg in that case so the slot stays
2509+
// zero and the host raise site falls through to the generic dual-cause message). Each offload task
2510+
// emits its own lazy-claim block, so the immediate is task-local at codegen time.
2511+
if (current_task != nullptr && current_task->ad_stack.registry_id != 0) {
2512+
llvm::Value *task_id_ptr = call("LLVMRuntime_get_adstack_overflow_task_id_dev_ptr", get_runtime());
2513+
llvm::Value *expected_zero = llvm::ConstantInt::get(i64ty_local, 0);
2514+
llvm::Value *new_id =
2515+
llvm::ConstantInt::get(i64ty_local, static_cast<uint64_t>(current_task->ad_stack.registry_id));
2516+
builder->CreateAtomicCmpXchg(task_id_ptr, expected_zero, new_id, llvm::MaybeAlign(),
2517+
llvm::AtomicOrdering::Monotonic, llvm::AtomicOrdering::Monotonic);
2518+
}
24822519
builder->CreateBr(overflow_merge_block);
24832520
}
24842521
builder->SetInsertPoint(overflow_merge_block);
@@ -2742,7 +2779,11 @@ void TaskCodeGenLLVM::visit(AdStackPushStmt *stmt) {
27422779
llvm::Value *max_size_addr = builder->CreateGEP(i64ty, ad_stack_max_sizes_ptr_llvm_, stack_id_i64);
27432780
llvm::Value *max_size = builder->CreateLoad(i64ty, max_size_addr);
27442781
llvm::Value *stack_base = get_ad_stack_base_llvm(stack);
2745-
call("stack_push", get_runtime(), stack_base, max_size, tlctx->get_constant(stack->element_size_in_bytes()));
2782+
auto *i64ty_local = llvm::Type::getInt64Ty(*llvm_context);
2783+
llvm::Value *registry_id_const = llvm::ConstantInt::get(
2784+
i64ty_local, current_task != nullptr ? static_cast<uint64_t>(current_task->ad_stack.registry_id) : 0u);
2785+
call("stack_push", get_runtime(), stack_base, max_size, tlctx->get_constant(stack->element_size_in_bytes()),
2786+
registry_id_const);
27462787
auto primal_ptr = call("stack_top_primal", stack_base, tlctx->get_constant(stack->element_size_in_bytes()));
27472788
primal_ptr = builder->CreateBitCast(primal_ptr, llvm::PointerType::get(tlctx->get_data_type(stmt->ret_type), 0));
27482789
builder->CreateStore(llvm_val[stmt->v], primal_ptr);

quadrants/codegen/llvm/llvm_compiled_data.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ struct AdStackSizingInfo {
7373
// the actual gate-passing thread count; `nullopt` falls through to dispatched-threads worst-case sizing (no behavior
7474
// change versus a kernel without this metadata).
7575
std::optional<StaticAdStackBoundExpr> bound_expr;
76+
// Identity in `Program::adstack_sizing_info_registry_`. Assigned at `finalize_offloaded_task_function`
77+
// time after the registry idempotently maps `&this` to a u32 id. Baked as an immediate into the
78+
// codegen-emitted lazy-claim `cmpxchg(0, registry_id)` so the host raise site can name the offending
79+
// kernel + task in its diagnostic message. `0` means "not registered" - the lazy-claim emit short-
80+
// circuits the cmpxchg in that case (no information to record). NOT serialised to the offline cache:
81+
// ids are assigned per `Program` lifetime, not per-kernel-content; a deserialised task re-registers
82+
// itself at the next launch.
83+
uint32_t registry_id{0};
7684
QD_IO_DEF(per_thread_stride,
7785
per_thread_stride_float,
7886
per_thread_stride_int,

quadrants/program/program.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
#include "program.h"
44

5+
#include <mutex>
6+
57
#include "quadrants/program/adstack_size_expr_eval.h"
68
#include "quadrants/ir/statements.h"
79
#include "quadrants/ir/type_factory.h"
@@ -239,6 +241,76 @@ void Program::synchronize_and_assert() {
239241
program_impl_->synchronize_and_assert();
240242
}
241243

244+
uint32_t Program::register_adstack_sizing_info(const void *ad_stack_ptr,
245+
const std::string &kernel_name,
246+
int task_id_in_kernel,
247+
std::vector<int> allocated_max_sizes) {
248+
std::lock_guard<std::mutex> lk(adstack_sizing_info_registry_mutex_);
249+
// Idempotent re-registration: same `ad_stack_ptr` yields the same id across re-compiles. The pointer is
250+
// task-stable for the kernel's lifetime; if a kernel is destroyed and a new one happens to hit the same
251+
// address, the previous entry's metadata is overwritten - acceptable because the diagnostic message reads
252+
// metadata at the same instant as the lookup, and the new entry's `kernel_name` / `allocated_max_sizes`
253+
// describe the live kernel.
254+
auto it = adstack_sizing_info_id_by_ptr_.find(ad_stack_ptr);
255+
if (it != adstack_sizing_info_id_by_ptr_.end()) {
256+
auto &entry = adstack_sizing_info_registry_[it->second];
257+
entry.kernel_name = kernel_name;
258+
entry.task_id_in_kernel = task_id_in_kernel;
259+
entry.allocated_max_sizes = std::move(allocated_max_sizes);
260+
return it->second;
261+
}
262+
uint32_t id = static_cast<uint32_t>(adstack_sizing_info_registry_.size());
263+
AdStackSizingInfoEntry entry;
264+
entry.ad_stack_ptr = ad_stack_ptr;
265+
entry.kernel_name = kernel_name;
266+
entry.task_id_in_kernel = task_id_in_kernel;
267+
entry.allocated_max_sizes = std::move(allocated_max_sizes);
268+
adstack_sizing_info_registry_.push_back(std::move(entry));
269+
adstack_sizing_info_id_by_ptr_.emplace(ad_stack_ptr, id);
270+
return id;
271+
}
272+
273+
const Program::AdStackSizingInfoEntry *Program::lookup_adstack_sizing_info(uint32_t id) const {
274+
std::lock_guard<std::mutex> lk(adstack_sizing_info_registry_mutex_);
275+
if (id == 0 || id >= adstack_sizing_info_registry_.size()) {
276+
return nullptr;
277+
}
278+
return &adstack_sizing_info_registry_[id];
279+
}
280+
281+
std::string Program::diagnose_adstack_overflow_message(uint32_t task_id) const {
282+
std::string identity_block;
283+
if (task_id != 0) {
284+
const auto *entry = lookup_adstack_sizing_info(task_id);
285+
if (entry != nullptr) {
286+
identity_block = " Offending task: kernel `" + entry->kernel_name + "` offload task #" +
287+
std::to_string(entry->task_id_in_kernel) + "; per-stack allocated max_size = [";
288+
for (size_t i = 0; i < entry->allocated_max_sizes.size(); ++i) {
289+
if (i != 0) {
290+
identity_block += ", ";
291+
}
292+
identity_block += std::to_string(entry->allocated_max_sizes[i]);
293+
}
294+
identity_block += "].\n";
295+
}
296+
}
297+
return identity_block +
298+
"Two possible causes:\n"
299+
" 1. A tensor backing a data-dependent loop bound was mutated outside Quadrants's tracking "
300+
"(typically a DLPack zero-copy mutation through a torch tensor sharing storage with a Quadrants "
301+
"ndarray, or a raw pointer write through a non-torch DLPack consumer). The cached adstack capacity "
302+
"was sized against the value before the mutation. Recovery: route the mutation through Quadrants "
303+
"APIs (`Ndarray.write` / `fill` / kernel writes) so the cache invalidates correctly, OR set a "
304+
"generous initial cap if a workload-change milestone genuinely grew capacity. Restart the "
305+
"iteration / training loop from a clean state.\n"
306+
" 2. (Quadrants bug) the pre-pass resolved the alloca to a bound tighter than the actual runtime "
307+
"push count - the enclosing loop shape is outside the current `SizeExpr` grammar, or the "
308+
"Bellman-Ford analyzer undercounted the forward-pass accumulation. Please file with the kernel IR "
309+
"(`QD_DUMP_IR=1`).\n"
310+
"Note: kernel state may be inconsistent post-overflow; do not retry the same step without "
311+
"addressing the cause and restarting from a clean state.";
312+
}
313+
242314
StreamSemaphore Program::flush() {
243315
return program_impl_->flush();
244316
}

quadrants/program/program.h

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
#pragma once
44

55
#include <functional>
6+
#include <mutex>
67
#include <optional>
78
#include <atomic>
89
#include <stack>
910
#include <shared_mutex>
11+
#include <string>
12+
#include <vector>
1013

1114
#define QD_RUNTIME_HOST
1215
#include "quadrants/ir/frontend_ir.h"
@@ -214,6 +217,29 @@ class QD_DLL_EXPORT Program {
214217
return *adstack_cache_;
215218
}
216219

220+
// Identity registry for adstack-sizer info. Codegen registers each `OffloadedTask::ad_stack` once per
221+
// kernel compilation and bakes the assigned id as an immediate into the lazy-claim overflow path; on
222+
// overflow the codegen emits `cmpxchg(0, id)` against the pinned-host task-id slot. The host raise site
223+
// reads the slot and routes through `diagnose_adstack_overflow_message(id)` to look up the kernel name,
224+
// task index, and per-stack metadata for an enriched error message. Pointer ownership stays with
225+
// `OffloadedTask`; entries are added but not removed - the registry size is bounded by the number of
226+
// adstack-bearing tasks compiled in the program's lifetime, typically dozens.
227+
struct AdStackSizingInfoEntry {
228+
const void *ad_stack_ptr{nullptr};
229+
std::string kernel_name;
230+
int task_id_in_kernel{0};
231+
std::vector<int> allocated_max_sizes;
232+
};
233+
uint32_t register_adstack_sizing_info(const void *ad_stack_ptr,
234+
const std::string &kernel_name,
235+
int task_id_in_kernel,
236+
std::vector<int> allocated_max_sizes);
237+
const AdStackSizingInfoEntry *lookup_adstack_sizing_info(uint32_t id) const;
238+
// Format a diagnostic message for an overflow signal. `task_id` is the value read from the pinned-host
239+
// task-id slot (0 if no thread overflowed; otherwise the registry id of the first overflowing task).
240+
// Returns the dual-cause message body to embed in the `QuadrantsAssertionError` raised at the poll site.
241+
std::string diagnose_adstack_overflow_message(uint32_t task_id) const;
242+
217243
/**
218244
* Destroys a new SNode tree.
219245
*
@@ -352,6 +378,14 @@ class QD_DLL_EXPORT Program {
352378
// Adstack caching state (per-task metadata, bytecode, size-expr results, generation counters). All adstack-specific
353379
// surface lives in `program/adstack_size_expr_eval.{h,cpp}`; routed through `adstack_cache()` getter.
354380
std::unique_ptr<AdStackCache> adstack_cache_;
381+
382+
// Adstack-sizing-info identity registry. See `register_adstack_sizing_info`. Index 0 is reserved as the
383+
// "no overflow" sentinel so the codegen-emitted `cmpxchg(0, id)` cleanly distinguishes "task id recorded"
384+
// from "slot still clean". Reverse lookup map keyed by `ad_stack_ptr` keeps `register_adstack_sizing_info`
385+
// idempotent across re-launches of the same kernel.
386+
std::vector<AdStackSizingInfoEntry> adstack_sizing_info_registry_{AdStackSizingInfoEntry{}};
387+
std::unordered_map<const void *, uint32_t> adstack_sizing_info_id_by_ptr_;
388+
mutable std::mutex adstack_sizing_info_registry_mutex_;
355389
std::stack<int> free_snode_tree_ids_;
356390

357391
std::vector<std::unique_ptr<Function>> functions_;

quadrants/runtime/llvm/llvm_adstack_lazy_claim.cpp

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -639,25 +639,29 @@ void LlvmRuntimeExecutor::check_adstack_overflow() {
639639
return;
640640
}
641641
int64_t flag = __atomic_exchange_n(adstack_overflow_flag_host_ptr_, (int64_t)0, __ATOMIC_RELAXED);
642-
if (flag != 0) {
643-
throw QuadrantsAssertionError(
644-
"Adstack overflow: a reverse-mode autodiff kernel pushed more elements than the adstack capacity "
645-
"allows. Raised at the next Quadrants Python entry rather than at the offending kernel launch. Two "
646-
"possible causes:\n"
647-
" 1. A tensor backing a data-dependent loop bound was mutated outside Quadrants's tracking "
648-
"(typically a DLPack zero-copy mutation through a torch tensor sharing storage with a Quadrants "
649-
"ndarray, or a raw pointer write through a non-torch DLPack consumer). The cached adstack capacity "
650-
"was sized against the value before the mutation. Recovery: route the mutation through Quadrants "
651-
"APIs (`Ndarray.write` / `fill` / kernel writes) so the cache invalidates correctly, OR set a "
652-
"generous initial cap if a workload-change milestone genuinely grew capacity. Restart the "
653-
"iteration / training loop from a clean state.\n"
654-
" 2. (Quadrants bug) the pre-pass resolved the alloca to a bound tighter than the actual runtime "
655-
"push count - the enclosing loop shape is outside the current `SizeExpr` grammar, or the "
656-
"Bellman-Ford analyzer undercounted the forward-pass accumulation. Please file with the kernel IR "
657-
"(`QD_DUMP_IR=1`).\n"
658-
"Note: kernel state may be inconsistent post-overflow; do not retry the same step without "
659-
"addressing the cause and restarting from a clean state.");
642+
if (flag == 0) {
643+
return;
644+
}
645+
// Drain the companion task-id slot in the same poll. Both slots cleared so the next overflow records
646+
// a fresh identity. `task_id == 0` means the kernel that overflowed pre-dates the registry wiring or
647+
// its `ad_stack.registry_id` was unset for any reason (e.g. a deserialised offline-cache task that has
648+
// not yet been re-registered); the diagnose helper falls through to the generic dual-cause message in
649+
// that case.
650+
uint32_t task_id = 0;
651+
if (adstack_overflow_task_id_host_ptr_ != nullptr) {
652+
int64_t recorded = __atomic_exchange_n(adstack_overflow_task_id_host_ptr_, (int64_t)0, __ATOMIC_RELAXED);
653+
task_id = static_cast<uint32_t>(recorded);
660654
}
655+
Program *prog = (program_impl_ != nullptr) ? program_impl_->program : nullptr;
656+
std::string diagnostic = (prog != nullptr) ? prog->diagnose_adstack_overflow_message(task_id)
657+
: std::string(
658+
"Adstack overflow: a reverse-mode autodiff kernel pushed more "
659+
"elements than the adstack capacity allows.");
660+
throw QuadrantsAssertionError(
661+
"Adstack overflow: a reverse-mode autodiff kernel pushed more elements "
662+
"than the adstack capacity allows. Raised at the next Quadrants Python "
663+
"entry rather than at the offending kernel launch.\n" +
664+
diagnostic);
661665
}
662666

663667
std::size_t LlvmRuntimeExecutor::publish_adstack_metadata(const AdStackSizingInfo &ad_stack,

0 commit comments

Comments
 (0)