Perf/fused sparse cache prefetch#430
Conversation
2a52ef4 to
1d0816a
Compare
Greptile SummaryThis PR replaces the multi-step cache lookup → value-copy → insert pipeline with a fused cooperative find-or-insert kernel plus a pipelined
Confidence Score: 3/5Not safe to merge without fixing the counter_flat_index formula and the missing cooperative-launch capability guard; the outstanding_keys_ref exception-safety gap is also a correctness concern. Three P1 issues: a likely index arithmetic bug in counter_flat_index that could corrupt ref counters for multi-table overflow configurations, a ref-count leak if _scalar_item raises after the increment, and a missing device capability check that can crash on non-cooperative-capable GPUs. These are present defects on the changed code paths. insert_and_evict.cu (counter_flat_index formula), batched_dynamicemb_function.py (outstanding_keys_ref increment ordering), scored_hashtable.py (cooperative launch capability guard) Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant PY as batched_dynamicemb_function.py
participant KVT as key_value_table.py
participant HT as scored_hashtable.py
participant GPU as CUDA Kernels
PY->>KVT: cache.find_or_insert(keys)
KVT->>HT: find_or_insert(keys, scores)
HT->>GPU: cudaLaunchCooperativeKernel(table_insert_and_evict)
GPU-->>GPU: Phase 1 - lookup hits, pin cache slots
GPU-->>GPU: grid sync barrier
GPU-->>GPU: Phase 2 - insert misses, evict LRU
GPU-->>HT: hits, miss_slots, evicted_keys
HT-->>KVT: CacheFindOrInsertResult
KVT-->>PY: cache_result
PY->>KVT: storage.exchange(cache_result)
KVT->>GPU: exchange_cache_storage_values_kernel
GPU-->>GPU: Stage 0 - async load tiles
GPU-->>GPU: Stage 1 - prefetch next tiles
GPU-->>GPU: Stage 2 - store completed tiles
GPU-->>KVT: CacheExchangeResult
alt eviction failed
KVT->>GPU: rollback_failed_evictions_kernel
GPU-->>KVT: slots restored
KVT-->>PY: direct_storage_rows fallback
else eviction succeeded
KVT-->>PY: cache rows ready
end
PY->>KVT: release_cache_exchange_refs()
KVT->>GPU: decrement ref counters
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
participant PY as batched_dynamicemb_function.py
participant KVT as key_value_table.py
participant HT as scored_hashtable.py
participant GPU as CUDA Kernels
PY->>KVT: cache.find_or_insert(keys)
KVT->>HT: find_or_insert(keys, scores)
HT->>GPU: cudaLaunchCooperativeKernel(table_insert_and_evict)
GPU-->>GPU: Phase 1 - lookup hits, pin cache slots
GPU-->>GPU: grid sync barrier
GPU-->>GPU: Phase 2 - insert misses, evict LRU
GPU-->>HT: hits, miss_slots, evicted_keys
HT-->>KVT: CacheFindOrInsertResult
KVT-->>PY: cache_result
PY->>KVT: storage.exchange(cache_result)
KVT->>GPU: exchange_cache_storage_values_kernel
GPU-->>GPU: Stage 0 - async load tiles
GPU-->>GPU: Stage 1 - prefetch next tiles
GPU-->>GPU: Stage 2 - store completed tiles
GPU-->>KVT: CacheExchangeResult
alt eviction failed
KVT->>GPU: rollback_failed_evictions_kernel
GPU-->>KVT: slots restored
KVT-->>PY: direct_storage_rows fallback
else eviction succeeded
KVT-->>PY: cache rows ready
end
PY->>KVT: release_cache_exchange_refs()
KVT->>GPU: decrement ref counters
|
| const ExchangeMetadata &metadata, int64_t tile_offset, ValueT *smem) { | ||
| constexpr uint32_t kActiveFlag = | ||
| Direction == kExchangeCacheToStorage | ||
| ? kExchangeMetadataCacheToStorage | ||
| : kExchangeMetadataStorageToCache; | ||
| const bool active = (metadata.flags & kActiveFlag) != 0; |
There was a problem hiding this comment.
__trap() on negative cache row crashes the GPU with no diagnostic
cache_row is cache_slots[input_id], which is the slot_indices tensor produced by find_or_insert. If find_or_insert fails to allocate a slot for any key (e.g., all bucket slots in the relevant probing chain are pinned via positive ref-counts from outstanding batches), slot_indices[i] will be -1. When that value reaches this assertion the kernel issues an illegal instruction (__trap()), which terminates the entire CUDA context with no recoverable diagnostic. The CPU-side outstanding_keys > cache_capacity guard reduces the risk but is a per-batch count, not a per-slot guarantee — with eviction disabled or counters skewed, the check can pass while individual slots still fail to insert. Consider replacing the __trap() with a soft error path (e.g., writing a sentinel to an output error flag tensor checked on the CPU after the kernel) so that a genuinely full cache produces an actionable error rather than a GPU crash.
| final_evict_score = *ovf_bucket.scores(local); | ||
| *ovf_bucket.scores(local) = ScoreType(); | ||
| final_evict_index = ovf_iter; | ||
| } else if (ovf_result == InsertResult::Insert) { | ||
| Iter local = ovf_iter - ovf_output_offsets[t_id]; | ||
| table_key_slot = ovf_bucket.keys(local); | ||
| } | ||
| score = Policy::update(ovf_bucket.scores(local), score); | ||
| } | ||
| } |
There was a problem hiding this comment.
Score updated without holding the bucket lock
table_update_score_by_slot_kernel writes input_scores[i] directly to *bucket.scores(slot % bucket_capacity) (and the overflow equivalent) without acquiring the per-slot lock. Although the slot's reference counter is positive (the caller owns an acquired reference), concurrent table_lookup_kernel threads that happen to be reading the same slot's score for a non-Const policy path will also hold the lock (via try_lock) while reading, creating a window where a torn 8-byte score read is possible. The score write should acquire the lock with bucket.try_lock before writing and release it afterward, consistent with every other in-place score update in this file.
Fuse the cache prefetch into one path: cache find_or_insert, backing-
storage lookup/insert metadata, a single symmetric cache<->storage
value-exchange kernel, admission, direct initialization, and rejected-
slot reclamation. Replaces the per-row legacy prefetch; there is no
legacy fallback and sparse masks stay device-resident.
Cap the exchange launch grid at ~SM/4 blocks. The exchange is latency-
bound on the sparse mapped-host PCIe access path, not SM-bound: a small
grid saturates the request path while freeing the remaining SMs for the
forward/backward compute the prefetch overlaps in the training pipeline.
Measured on EOS H100 no slower -- slightly faster -- than an all-SM
launch (38.8 vs 37.1 GB/s peak), which over-subscribes and adds DRAM
bank/row contention.
Consolidate to the fused kernel with no opt-in flags by removing every
experimental exchange variant explored during development:
- split-overlap / HBM-snapshot directional reader+writer kernels
- independently sorted (CUB radix) directional worklists
- Green-Context writer-SM partitioning
- host-staged CPU gather/scatter engine and its Hopper TMA path
(the whole host_staged_exchange.{cu,h})
- VMM host-NUMA large-page probe in HostVMMTensor
A verified 2 MiB GMMU host mapping did not reduce the sparse-address
penalty (2 MiB == 4 KiB), so GPU translation coverage is not the
bottleneck; and every scheduling/staging variant regressed against the
fused kernel, so none are retained.
Validation on EOS H100 (devel container): clean SM75/80/90/100 build;
377 batched dynamic-embedding-table tests (incl. TestCaching), 584
table-operation tests, and 29 VMM-tensor tests all pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1d0816a to
70494be
Compare
Description
Checklist