From 237d3a39946708a4e5e0631a1f2f596589bfebc7 Mon Sep 17 00:00:00 2001 From: Wonhyuk Yang Date: Tue, 14 Jul 2026 05:49:06 +0900 Subject: [PATCH] [TOGSim] Do not rescan the blocked head of the ready queue on every issue Core's issue scan walked the ready queue from the front every cycle and issued the first instruction it could. The ones it walked past are blocked -- and only ever on one of two things: free spad bytes (try_occupy_sram) or a free SA weight slot (pick_free_weight_sa). Every other path in the scan issues. So the scan re-walked the same blocked prefix on its way to every single issue. Measured on an 8x8 conv (the CI wrapper3 config, where the small array keeps preloads waiting on weight slots so the prefix grows to ~6000): case steps walked issues steps per issue CM16 762,271,822 232,072 3284 MM16 1,070,436,422 302,112 3543 Give each Tile a cursor at the last instruction the scan found blocked and let the next scan resume after it. Blocked stays blocked until a resource GROWS, so the cursor is keyed on the resource levels (_sram_used, _weight_free) at the time, not on a free event: a freed weight slot that the woken preload immediately takes leaves the rest of the prefix blocked, and the cursor stays good. It is dropped only when its own instruction is erased. New ready instructions are appended, so they always land after the cursor. case steps walked steps per issue CM16 257,156 1.11 MM16 402,460 1.33 The scan issues the same instruction on the same cycle -- it just stops re-deriving what it already knew. Total execution cycles are identical on every case checked: CM16 803,796, MM16 803,345, MM36 9,835,778, CI36 39,449,132. The 8x8 conv2d of tests/ops/conv/test_conv2d.py (CI36) goes from 4h36m to 7m19s. That job currently fails on develop (SIGKILL, out of memory) and still failed after the on-demand TileGraph (3h job limit, simulation unfinished). --- TOGSim/include/Core.h | 3 +++ TOGSim/include/Tile.h | 25 +++++++++++++++++++++++++ TOGSim/src/Core.cc | 11 +++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/TOGSim/include/Core.h b/TOGSim/include/Core.h index dbc91950..8b812368 100644 --- a/TOGSim/include/Core.h +++ b/TOGSim/include/Core.h @@ -137,6 +137,9 @@ class Core { // to its accumulated footprint bytes (freed when its last reader issues). size_t _sram_used = 0; size_t _sram_capacity = 0; + // Free SA weight slots. With _sram_used it keys the issue scan's cursor: an + // instruction it walked past wakes only if one of the two grows. + int _weight_free = 0; std::unordered_map _sram_allocs; // SA weight-buffer throttle (sec 10.4). _weight_slots_used[s] = weights resident diff --git a/TOGSim/include/Tile.h b/TOGSim/include/Tile.h index b11b9f8e..3ba97cbd 100644 --- a/TOGSim/include/Tile.h +++ b/TOGSim/include/Tile.h @@ -39,6 +39,25 @@ class Tile : public std::enable_shared_from_this { std::deque>& get_instructions() { return _instructions; } void enqueue_ready(const std::shared_ptr& inst) { _ready_queue.push_back(inst); } std::list>& get_ready_instructions() { return _ready_queue; } + + // Issue-scan cursor (Core::cycle): the scan only walks past instructions blocked on + // free spad bytes or a free weight slot, so it can resume where it left off. + using ReadyIt = std::list>::iterator; + ReadyIt scan_from(size_t sram_used, int weight_free) { + if (_scan_blocked && sram_used >= _scan_sram_used && weight_free <= _scan_weight_free) + return std::next(_scan_blocked_at); + return _ready_queue.begin(); // a resource grew -> the prefix may issue now + } + void note_blocked(ReadyIt it, size_t sram_used, int weight_free) { + _scan_blocked_at = it; + _scan_blocked = true; + _scan_sram_used = sram_used; // it could not issue with this much spad free... + _scan_weight_free = weight_free; // ...nor with this many weight slots free + } + // Kept across a rescan (the prefix blocks again once the freed resource is taken); + // it only dies with its instruction. + bool is_scan_cursor(ReadyIt it) const { return _scan_blocked && it == _scan_blocked_at; } + void drop_scan_cursor() { _scan_blocked = false; } void print(); size_t nr_insts() { return _nr_insts; } size_t nr_finshed_insts() { return _nr_finished_insts; } @@ -61,6 +80,12 @@ class Tile : public std::enable_shared_from_this { size_t _nr_finished_insts = 0; std::deque> _instructions; std::list> _ready_queue; + // Only dereferenced while resources are no better than at note_blocked, and a + // still-blocked instruction is never erased. + ReadyIt _scan_blocked_at; + bool _scan_blocked = false; + size_t _scan_sram_used = 0; + int _scan_weight_free = 0; std::vector> _child_tiles; void *_custom_data=NULL; bool _stonne_tile=false; diff --git a/TOGSim/src/Core.cc b/TOGSim/src/Core.cc index 8588fede..41fe856e 100644 --- a/TOGSim/src/Core.cc +++ b/TOGSim/src/Core.cc @@ -24,6 +24,7 @@ Core::Core(uint32_t id, SimulationConfig config) exit(EXIT_FAILURE); } _weight_slots_used.resize(_num_systolic_array_per_core, 0); + _weight_free = _num_systolic_array_per_core * (int)_weight_slot_depth; } // Round-robin a systolic array that still has a free weight slot; -1 if all full @@ -42,7 +43,7 @@ int Core::pick_free_weight_sa() { void Core::apply_due(const DueAction& a) { switch (a.kind) { case DueAction::FreeWeightSlot: - if (--a.token->refcount <= 0) { _weight_slots_used[a.token->sa]--; _issue_dirty = true; } // weight slot freed -> re-arm + if (--a.token->refcount <= 0) { _weight_slots_used[a.token->sa]--; _weight_free++; _issue_dirty = true; } // weight slot freed -> re-arm break; case DueAction::WakeBar: { auto bar = a.bar; // async load data arrived -> fire its MEMORY_BAR @@ -286,7 +287,8 @@ void Core::cycle() { for (int i=0; i<_tiles.size() && !issued; i++) { auto& instructions = _tiles[i]->get_ready_instructions(); - for (auto it=instructions.begin(); it!=instructions.end();) { + // Resume after the prefix already known to be blocked (Tile::scan_from). + for (auto it=_tiles[i]->scan_from(_sram_used, _weight_free); it!=instructions.end();) { auto& inst = *it; switch (inst->get_opcode()) { @@ -365,6 +367,7 @@ void Core::cycle() { exit(EXIT_FAILURE); } _weight_slots_used[sa_idx]++; + _weight_free--; auto tok = std::make_shared(WeightToken{sa_idx, n_consumers}); for (auto& c : inst->get_deps(DepEvent::ISSUE)) if (c->get_compute_type() == MATMUL) { @@ -408,6 +411,7 @@ void Core::cycle() { inst->finish_instruction(); static_cast(inst->get_owner())->inc_finished_inst(); _stat_tot_skipped_inst.at(static_cast(inst->get_opcode()))++; + if (_tiles[i]->is_scan_cursor(it)) _tiles[i]->drop_scan_cursor(); it = instructions.erase(it); // erase returns the next iterator; the continue; // old code fell through to it++ on the // erased (invalidated) iterator -> UB @@ -461,9 +465,12 @@ void Core::cycle() { if (issued) { _stat_inst_count.at(static_cast(inst->get_opcode()))++; + if (_tiles[i]->is_scan_cursor(it)) _tiles[i]->drop_scan_cursor(); instructions.erase(it); break; } + // Did not issue -> blocked on spad or a weight slot (the only two stalls). + _tiles[i]->note_blocked(it, _sram_used, _weight_free); it++; } }