diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp b/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
index c3f00927..0308a833 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/host/runtime_maker.cpp
@@ -608,9 +608,13 @@ static void apply_orch_sched_env_flags(Runtime *runtime) {
const char *serial_env = std::getenv("SIMPLER_TMR_SERIAL_ORCH_SCHED_ENABLE");
runtime->dev.serial_orch_sched =
serial_env && (serial_env[0] == '1' || serial_env[0] == 't' || serial_env[0] == 'T');
+ const char *drain_aba_env = std::getenv("SIMPLER_DRAIN_ABA_TEST");
+ runtime->dev.drain_aba_test_mode =
+ drain_aba_env && (drain_aba_env[0] == '1' || drain_aba_env[0] == 't' || drain_aba_env[0] == 'T');
LOG_INFO_V0(
"Serial orchestrator-to-scheduler start gate: %s", runtime->dev.serial_orch_sched ? "enabled" : "disabled"
);
+ LOG_INFO_V0("Drain ABA test hook: %s", runtime->dev.drain_aba_test_mode ? "enabled" : "disabled");
}
// per-(cid,config): reserve and acquire the static device pools. GM heap, PTO2
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/pto_ring_buffer.h b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/pto_ring_buffer.h
index 070074ce..fbbaca10 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/pto_ring_buffer.h
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/pto_ring_buffer.h
@@ -453,6 +453,84 @@ private:
static_cast<int>(h.task_state.load(std::memory_order_acquire)), rc & ~PTO2_FANOUT_SCOPE_BIT,
fc & ~PTO2_FANOUT_SCOPE_BIT, (rc & PTO2_FANOUT_SCOPE_BIT) ? 1 : 0
);
+ int32_t dump_end = std::min(local_task_id_, last_alive + 9);
+ for (int32_t tid = last_alive; tid < dump_end; tid++) {
+ PTO2TaskSlotState &s = slot_states_[tid & window_mask_];
+ PTO2TaskPayload *p = s.payload;
+ uint32_t s_fc = s.fanout_count;
+ uint32_t s_rc = s.fanout_refcount.load(std::memory_order_acquire);
+ int32_t staged_cores = 0;
+ int32_t published = -1;
+ int32_t early_dispatch = -1;
+ int32_t launch = -1;
+ int32_t drain = -1;
+ int32_t running_slot = -1;
+ if (p != nullptr) {
+ for (int w = 0; w < PTO2_EARLY_DISPATCH_CORE_MASK_WORDS; w++) {
+ staged_cores += __builtin_popcountll(p->staged_core_mask[w].load(std::memory_order_seq_cst));
+ }
+ published = static_cast<int32_t>(p->published_block_count.load(std::memory_order_seq_cst));
+ early_dispatch = static_cast<int32_t>(p->early_dispatch_state.load(std::memory_order_seq_cst));
+ launch = static_cast<int32_t>(p->early_dispatch_launch_state.load(std::memory_order_seq_cst));
+ drain = static_cast<int32_t>(p->early_sync_drain_state.load(std::memory_order_seq_cst));
+ running_slot = static_cast<int32_t>(p->running_slot_count.load(std::memory_order_seq_cst));
+ }
+ LOG_ERROR(
+ " DRAIN_ABA_DIAG slot local=%d raw=%lld state=%d fanin=%d/%d fanout=%u/%u scope=%d "
+ "subtasks=%d/%d next_block=%d logical=%d published=%d staged_cores=%d running_slot=%d "
+ "early_dispatch=%d launch=%d drain=%d",
+ tid, s.task != nullptr ? static_cast<long long>(s.task->task_id.raw) : -1LL,
+ static_cast<int>(s.task_state.load(std::memory_order_acquire)),
+ s.fanin_refcount.load(std::memory_order_acquire), s.fanin_count, s_rc & ~PTO2_FANOUT_SCOPE_BIT,
+ s_fc & ~PTO2_FANOUT_SCOPE_BIT, (s_rc & PTO2_FANOUT_SCOPE_BIT) ? 1 : 0,
+ static_cast<int32_t>(s.completed_subtasks.load(std::memory_order_acquire)),
+ static_cast<int32_t>(s.total_required_subtasks),
+ static_cast<int32_t>(s.next_block_idx.load(std::memory_order_seq_cst)),
+ static_cast<int32_t>(s.logical_block_num), published, staged_cores, running_slot, early_dispatch,
+ launch, drain
+ );
+ if (p != nullptr) {
+ int32_t inline_count = std::min(p->fanin_actual_count, PTO2_FANIN_INLINE_CAP);
+ for (int32_t fi = 0; fi < inline_count; fi++) {
+ PTO2TaskSlotState *prod = p->fanin_inline_slot_states[fi];
+ uint32_t prod_fc = prod != nullptr ? prod->fanout_count : 0;
+ uint32_t prod_rc =
+ prod != nullptr ? prod->fanout_refcount.load(std::memory_order_acquire) : 0;
+ LOG_ERROR(
+ " DRAIN_ABA_DIAG slot local=%d fanin[%d]=raw:%lld state:%d fanout:%u/%u scope:%d",
+ tid, fi,
+ prod != nullptr && prod->task != nullptr ? static_cast<long long>(prod->task->task_id.raw) :
+ -1LL,
+ prod != nullptr ? static_cast<int>(prod->task_state.load(std::memory_order_acquire)) : -1,
+ prod_rc & ~PTO2_FANOUT_SCOPE_BIT, prod_fc & ~PTO2_FANOUT_SCOPE_BIT,
+ (prod_rc & PTO2_FANOUT_SCOPE_BIT) ? 1 : 0
+ );
+ }
+ }
+ int32_t fanout_dump_count = 0;
+ for (PTO2DepListEntry *edge = s.fanout_head; edge != nullptr && fanout_dump_count < 8;
+ edge = edge->next, fanout_dump_count++) {
+ PTO2TaskSlotState *consumer = edge->slot_state;
+ int32_t consumer_local =
+ consumer != nullptr ? static_cast<int32_t>(consumer - slot_states_) : -1;
+ LOG_ERROR(
+ " DRAIN_ABA_DIAG slot local=%d fanout[%d]=local:%d raw:%lld state:%d fanin:%d/%d "
+ "subtasks:%d/%d",
+ tid, fanout_dump_count, consumer_local,
+ consumer != nullptr && consumer->task != nullptr ?
+ static_cast<long long>(consumer->task->task_id.raw) :
+ -1LL,
+ consumer != nullptr ? static_cast<int>(consumer->task_state.load(std::memory_order_acquire)) :
+ -1,
+ consumer != nullptr ? consumer->fanin_refcount.load(std::memory_order_acquire) : -1,
+ consumer != nullptr ? consumer->fanin_count : -1,
+ consumer != nullptr ?
+ static_cast<int32_t>(consumer->completed_subtasks.load(std::memory_order_acquire)) :
+ -1,
+ consumer != nullptr ? static_cast<int32_t>(consumer->total_required_subtasks) : -1
+ );
+ }
+ }
}
LOG_ERROR("Solution:");
if (scope_gated) {
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/runtime.h b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/runtime.h
index 90977d3e..427a0923 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/runtime.h
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/runtime.h
@@ -187,6 +187,12 @@ struct alignas(64) DeviceRuntimeLaunchDesc {
// Controlled via SIMPLER_TMR_SERIAL_ORCH_SCHED_ENABLE environment variable.
bool serial_orch_sched;
+ // Test-only drain interleaving hook. When enabled, scheduler threads create a
+ // deterministic stale-attempt window in sync_start drain to prove whether the
+ // reusable ack/election fields are generation-less.
+ // Controlled via SIMPLER_DRAIN_ABA_TEST environment variable.
+ bool drain_aba_test_mode;
+
void *gm_sm_ptr_; // GM pointer to PTO2 shared memory (device)
ChipStorageTaskArgs orch_args_storage_; // Copy of args for device
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
index 7d4e1a82..9e4f989e 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_cold_path.cpp
@@ -1130,6 +1130,7 @@ int32_t SchedulerContext::pre_handshake_init(
aic_count_ = cores_total_num_ / 3;
aiv_count_ = (cores_total_num_ * 2) / 3;
active_sched_threads_ = (sched_thread_num_ > 0) ? sched_thread_num_ : aicpu_thread_num_;
+ drain_aba_test_mode_ = runtime->dev.drain_aba_test_mode;
handshake_failed_.store(false, std::memory_order_release);
// State the barrier-free per-thread init path no longer reaches via
@@ -1311,6 +1312,9 @@ void SchedulerContext::deinit() {
drain_state_.drain_stage_go.store(0, std::memory_order_release);
drain_state_.drain_stage_done_mask.store(0, std::memory_order_release);
drain_state_.drain_running_staged.store(0, std::memory_order_release);
+ drain_state_.drain_attempt_seq.store(0, std::memory_order_release);
+ drain_state_.drain_test_victim_armed.store(0, std::memory_order_release);
+ drain_state_.drain_test_stale_seen.store(0, std::memory_order_release);
drain_state_.pending_task.store(nullptr, std::memory_order_release);
// Reset task counters and orchestrator state
@@ -1324,6 +1328,7 @@ void SchedulerContext::deinit() {
aiv_count_ = 0;
cores_total_num_ = 0;
aicpu_thread_num_ = 0;
+ drain_aba_test_mode_ = false;
sched_thread_num_ = 0;
active_sched_threads_ = 0;
for (int32_t t = 0; t < MAX_AICPU_THREADS; t++) {
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp
index 1b162a61..ac2ae387 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_completion.cpp
@@ -18,6 +18,7 @@
#include "common/l2_swimlane_profiling.h"
#include "common/memory_barrier.h"
#include "common/platform_config.h"
+#include "pto_runtime_status.h"
#include "pto_runtime2.h"
#include "runtime.h"
#include "spin_hint.h"
@@ -240,6 +241,23 @@ void SchedulerContext::complete_slot_task(
}
deferred_release_slot_states[deferred_release_count++] = &slot_state;
}
+ if (slot_state.task_attrs.requires_sync_start()) {
+ static std::atomic<int32_t> drain_aba_sync_release_logs{0};
+ int32_t log_idx = drain_aba_sync_release_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 256) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG sync_complete_release sample=%d thread=%d task=%lld count=%d",
+ log_idx, thread_idx, static_cast<long long>(slot_state.task->task_id.raw), deferred_release_count
+ );
+ }
+ while (deferred_release_count > 0) {
+#if SIMPLER_SCHED_PROFILING
+ (void)sched_->on_task_release(*deferred_release_slot_states[--deferred_release_count], thread_idx);
+#else
+ sched_->on_task_release(*deferred_release_slot_states[--deferred_release_count]);
+#endif
+ }
+ }
completed_this_turn++;
}
@@ -514,6 +532,11 @@ bool SchedulerContext::enter_drain_mode(PTO2TaskSlotState *slot_state, int32_t b
drain_state_.drain_stage_go.store(0, std::memory_order_relaxed);
drain_state_.drain_stage_done_mask.store(0, std::memory_order_relaxed);
drain_state_.drain_running_staged.store(0, std::memory_order_relaxed);
+ if (drain_aba_test_mode_) {
+ drain_state_.drain_attempt_seq.store(1, std::memory_order_release);
+ drain_state_.drain_test_victim_armed.store(0, std::memory_order_relaxed);
+ drain_state_.drain_test_stale_seen.store(0, std::memory_order_relaxed);
+ }
// Release store: all stores above are now visible to any thread that
// acquire-loads sync_start_pending and sees block_num > 0.
drain_state_.sync_start_pending.store(block_num, std::memory_order_release);
@@ -702,21 +725,117 @@ void SchedulerContext::handle_drain_mode(int32_t thread_idx, [[maybe_unused]] ui
block_num = drain_state_.sync_start_pending.load(std::memory_order_acquire);
} while (block_num < 0);
if (block_num == 0) return;
+ const int32_t local_drain_attempt =
+ drain_aba_test_mode_ ? drain_state_.drain_attempt_seq.load(std::memory_order_acquire) : 0;
uint32_t all_acked = (1u << active_sched_threads_) - 1;
+ {
+ static std::atomic<int32_t> drain_aba_handle_enter_logs{0};
+ int32_t log_idx = drain_aba_handle_enter_logs.fetch_add(1, std::memory_order_relaxed);
+ PTO2TaskSlotState *pending = drain_state_.pending_task.load(std::memory_order_acquire);
+ bool later_task = pending != nullptr && pending->task != nullptr && pending->task->task_id.local() != 2;
+ if (log_idx < 256 || later_task) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG handle_enter sample=%d thread=%d task=%lld block_num=%d ack_mask=0x%x "
+ "elected=%d stage_go=%d stage_done=0x%x sync_start_pending=%d",
+ log_idx, thread_idx,
+ pending != nullptr && pending->task != nullptr ? static_cast<long long>(pending->task->task_id.raw) :
+ -1LL,
+ block_num, drain_state_.drain_ack_mask.load(std::memory_order_relaxed),
+ drain_state_.drain_worker_elected.load(std::memory_order_relaxed),
+ drain_state_.drain_stage_go.load(std::memory_order_relaxed),
+ drain_state_.drain_stage_done_mask.load(std::memory_order_relaxed),
+ drain_state_.sync_start_pending.load(std::memory_order_relaxed)
+ );
+ }
+ }
// Ack barrier -- signal this thread has stopped dispatch.
- drain_state_.drain_ack_mask.fetch_or(1u << thread_idx, std::memory_order_release);
+ uint32_t ack_after = drain_state_.drain_ack_mask.fetch_or(1u << thread_idx, std::memory_order_release) |
+ (1u << thread_idx);
+ {
+ static std::atomic<int32_t> drain_aba_ack_logs{0};
+ int32_t log_idx = drain_aba_ack_logs.fetch_add(1, std::memory_order_relaxed);
+ PTO2TaskSlotState *pending = drain_state_.pending_task.load(std::memory_order_acquire);
+ bool later_task = pending != nullptr && pending->task != nullptr && pending->task->task_id.local() != 2;
+ if (log_idx < 256 || later_task) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG ack_after sample=%d thread=%d task=%lld ack_mask=0x%x all_acked=0x%x "
+ "elected=%d stage_go=%d",
+ log_idx, thread_idx,
+ pending != nullptr && pending->task != nullptr ? static_cast<long long>(pending->task->task_id.raw) :
+ -1LL,
+ ack_after, all_acked, drain_state_.drain_worker_elected.load(std::memory_order_relaxed),
+ drain_state_.drain_stage_go.load(std::memory_order_relaxed)
+ );
+ }
+ }
// Spin until all threads have acked.
// If our bit is cleared while waiting, elected reset due to insufficient resources.
+ int32_t ack_wait_spins = 0;
while (true) {
if (is_completed()) return;
uint32_t ack = drain_state_.drain_ack_mask.load(std::memory_order_acquire);
if ((ack & all_acked) == all_acked) break;
if ((ack & (1u << thread_idx)) == 0) return;
+ ack_wait_spins++;
+ if (ack_wait_spins == (1 << 20)) {
+ static std::atomic<int32_t> drain_aba_ack_wait_logs{0};
+ int32_t log_idx = drain_aba_ack_wait_logs.fetch_add(1, std::memory_order_relaxed);
+ PTO2TaskSlotState *pending = drain_state_.pending_task.load(std::memory_order_acquire);
+ bool later_task = pending != nullptr && pending->task != nullptr && pending->task->task_id.local() != 2;
+ if (log_idx < 128 || later_task) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG ack_wait sample=%d thread=%d task=%lld ack_mask=0x%x all_acked=0x%x "
+ "elected=%d stage_go=%d sync_start_pending=%d",
+ log_idx, thread_idx,
+ pending != nullptr && pending->task != nullptr ?
+ static_cast<long long>(pending->task->task_id.raw) :
+ -1LL,
+ ack, all_acked, drain_state_.drain_worker_elected.load(std::memory_order_relaxed),
+ drain_state_.drain_stage_go.load(std::memory_order_relaxed),
+ drain_state_.sync_start_pending.load(std::memory_order_relaxed)
+ );
+ }
+ }
SPIN_WAIT_HINT();
}
+
+ if (drain_aba_test_mode_ && active_sched_threads_ >= 3 && thread_idx == 1) {
+ int32_t expected_armed = 0;
+ if (drain_state_.drain_test_victim_armed.compare_exchange_strong(
+ expected_armed, 1, std::memory_order_acq_rel, std::memory_order_relaxed
+ )) {
+ int32_t spin_count = 0;
+ int32_t current_attempt = drain_state_.drain_attempt_seq.load(std::memory_order_acquire);
+ while (current_attempt == local_drain_attempt && !is_completed() && spin_count < (1 << 24)) {
+ ++spin_count;
+ SPIN_WAIT_HINT();
+ current_attempt = drain_state_.drain_attempt_seq.load(std::memory_order_acquire);
+ }
+ if (current_attempt != local_drain_attempt) {
+ PTO2TaskSlotState *pending = drain_state_.pending_task.load(std::memory_order_acquire);
+ uint32_t peer_acked = all_acked & ~(1u << thread_idx);
+ uint32_t ack = drain_state_.drain_ack_mask.load(std::memory_order_acquire);
+ while (!is_completed() && ((ack & peer_acked) != peer_acked || (ack & (1u << thread_idx)) != 0)) {
+ SPIN_WAIT_HINT();
+ ack = drain_state_.drain_ack_mask.load(std::memory_order_acquire);
+ }
+ drain_state_.drain_test_stale_seen.store(1, std::memory_order_release);
+ LOG_ERROR(
+ "DRAIN_ABA_TEST releasing stale drain attempt into original protocol: thread=%d task=%lld "
+ "local_attempt=%d current_attempt=%d block_num=%d ack_mask=0x%x elected=%d spin_count=%d",
+ thread_idx,
+ pending != nullptr && pending->task != nullptr ? static_cast<long long>(pending->task->task_id.raw) :
+ -1LL,
+ local_drain_attempt, current_attempt, block_num, ack,
+ drain_state_.drain_worker_elected.load(std::memory_order_relaxed), spin_count
+ );
+ }
+ }
+ }
+
// Election -- exactly one thread wins the CAS.
int32_t expected = 0;
drain_state_.drain_worker_elected.compare_exchange_strong(
@@ -744,12 +863,51 @@ void SchedulerContext::handle_drain_mode(int32_t thread_idx, [[maybe_unused]] ui
// needs block_num idle cores/clusters.
int32_t available =
count_global_available(shape, slot_state->active_mask.core_mask(), /*include_pending=*/gated);
+ if (drain_aba_test_mode_ && thread_idx == 1 &&
+ drain_state_.drain_test_stale_seen.load(std::memory_order_acquire) == 1) {
+ LOG_ERROR(
+ "DRAIN_ABA_TEST forcing stale owner past availability check: thread=%d task=%lld observed_available=%d "
+ "block_num=%d ack_mask=0x%x elected=%d",
+ thread_idx, static_cast<long long>(slot_state->task->task_id.raw), available, block_num,
+ drain_state_.drain_ack_mask.load(std::memory_order_relaxed),
+ drain_state_.drain_worker_elected.load(std::memory_order_relaxed)
+ );
+ available = block_num;
+ }
if (available < block_num) {
+ static std::atomic<int32_t> drain_aba_retry_logs{0};
+ int32_t log_idx = drain_aba_retry_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 256 || slot_state->task->task_id.local() != 2) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG retry_reset sample=%d thread=%d task=%lld shape=%d gated=%d global_available=%d "
+ "block_num=%d ack_mask=0x%x elected=%d stage_go=%d stage_done=0x%x",
+ log_idx, thread_idx, static_cast<long long>(slot_state->task->task_id.raw),
+ static_cast<int32_t>(shape), static_cast<int32_t>(gated), available, block_num,
+ drain_state_.drain_ack_mask.load(std::memory_order_relaxed),
+ drain_state_.drain_worker_elected.load(std::memory_order_relaxed),
+ drain_state_.drain_stage_go.load(std::memory_order_relaxed),
+ drain_state_.drain_stage_done_mask.load(std::memory_order_relaxed)
+ );
+ }
// Insufficient -- reset so all threads resume completion polling to free cores, then retry.
+ if (drain_aba_test_mode_) {
+ drain_state_.drain_attempt_seq.fetch_add(1, std::memory_order_acq_rel);
+ }
drain_state_.drain_ack_mask.store(0, std::memory_order_release);
drain_state_.drain_worker_elected.store(0, std::memory_order_release);
return;
}
+ static std::atomic<int32_t> drain_aba_stage_logs{0};
+ int32_t log_idx = drain_aba_stage_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 128) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG stage_go sample=%d thread=%d task=%lld shape=%d gated=%d global_available=%d "
+ "block_num=%d ack_mask=0x%x",
+ log_idx, thread_idx, static_cast<long long>(slot_state->task->task_id.raw), static_cast<int32_t>(shape),
+ static_cast<int32_t>(gated), available, block_num,
+ drain_state_.drain_ack_mask.load(std::memory_order_relaxed)
+ );
+ }
// Release parallel staging: every thread (this one included) now stages its own cores.
drain_state_.drain_running_staged.store(0, std::memory_order_relaxed);
drain_state_.drain_stage_done_mask.store(0, std::memory_order_relaxed);
@@ -772,6 +930,31 @@ void SchedulerContext::handle_drain_mode(int32_t thread_idx, [[maybe_unused]] ui
if (drain_prof) drain_acked_ts = get_sys_cnt_aicpu(); // pre-stage
#endif
int32_t my_running = drain_stage_cores(slot_state, block_num, thread_idx, gated);
+ {
+ static std::atomic<int32_t> drain_aba_stage_done_logs{0};
+ int32_t log_idx = drain_aba_stage_done_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 96) {
+ int32_t staged_cores = 0;
+ if (slot_state->payload != nullptr) {
+ for (int w = 0; w < PTO2_EARLY_DISPATCH_CORE_MASK_WORDS; w++) {
+ staged_cores += __builtin_popcountll(
+ slot_state->payload->staged_core_mask[w].load(std::memory_order_seq_cst)
+ );
+ }
+ }
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG stage_done sample=%d thread=%d task=%lld gated=%d my_running=%d next_block=%d "
+ "published=%d staged_cores=%d done_before=0x%x running_staged_before=%d",
+ log_idx, thread_idx, static_cast<long long>(slot_state->task->task_id.raw), static_cast<int32_t>(gated),
+ my_running, slot_state->next_block_idx.load(std::memory_order_seq_cst),
+ slot_state->payload != nullptr ?
+ static_cast<int32_t>(slot_state->payload->published_block_count.load(std::memory_order_seq_cst)) :
+ -1,
+ staged_cores, drain_state_.drain_stage_done_mask.load(std::memory_order_relaxed),
+ drain_state_.drain_running_staged.load(std::memory_order_relaxed)
+ );
+ }
+ }
#if SIMPLER_DFX
// out param carries the PURE drain_stage_cores wall (build_payload + MMIO publish of
// this thread's cores), isolating it from availability + stage_go handshake.
@@ -798,6 +981,37 @@ void SchedulerContext::handle_drain_mode(int32_t thread_idx, [[maybe_unused]] ui
if (is_completed()) return;
SPIN_WAIT_HINT();
}
+ {
+ static std::atomic<int32_t> drain_aba_finalize_logs{0};
+ int32_t log_idx = drain_aba_finalize_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 64) {
+ int32_t staged_cores = 0;
+ if (slot_state->payload != nullptr) {
+ for (int w = 0; w < PTO2_EARLY_DISPATCH_CORE_MASK_WORDS; w++) {
+ staged_cores += __builtin_popcountll(
+ slot_state->payload->staged_core_mask[w].load(std::memory_order_seq_cst)
+ );
+ }
+ }
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG finalize sample=%d thread=%d task=%lld gated=%d stage_done=0x%x "
+ "running_staged=%d staged_cores=%d next_block=%d published=%d launch=%d drain_state=%d",
+ log_idx, thread_idx, static_cast<long long>(slot_state->task->task_id.raw), static_cast<int32_t>(gated),
+ drain_state_.drain_stage_done_mask.load(std::memory_order_relaxed),
+ drain_state_.drain_running_staged.load(std::memory_order_acquire), staged_cores,
+ slot_state->next_block_idx.load(std::memory_order_seq_cst),
+ slot_state->payload != nullptr ?
+ static_cast<int32_t>(slot_state->payload->published_block_count.load(std::memory_order_seq_cst)) :
+ -1,
+ slot_state->payload != nullptr ?
+ static_cast<int32_t>(slot_state->payload->early_dispatch_launch_state.load(std::memory_order_seq_cst)) :
+ -1,
+ slot_state->payload != nullptr ?
+ static_cast<int32_t>(slot_state->payload->early_sync_drain_state.load(std::memory_order_seq_cst)) :
+ -1
+ );
+ }
+ }
if (gated) {
// Seed the rendezvous with the running-slot cores staged across all threads; pending
// cores advance it as they promote. maybe_rendezvous_ring (producer release) rings iff
@@ -824,7 +1038,26 @@ void SchedulerContext::handle_drain_mode(int32_t thread_idx, [[maybe_unused]] ui
// ahead of drain completion and fail while running_slot_count is still incomplete. When
// every block landed directly in a running slot, no pending promotion remains to retry it.
if (gated) {
- sched_->retry_sync_start_rendezvous_after_drain(*slot_state);
+ bool launched = sched_->retry_sync_start_rendezvous_after_drain(*slot_state);
+ static std::atomic<int32_t> drain_aba_rendezvous_logs{0};
+ int32_t log_idx = drain_aba_rendezvous_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 64) {
+ int32_t staged_cores = 0;
+ for (int w = 0; w < PTO2_EARLY_DISPATCH_CORE_MASK_WORDS; w++) {
+ staged_cores +=
+ __builtin_popcountll(slot_state->payload->staged_core_mask[w].load(std::memory_order_seq_cst));
+ }
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG rendezvous sample=%d thread=%d task=%lld launched=%d running_slot=%d "
+ "staged_cores=%d launch=%d early_dispatch=%d",
+ log_idx, thread_idx, static_cast<long long>(slot_state->task->task_id.raw),
+ static_cast<int32_t>(launched),
+ static_cast<int32_t>(slot_state->payload->running_slot_count.load(std::memory_order_seq_cst)),
+ staged_cores,
+ static_cast<int32_t>(slot_state->payload->early_dispatch_launch_state.load(std::memory_order_seq_cst)),
+ static_cast<int32_t>(slot_state->payload->early_dispatch_state.load(std::memory_order_seq_cst))
+ );
+ }
} else {
sched_->propagate_dispatch_fanin(*slot_state);
}
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h
index a6f31617..31ce15c3 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_context.h
@@ -185,6 +185,7 @@ private:
int32_t sched_thread_num_{0};
int32_t aicpu_thread_num_{0};
int32_t cores_total_num_{0};
+ bool drain_aba_test_mode_{false};
// Cluster-ordered worker_id lists, populated by post_handshake_init().
int32_t aic_worker_ids_[RUNTIME_MAX_WORKER]{};
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_dispatch.cpp b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_dispatch.cpp
index c090f94c..67985a4c 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_dispatch.cpp
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_dispatch.cpp
@@ -421,8 +421,36 @@ void SchedulerContext::dispatch_shape(
}
int32_t available = is_mix ? selected_mix_clusters.count() : cores.count();
if (available < slot_state->logical_block_num) {
+ static std::atomic<int32_t> drain_aba_enter_logs{0};
+ int32_t log_idx = drain_aba_enter_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 64) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG enter_ready_drain sample=%d thread=%d task=%lld shape=%d local_available=%d "
+ "block_num=%d",
+ log_idx, thread_idx, static_cast<long long>(slot_state->task->task_id.raw),
+ static_cast<int32_t>(shape), available, slot_state->logical_block_num
+ );
+ }
flush_publish();
- if (!enter_drain_mode(slot_state, slot_state->logical_block_num)) {
+ bool drain_entered = enter_drain_mode(slot_state, slot_state->logical_block_num);
+ static std::atomic<int32_t> drain_aba_enter_result_logs{0};
+ int32_t result_log_idx = drain_aba_enter_result_logs.fetch_add(1, std::memory_order_relaxed);
+ if (result_log_idx < 128) {
+ PTO2TaskSlotState *pending = drain_state_.pending_task.load(std::memory_order_acquire);
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG enter_ready_drain_result sample=%d thread=%d task=%lld entered=%d "
+ "pending=%lld sync_start_pending=%d ack_mask=0x%x elected=%d",
+ result_log_idx, thread_idx, static_cast<long long>(slot_state->task->task_id.raw),
+ static_cast<int32_t>(drain_entered),
+ pending != nullptr && pending->task != nullptr ?
+ static_cast<long long>(pending->task->task_id.raw) :
+ -1LL,
+ drain_state_.sync_start_pending.load(std::memory_order_acquire),
+ drain_state_.drain_ack_mask.load(std::memory_order_relaxed),
+ drain_state_.drain_worker_elected.load(std::memory_order_relaxed)
+ );
+ }
+ if (!drain_entered) {
disp_queues[static_cast<int32_t>(shape)].push(slot_state);
}
for (int rem = bi + 1; rem < got; rem++) {
@@ -1146,6 +1174,23 @@ int32_t SchedulerContext::resolve_and_dispatch(Runtime *runtime, int32_t thread_
// Phase 2 drain check
if (drain_state_.sync_start_pending.load(std::memory_order_acquire) != 0) {
+ if (deferred_release_count > 0) {
+ static std::atomic<int32_t> drain_aba_pre_drain_release_logs{0};
+ int32_t log_idx = drain_aba_pre_drain_release_logs.fetch_add(1, std::memory_order_relaxed);
+ if (log_idx < 64) {
+ LOG_INFO_V9(
+ "DRAIN_ABA_DIAG pre_drain_release sample=%d thread=%d count=%d",
+ log_idx, thread_idx, deferred_release_count
+ );
+ }
+ while (deferred_release_count > 0) {
+#if SIMPLER_SCHED_PROFILING
+ (void)sched_->on_task_release(*deferred_release_slot_states[--deferred_release_count], thread_idx);
+#else
+ sched_->on_task_release(*deferred_release_slot_states[--deferred_release_count]);
+#endif
+ }
+ }
#if SIMPLER_DFX
// The drain is otherwise a swimlane blind spot: the `continue` below skips
// every phase record, and handle_drain_mode is uninstrumented. Time it here so
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_types.h b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_types.h
index ff60af0f..28a222be 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_types.h
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/scheduler/scheduler_types.h
@@ -534,7 +534,10 @@ struct alignas(64) SyncStartDrainState {
std::atomic<int32_t> drain_stage_go{0}; // 0=hold; 1=elected released parallel staging
std::atomic<uint32_t> drain_stage_done_mask{0}; // bit per thread; all-set = all threads done staging
std::atomic<int32_t> drain_running_staged{0}; // sum of running-slot cores staged (rendezvous seed)
- int32_t _pad[7];
+ std::atomic<int32_t> drain_attempt_seq{0}; // test hook: generation for the current drain attempt
+ std::atomic<int32_t> drain_test_victim_armed{0}; // test hook: exactly one stale-attempt victim
+ std::atomic<int32_t> drain_test_stale_seen{0}; // test hook: latch first stale-attempt observation
+ int32_t _pad[4];
};
static_assert(sizeof(SyncStartDrainState) == 64);
diff --git a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp
index a557cb30..a9cc7fe1 100644
--- a/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp
+++ b/src/a2a3/runtime/tensormap_and_ringbuffer/runtime/shared/runtime.cpp
@@ -38,6 +38,7 @@ Runtime::Runtime() {
dev.aicpu_allowed_cpu_count = 0;
dev.aicpu_launch_count = 0;
dev.serial_orch_sched = false;
+ dev.drain_aba_test_mode = false;
dev.gm_sm_ptr_ = nullptr;
dev.orch_args_storage_.clear();
dev.prebuilt_arena_base_ = nullptr;
Platform
a2a3 (Ascend 910B/C hardware)
Runtime Variant
tensormap_and_ringbuffer
Description
sync_startdrain appears to use reusable shared coordination fields without an attempt/generation tag:When the elected drain owner observes insufficient resources, it resets
drain_ack_maskanddrain_worker_electedso scheduler threads can resume completion polling and retry the same pendingsync_starttask.However, a scheduler thread from the old attempt can already have passed the ack barrier and then be delayed before election. After the owner resets the shared fields, that stale thread can continue and interpret the reset
drain_worker_elected == 0/drain_ack_maskvalues as if they belonged to its own attempt.This is a generation-less reusable-barrier ABA issue: every individual shared field value is valid, but the fields can come from different drain attempts.
I used a deterministic test-only harness to force this interleaving. The harness does not directly return a synthetic error code. It lets the stale thread continue into the original
handle_drain_mode()election/stage protocol. The original protocol then enters a split barrier and eventually fails with an AICPU timeout.One detail in the harness is intentionally deterministic: after proving that the stale thread crossed from attempt 1 to attempt 2, it lets the stale owner pass the availability check. This models the real timing where resources become available before the stale owner performs that check. The hook does not replace the ack/election/stage protocol; the deadlock still comes from the original protocol accepting a stale owner and publishing
stage_gowith a mixed-generationack_mask.Steps to Reproduce
Expected Behavior
The drain retry protocol should not mix scheduler threads from different drain attempts.
A stale scheduler thread from an old drain attempt should not be able to:
stage_gousing a mixed-generationack_mask,stage_donefrom threads that are actually waiting in the next attempt's ack barrier.Expected behavior would be one of:
Actual Behavior
The test fails by AICPU timeout:
Key logs from the failing run:
The final state is split across two attempts:
Both waits are individually valid for their own attempt, but they are now waiting on different generations. This causes a deadlock.
Git Commit ID
d0bc661
CANN Version
Ascend CANN toolkit 9.0.0
Driver Version
26.0.rc1
Host Platform
Linux (aarch64)
Additional Context
The test-only harness is only used to make the interleaving deterministic. It is not the failure mechanism itself:
local_attempt=1tocurrent_attempt=2;handle_drain_mode()protocol;stage_gowithack_mask=0x5;ack_mask=0x7.A likely fix is to add an explicit drain generation/attempt tag. Each scheduler thread should record the generation it observed before acking. Election, availability check,
stage_go, stage-done, and finalize should all verify the generation still matches. On insufficient-resource retry, the owner should advance the generation before clearing/reusingack_maskanddrain_worker_elected, so old-attempt threads can safely return instead of entering the next attempt's election/stage protocol.Tested and suspected commits
The failing run was tested against upstream
hw-native-sys/simplercommitd0bc661a5b41b4925f2d72c77e86228cc351cf23. I have not completed a full bisection, so the following is only static-history analysis:f79c3bff1/ Refactor: extract AICPU scheduler into SchedulerContext class (a2a3 + a5) #654 introduced theSchedulerContextsync_start drain state and the generation-lesssync_start_pending/drain_ack_mask/drain_worker_electedprotocol shape.c1c1a70c0/ fix: null-guard pending_task in sync_start drain election (sim segfault) #898 fixed a related stale-election/null-pending_taskbug and explicitly noted that stale elected threads must not clear state that may already belong to a newer drain run. This looks like the same family of generation-less reusable-state risk.60fcc5985/ Add: early-dispatch for require_sync_start SPMD cohorts (a2a3 tmr) #1304 introduced the parallel staging fieldsdrain_stage_go,drain_stage_done_mask, anddrain_running_staged. The split-barrier deadlock reproduced here is most likely introduced or at least exposed by this change, because stale election can now publishstage_goand wait onstage_donewhile peer threads are in the next attempt's ack barrier.Complete runtime hook/instrumentation patch
This is the full runtime hook and instrumentation patch used for the deterministic run. The important interleaving hook is the
SIMPLER_DRAIN_ABA_TESTpath inhandle_drain_mode(); the additionalDRAIN_ABA_DIAGlogs were used to confirm the final split wait.Show full runtime hook patch
Complete ST code
The full ST code used for the reproducer is below. The paths are relative to the repository root.
Show full ST files
tests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba_hook/test_spmd_sync_start_drain_aba_hook.pytests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba_hook/kernels/orchestration/spmd_sync_start_drain_aba_hook_orch.cpptests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba/kernels/aic/kernel_spmd_mix_pressure.cpptests/st/a2a3/tensormap_and_ringbuffer/spmd_sync_start_drain_aba/kernels/aiv/kernel_spmd_mix_pressure.cpp中文简要说明
这个问题本质上是
sync_startdrain 协议里的可复用 barrier 没有 generation/attempt tag。owner 在资源不足时会清空ack_mask和drain_worker_elected让各线程 retry,但旧 attempt 中已经越过 ack barrier 的线程仍可能继续执行。这样它会把新 attempt 的空 election 状态当作旧 attempt 的状态来使用。复现中,第一次 drain 三个线程都 ack,owner 看到
available=23 < block_num=24后 reset。随后 thread1 作为旧 attempt 的 stale 线程继续原协议,发布stage_go;而 thread0/thread2 已经在新 attempt 的 ack barrier 中等待ack_mask=0x7。最终形成跨 generation 的 split wait:因此任务稳定超时并报
RuntimeError: run failed with code 507018。建议修复方向是在 drain ack/election/stage/finalize 全流程加入 generation 校验;资源不足 retry 时先推进 generation,再复用 ack/election 字段,使旧 attempt 线程能检测到 mismatch 并安全返回。