@@ -873,6 +873,114 @@ void * WeightPager::ensure(int page_idx) {
873873 return slot_ptr_ (slot);
874874}
875875
876+ void WeightPager::ensure_batch (const std::vector<int > & page_indices,
877+ std::vector<void *> & out_ptrs,
878+ std::vector<int > & out_pinned) {
879+ out_ptrs.assign (page_indices.size (), nullptr );
880+ out_pinned.clear ();
881+ if (!initialized_) return ;
882+
883+ // Pass 1 — resolve hits/pinned inline; for every cold miss reserve AND PIN
884+ // a slot up front. Pinning before any sibling read means alloc_slot for a
885+ // later miss can never evict an earlier miss's in-flight slot (alloc_slot
886+ // skips pinned), which is what collapsed effective QD to ~1 under decode
887+ // eviction. Each pinned page is reported in out_pinned for the caller to
888+ // release next callback.
889+ struct Miss { int page; int slot; std::size_t out_i; };
890+ std::vector<Miss> misses;
891+ misses.reserve (page_indices.size ());
892+ for (std::size_t i = 0 ; i < page_indices.size (); ++i) {
893+ const int p = page_indices[i];
894+ if (p < 0 || p >= catalog_.size ()) continue ;
895+ const PageMeta & m = catalog_.at (p);
896+ if (m.is_pinned ) { // MAD-236: always-resident, no slot
897+ out_ptrs[i] = m.resident_ptr ;
898+ continue ;
899+ }
900+ if (page_loaded_[p]) { // hit — bump LRU, pin, harvest
901+ const int s = page_to_slot_[p];
902+ pool_.mark_used (s);
903+ pool_.pin_slot (s);
904+ out_pinned.push_back (p);
905+ out_ptrs[i] = slot_ptr_ (s);
906+ continue ;
907+ }
908+ if (page_to_slot_[p] >= 0 ) {
909+ // Reserved by an in-flight (e.g. cross-layer) prefetch. Harvest via
910+ // the tested ensure() path (waits on the prefetch, or syncs), then
911+ // pin the result. Not part of the concurrent batch.
912+ void * ptr = ensure (p);
913+ if (ptr != nullptr ) {
914+ pool_.pin_slot (page_to_slot_[p]);
915+ out_pinned.push_back (p);
916+ out_ptrs[i] = ptr;
917+ }
918+ continue ;
919+ }
920+ const int s = pool_.alloc_slot (m.size );
921+ if (s < 0 ) { ++stats_.sync_fallbacks ; continue ; } // pool exhausted (rare)
922+ ensure_slot_map_ (s);
923+ page_to_slot_[p] = s;
924+ slot_to_page_[s] = p;
925+ pool_.pin_slot (s); // PIN before the next alloc
926+ out_pinned.push_back (p);
927+ misses.push_back ({ p, s, i });
928+ }
929+ if (misses.empty ()) return ;
930+
931+ // Pass 2 — issue all cold-miss reads. On the P2P/direct-to-device path the
932+ // reads land straight in the VRAM slots, so one io_uring batch keeps N reads
933+ // in flight at once (true QD=N). Off P2P the shared staging buffer can't
934+ // hold N pages, so fall back to serial sync into each pinned slot (still
935+ // correct; the throughput case that matters for decode is P2P).
936+ if (file_io_->direct_to_device ()) {
937+ std::vector<FileIOBatchRequest> reqs;
938+ reqs.reserve (misses.size ());
939+ for (std::size_t k = 0 ; k < misses.size (); ++k) {
940+ const PageMeta & m = catalog_.at (misses[k].page );
941+ reqs.push_back ({ (uint64_t ) k, (int ) m.file_idx , m.file_offset ,
942+ m.size , slot_ptr_ (misses[k].slot ) });
943+ }
944+ const int n_sub = file_io_->submit_batch (reqs);
945+ file_io_->flush ();
946+ std::vector<bool > ok (misses.size (), false );
947+ int reaped = 0 ;
948+ while (reaped < n_sub) {
949+ IoResult r = file_io_->wait_any (/* timeout_ms=*/ -1 );
950+ if (r.req_id < misses.size ()) {
951+ ok[(std::size_t ) r.req_id ] =
952+ (r.status == IoStatus::Ok &&
953+ r.bytes_read == (int ) catalog_.at (misses[(std::size_t ) r.req_id ].page ).size );
954+ ++reaped;
955+ }
956+ // Unknown req_id (stale prefetch completion): drop, keep waiting.
957+ }
958+ const auto io_t0 = std::chrono::steady_clock::now ();
959+ for (std::size_t k = 0 ; k < misses.size (); ++k) {
960+ const Miss & mm = misses[k];
961+ const PageMeta & m = catalog_.at (mm.page );
962+ if (ok[k] && zero_device_padding (slot_ptr_ (mm.slot ), m.size , pool_.slot_size (mm.slot ))) {
963+ page_to_slot_[mm.page ] = mm.slot ;
964+ page_loaded_[mm.page ] = true ;
965+ slot_to_page_[mm.slot ] = mm.page ;
966+ pool_.mark_used (mm.slot );
967+ record_page_in_ (m.size , seconds_since (io_t0));
968+ out_ptrs[mm.out_i ] = slot_ptr_ (mm.slot );
969+ } else {
970+ // read (or padding) failed — sync-fallback into the SAME pinned
971+ // slot so the up-front pin/out_pinned bookkeeping stays valid.
972+ const int s = page_in_sync_ (mm.page , /* reuse_slot=*/ mm.slot );
973+ out_ptrs[mm.out_i ] = (s < 0 ) ? nullptr : slot_ptr_ (s);
974+ }
975+ }
976+ } else {
977+ for (const Miss & mm : misses) {
978+ const int s = page_in_sync_ (mm.page , /* reuse_slot=*/ mm.slot );
979+ out_ptrs[mm.out_i ] = (s < 0 ) ? nullptr : slot_ptr_ (s);
980+ }
981+ }
982+ }
983+
876984int WeightPager::take_async_transfer_event (int page_idx) {
877985 if (!initialized_) return -1 ;
878986 if (page_idx < 0 || page_idx >= (int ) page_async_event_.size ()) return -1 ;
@@ -1119,10 +1227,13 @@ void WeightPager::mark_cross_layer_prefetch_candidates(const std::vector<int> &
11191227 }
11201228}
11211229
1122- int WeightPager::page_in_sync_ (int page_idx) {
1123- // Synchronous read into a fresh slot, bypassing the prefetch pipeline.
1124- // Used by ensure() on miss. Host transports read into staging and then
1125- // hand off to GpuTransport; P2P transports read into the VRAM slot.
1230+ int WeightPager::page_in_sync_ (int page_idx, int reuse_slot) {
1231+ // Synchronous read into a slot, bypassing the prefetch pipeline. Used by
1232+ // ensure() on miss (reuse_slot < 0: alloc a fresh slot) and by ensure_batch
1233+ // as its per-page failure fallback (reuse_slot >= 0: read into that already-
1234+ // reserved+pinned slot; never release it here). Host transports read into
1235+ // staging and then hand off to GpuTransport; P2P transports read into VRAM.
1236+ const bool owns_slot = (reuse_slot < 0 );
11261237
11271238 static int s_diag_count = 0 ;
11281239 const bool diag = (s_diag_count < 5 );
@@ -1137,9 +1248,12 @@ int WeightPager::page_in_sync_(int page_idx) {
11371248
11381249 const PageMeta & m = catalog_.at (page_idx);
11391250
1140- const int slot = pool_.alloc_slot (m.size );
1141- if (slot < 0 ) return -1 ;
1142- ensure_slot_map_ (slot);
1251+ int slot = reuse_slot;
1252+ if (slot < 0 ) {
1253+ slot = pool_.alloc_slot (m.size );
1254+ if (slot < 0 ) return -1 ;
1255+ ensure_slot_map_ (slot);
1256+ }
11431257 void * dst = slot_ptr_ (slot);
11441258 if (diag) LLAMA_LOG_ERROR (" [DIAG] page_in_sync_[%d]: alloc_slot ok, slot=%d dst=%p\n " , s_diag_count, slot, dst);
11451259
@@ -1151,7 +1265,7 @@ int WeightPager::page_in_sync_(int page_idx) {
11511265 if (staging == nullptr || m.size > sync_staging_size_) {
11521266 LLAMA_LOG_ERROR (" wp::WeightPager::page_in_sync_: page %d size %zu exceeds shared staging size %zu\n " ,
11531267 page_idx, m.size , sync_staging_size_);
1154- pool_.release_slot (slot);
1268+ if (owns_slot) pool_.release_slot (slot);
11551269 return -1 ;
11561270 }
11571271
@@ -1163,7 +1277,7 @@ int WeightPager::page_in_sync_(int page_idx) {
11631277 if (evt < 0 ) {
11641278 LLAMA_LOG_WARN (" wp::WeightPager::page_in_sync_: host tier gpu stage_in failed for page %d\n " ,
11651279 page_idx);
1166- pool_.release_slot (slot);
1280+ if (owns_slot) pool_.release_slot (slot);
11671281 return -1 ;
11681282 }
11691283 transport_.release_event (evt);
@@ -1212,13 +1326,13 @@ int WeightPager::page_in_sync_(int page_idx) {
12121326 if (diag) LLAMA_LOG_ERROR (" [DIAG] page_in_sync_[%d]: stage1 done ok=%d\n " , s_diag_count, (int )ok);
12131327 if (!ok) {
12141328 LLAMA_LOG_WARN (" wp::WeightPager::page_in_sync_: file IO failed for page %d\n " , page_idx);
1215- pool_.release_slot (slot);
1329+ if (owns_slot) pool_.release_slot (slot);
12161330 return -1 ;
12171331 }
12181332
12191333 if (direct_to_device) {
12201334 if (!zero_device_padding (dst, m.size , pool_.slot_size (slot))) {
1221- pool_.release_slot (slot);
1335+ if (owns_slot) pool_.release_slot (slot);
12221336 return -1 ;
12231337 }
12241338 page_to_slot_[page_idx] = slot;
@@ -1241,7 +1355,7 @@ int WeightPager::page_in_sync_(int page_idx) {
12411355 if (diag) LLAMA_LOG_ERROR (" [DIAG] page_in_sync_[%d]: stage_in returned evt=%d\n " , s_diag_count, evt);
12421356 if (evt < 0 ) {
12431357 LLAMA_LOG_WARN (" wp::WeightPager::page_in_sync_: gpu stage_in failed for page %d\n " , page_idx);
1244- pool_.release_slot (slot);
1358+ if (owns_slot) pool_.release_slot (slot);
12451359 return -1 ;
12461360 }
12471361 transport_.release_event (evt);
0 commit comments