@@ -129,6 +129,14 @@ std::vector<AsyncTransferEvent> s_async_events_prev_op;
129129std::vector<PendingAsyncOp> s_pending_async_ops;
130130WeightPager * s_prev_op_pager = nullptr ;
131131
132+ // WP_PAGED_BATCH per-range pin lifecycle. Under paged-batch the callback does
133+ // NOT unpin per-op; pins accumulate in s_range_pins across the current batch
134+ // range, move to s_range_pins_pending when the range ends (eval_cb_op_return
135+ // returns true), and are released at the top of the next callback — which the
136+ // scheduler guarantees runs only after that range's compute+sync.
137+ std::vector<int > s_range_pins;
138+ std::vector<int > s_range_pins_pending;
139+
132140void release_async_op (PendingAsyncOp & op) {
133141 WeightPager * owner = op.pager ;
134142 for (int page_idx : op.pages ) {
@@ -148,11 +156,22 @@ void release_async_op(PendingAsyncOp & op) {
148156}
149157#else
150158std::vector<int > s_pinned_pages_prev_op;
159+ std::vector<int > s_range_pins;
160+ std::vector<int > s_range_pins_pending;
151161#endif
152162} // namespace
153163
154164void weight_pager_eval_cb_reset (WeightPager * pager) {
155165#if defined(GGML_USE_HIP)
166+ // WP_PAGED_BATCH: release any range pins still held at teardown (a range that
167+ // ended at a split boundary with no following callback, or reset mid-range).
168+ // Independent of async ensure, so do it before the async early-return.
169+ if (pager != nullptr && (!s_range_pins.empty () || !s_range_pins_pending.empty ())) {
170+ for (int p : s_range_pins_pending) { pager->unpin_page (p); }
171+ for (int p : s_range_pins) { pager->unpin_page (p); }
172+ s_range_pins.clear ();
173+ s_range_pins_pending.clear ();
174+ }
156175 if (pager == nullptr || !pager->async_ensure_enabled ()) {
157176 return ;
158177 }
@@ -237,32 +256,48 @@ void weight_pager_eval_cb_print_profile() {
237256bool weight_pager_eval_cb (struct ggml_tensor * t, bool ask, void * user_data) {
238257 // Only act on the pre-execution call. The post-execution call is
239258 // informational and would re-trigger the same lookups.
240- if (!ask) return true ;
241259 if (t == nullptr ) return true ;
242260 auto * pager = (WeightPager *) user_data;
243261 if (pager == nullptr ) return true ;
244- const bool eval_debug = eval_debug_enabled ();
245262 const bool batch_eval_cb = wp_batch_eval_cb_enabled ();
263+ const bool paged_batch = batch_eval_cb && wp_paged_batch_enabled ();
264+ // WP_PAGED_BATCH: release the previous range's pins now. The top of this
265+ // callback is guaranteed to run after that range's compute+sync (the
266+ // scheduler computes+syncs a range before it issues the next ask=true, and
267+ // before the ask=false on the range's last node).
268+ if (paged_batch && !s_range_pins_pending.empty ()) {
269+ for (int p : s_range_pins_pending) { pager->unpin_page (p); }
270+ s_range_pins_pending.clear ();
271+ }
272+ if (!ask) return true ;
273+ const bool eval_debug = eval_debug_enabled ();
246274 const uint64_t sync_fallbacks_before =
247275 batch_eval_cb ? pager->sync_fallback_count () : 0 ;
248276 bool routing_tls_set = false ;
249- const bool paged_batch = batch_eval_cb && wp_paged_batch_enabled ();
250277 auto eval_cb_op_return = [&]() -> bool {
278+ bool end_range;
251279 // WP_PAGED_BATCH: break the batch range at every routing boundary
252280 // (each MUL_MAT_ID and its ids-producer). Ending the range here forces
253281 // the scheduler's compute+sync, so the router's ids are materialized
254282 // before the next range reads them, and the routing op runs isolated
255283 // (fixes the read-before-produce H3 + TLS take-steal H4 faults).
256284 if (paged_batch && pager->is_routing_break (t)) {
257- return true ;
285+ end_range = true ;
286+ } else if (batch_eval_cb &&
287+ pager->batch_safe () &&
288+ !routing_tls_set &&
289+ pager->sync_fallback_count () == sync_fallbacks_before) {
290+ end_range = false ;
291+ } else {
292+ end_range = true ;
258293 }
259- if (batch_eval_cb &&
260- pager-> batch_safe () &&
261- !routing_tls_set &&
262- pager-> sync_fallback_count () == sync_fallbacks_before ) {
263- return false ;
294+ if (paged_batch && end_range) {
295+ // Range ends after this op: hand its accumulated pins to the pending
296+ // set, released at the top of the next callback (post-sync).
297+ for ( int p : s_range_pins ) { s_range_pins_pending. push_back (p); }
298+ s_range_pins. clear () ;
264299 }
265- return true ;
300+ return end_range ;
266301 };
267302
268303 // WP_PROFILE_EVAL: time the whole callback body (every return path past
@@ -612,7 +647,7 @@ bool weight_pager_eval_cb(struct ggml_tensor * t, bool ask, void * user_data) {
612647 // alloc_slot in this same eval_cb can't evict
613648 // it. Unpinned in the NEXT eval_cb (above).
614649 pager->pin_page (sub_page_idx);
615- s_pinned_pages_prev_op.push_back (sub_page_idx);
650+ (paged_batch ? s_range_pins : s_pinned_pages_prev_op) .push_back (sub_page_idx);
616651#if defined(GGML_USE_HIP)
617652 s_prev_op_pager = pager;
618653#endif
@@ -1015,7 +1050,7 @@ bool weight_pager_eval_cb(struct ggml_tensor * t, bool ask, void * user_data) {
10151050 // GPU is still reading from it. Unpinned at the top of the NEXT
10161051 // eval_cb invocation.
10171052 pager->pin_page (page_idx);
1018- s_pinned_pages_prev_op.push_back (page_idx);
1053+ (paged_batch ? s_range_pins : s_pinned_pages_prev_op) .push_back (page_idx);
10191054#if defined(GGML_USE_HIP)
10201055 s_prev_op_pager = pager;
10211056#endif
0 commit comments