Skip to content

Implement pre-sorting, caching and contigous warp processing in group_index_select#144

Open
avbokovoy wants to merge 126 commits into
abokovoi/upstreamfrom
abokovoi/group-index-sort-and-cache-opt
Open

Implement pre-sorting, caching and contigous warp processing in group_index_select#144
avbokovoy wants to merge 126 commits into
abokovoi/upstreamfrom
abokovoi/group-index-sort-and-cache-opt

Conversation

@avbokovoy

Copy link
Copy Markdown

Follow-up of #139

The differences are:

  1. Reduced #ifdef USE_ROCM usage in favor of if constexpr (OPT_BOOL).
  2. Added compile-time host side codegen guard for the kernel (CUDA vs ROCm)
  3. Fixed an issue with tailing row cache flush

@avbokovoy avbokovoy self-assigned this Mar 3, 2026

@aryaman-gupta aryaman-gupta left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR introduces crucial optimizations for the group_index_select_or_add_2d_kernel. The majority of the code is clean and the separation of ROCm and CUDA codepaths has been done well.

Most of these changes were already reviewed in #139 . I have left a few comments that I think should be looked at before merging. Some of these are design choices, and the PR could proceed with merging even if the code is not modified,

Comment thread fbgemm_gpu/include/fbgemm_gpu/utils/rocm/sparse_group_utils.h Outdated
Comment thread fbgemm_gpu/src/sparse_ops/sparse_ops_cpu.cpp Outdated
Comment thread fbgemm_gpu/src/sparse_ops/sparse_ops_cpu.cpp Outdated
Comment thread fbgemm_gpu/src/sparse_ops/sparse_ops_cpu.cpp Outdated
Comment thread fbgemm_gpu/src/sparse_ops/sparse_ops_gpu.cpp Outdated
Comment thread fbgemm_gpu/src/sparse_ops/sparse_ops_gpu.cpp Outdated
Comment thread fbgemm_gpu/include/fbgemm_gpu/utils/rocm/sparse_group_utils.h Outdated
Comment thread fbgemm_gpu/include/fbgemm_gpu/utils/rocm/sparse_group_utils.h Outdated
Comment thread fbgemm_gpu/src/sparse_ops/sparse_group_index.cu
@avbokovoy
avbokovoy force-pushed the abokovoi/group-index-sort-and-cache-opt branch from 3754ab4 to 1248c83 Compare March 11, 2026 12:21
@avbokovoy
avbokovoy requested a review from aryaman-gupta March 11, 2026 12:35

@aryaman-gupta aryaman-gupta left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The points raised in #144 (review) have been addressed and the PR is ready for merging

@avbokovoy
avbokovoy force-pushed the abokovoi/group-index-sort-and-cache-opt branch from ed3fc4b to f5f9e70 Compare April 1, 2026 13:12
AlbertDachiChen and others added 22 commits June 9, 2026 08:35
Summary:
Pull Request resolved: pytorch#5799

X-link: https://github.com/facebookresearch/FBGEMM/pull/2727

D104827588 (length kernel) and f2e9cba2e279 (delinearize kernel) both introduced an unstated "indices must lie in [0, per_feature_hash)" assumption that the op never enforced before. ZCH callers that pass raw IDs to `jagged_unique_indices` (dedup runs before the hash-to-bucket remap) tripped this on the last feature in prod, producing `sum(output_lengths) < unique_indices.numel()` and crashing downstream `dist.all_to_all_single` with "Split sizes doesn't match total dim 0 size".

Two fixes in `jagged_unique_indices.cu`:

1. `unique_indices_length_kernel`: when the group's `hash_end == hash_size_cumsum.size(0) - 1` (last feature), set `hi_pos = linear_unique_indices.size(0)` instead of doing the upper-bound binary search. The OOB tail of the last feature sorts past `hash_size_cumsum[T]` and the binary search would otherwise stop short of it.

2. Replace `delinearize_unique_from_sorted_kernel` (gather: `unique_indices[i] = v - hash_size_cumsum[t]`) with the original `delinearize_unique_index_kernel` (scatter: `unique_indices[reverse_index[i]] = indices[i]`). The gather form misattributes OOB values whose linearized key exceeds `hash_size_cumsum[T]` to a phantom feature `t = T` and emits `v - hash_size_cumsum[T]` instead of the original index value. The scatter form is index-bound-agnostic by construction. Note `reverse_index` is `int64_t` (from the cub pipeline) rather than templated `index_t` (which was the case under `at::_unique`).

Contract enforcement: added a `CUDA_KERNEL_ASSERT` in `linearize_index_flat_kernel` that fires when an intermediate feature (`t < T - 1`) with `per_feature_hash > 0` has `idx >= per_feature_hash`. Intermediate-feature OOB causes silent per-feature count drift (counts leak from `t` to `t+1` while the total is preserved) — this has been broken since the op was written, but no caller hit it, so the assert surfaces violations rather than silently corrupting downstream embedding lookups. The assert exempts (a) the last feature (legitimately supported) and (b) merged/masked features with `per_feature_hash == 0` (the `hash_size_offsets` indirection pattern used by `multi_keys` and `zch_huge_hash_size`).

The pipeline contract doc comment above `jagged_unique_indices_cuda` is updated to enumerate the three cases.

Reviewed By: q10

Differential Revision: D106305472

fbshipit-source-id: 1ccfb44e5a08ddf257e1484473323f7a1accd6ba
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2780

Pull Request resolved: pytorch#5859

- Remove CUDA 12.9.1 support from FBGEMM, following PyTorch

Reviewed By: cthi

Differential Revision: D107971273

fbshipit-source-id: 04f754d52137bb7c884dfe8e19e517540eaf121a
Summary:
Pull Request resolved: pytorch#5858

X-link: https://github.com/facebookresearch/FBGEMM/pull/2773

This PR implements direct HBM->LDS stores in tbe inference kernel. There are 2 major changes:

1. Rows data isn't loaded in-place, instead we store pointers to global memory and store the actual data w.r.t. the predicate into LDS. In case predicate is false, we pre-allocate small chunk of static device memory of 16B once, fill it with zeros, and fallback to this chunk
2. HBM->LDS 16B loads are implemented for ROCm >= 7.0 and MI350. We can expand the support range to MI30* through 4B loads, however it doesn't bring any performance benefits because we'll have to introduce an overhead of addresses transposition and 4x more load operations. You can find out the reference implementation here: pytorch@fe52557.

Due to pre-7.2 ROCm features, we are forced to used assembly inline to get 16B loads to work, so manual synchronization was added. In case of ROCm >= 7.2, we use proper intrinsics to handle memory synchronization.

This change brings ~10% performance boost on average for weighted and unweighted cases. We may try to push it further by doing async loads for indices weights.

Pull Request resolved: pytorch#5348

Reviewed By: spcyppt

Differential Revision: D91496421

Pulled By: q10

fbshipit-source-id: acbbc2287b87ba2ecab1524c1300bb431667cc34
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2781

generate_opcheck_tests records a "xsuccess" status when a previously-failing opcheck
variant now passes -- i.e. the entry is stale and should be removed. Removing these
de-clutters the failures dicts and drops the problematic-test count with zero
behavioral risk (the tests already pass). 12 stale xsuccess entries removed:

- jagged: fbgemm::jagged_index_select (KeyedJaggedIndexSelectTest aot_dispatch_dynamic
  + faketensor for test_keyed_jagged_index_select_dim1) -> {}
- sparse: fbgemm::generic_histogram_binning_calibration_by_feature (4),
  fbgemm::group_index_select_dim0 (2), fbgemm::segment_sum_csr (2) -> {}
- quantize: fbgemm::FloatToFP8RowwiseQuantized (2) -> {}

Emptied keys are left as {} per the existing convention for healthy ops. Legitimate
xfail entries (data-dependent / dynamic-output-shape variants) are untouched.

Part of the fbgemm_dev test-health remediation for T191384137.

Reviewed By: henrylhtsang

Differential Revision: D107927030

fbshipit-source-id: ca72cd34d26db7a854e54a3a7c1f47009e9c9b28
Summary:
Pull Request resolved: pytorch#5856

Fixes a ROCm/AMD GPU deadlock in gemv_quantized_fp8_fp8
(experimental/gen_ai/src/quantize/fast_gemv/include/fast_gemv.cuh), found by
auditing fbgemm_gpu for the same barrier-divergence pattern fixed in D107554507.

In the single-warp branch (blockDim.x <= WARP_SIZE), the `return` was nested
inside `if (tid == 0)`, so only thread 0 exited while lanes 1..N-1 fell through
to the block-wide __syncthreads(). On ROCm (WARP_SIZE=64) the blockDim.x in
{32,64} instantiations take this branch, so the waiting lanes deadlock (latent
UB on NVIDIA).

This diff moves the `return` out of the `if (tid == 0)` block so all threads in
a single-warp block return together and none reach __syncthreads() -- matching
the sibling gemv_bf16 / gemv_quantized_bf16_fp8 / gemv_quantized_int4 kernels in
the same file. The multi-warp path is unchanged.

Reviewed By: henrylhtsang

Differential Revision: D107947000

fbshipit-source-id: b3aa48de725eab6068ee5532f10106d76c062f60
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2743

Pull Request resolved: pytorch#5817

Add out-of-band dirty bit tracking to FixedBlockPool using a striped concurrent hash set. The dirty bit is tracked separately from MetaHeader to maintain backward compatibility with serialization. Blocks are marked dirty when their embedding data is modified, and the bit is cleared on allocate/deallocate. This is foundational for the DRAM+SSD composite backend to know which DRAM blocks need to be flushed to SSD.

Reviewed By: EddyLXJ

Differential Revision: D107291829

fbshipit-source-id: bc06ee629ffc09556021ef339b9b86c45a21c4cc
…ytorch#5861)

Summary:
Pull Request resolved: pytorch#5861

X-link: https://github.com/facebookresearch/FBGEMM/pull/2782

block_bucketize_sparse_features_meta unconditionally returned a Tensor for the 5th
output (unbucketize_permute), but the real CPU/CUDA kernels return it only when
`sequence` is True (CPU sparse_ops_cpu.cpp:1175/1262/1280; CUDA
sparse_block_bucketize_features.cu:831-833/956-957; schema sparse_ops_cpu.cpp:3822
-> (Tensor, Tensor, Tensor?, Tensor?, Tensor?)). This Tensor-vs-None mismatch is the
root cause behind the block_bucketize test_faketensor__* opcheck xfails.

Gate output 5 on `sequence` (the param is already in scope) so the meta matches real
kernel semantics. outputs 3 (weights) and 4 (pos) were already correct.

The corresponding block_bucketize test_faketensor__* xfails in
sparse/failures_dict.json are intentionally left in place: once GPU CI confirms they
now pass they will surface as (tolerated) xsuccess and can be removed in a follow-up
cleanup. Removing them pre-emptively would hard-fail if any variant still mismatches.

Part of the fbgemm_dev test-health remediation for T191384137.

Reviewed By: henrylhtsang

Differential Revision: D107927031

fbshipit-source-id: 02a28b1bbdd593332e1eced9f8dce27c80adb3c2
Summary:
Pull Request resolved: pytorch#5862

The py3_14t-rocm7_2 manywheel build/upload legs were failing in OSS CI (3 FAILING
signals on the fbgemm_dev surface, T191384137). Free-threaded Python 3.14 is not yet
supported for the ROCm wheel build, mirroring the already-excluded 3.13t+rocm combo.

Extend the existing Nova matrix exclusion from
  gpu_arch_type:rocm;python_version:3.13t
to
  gpu_arch_type:rocm;python_version:3.13t,3.14t
in the three wheel workflows that build ROCm (build_wheels_linux_x86,
build_wheels_genai_linux_x86, build_wheels_linux_aarch64). This drops the failing
py3.14t ROCm legs until upstream free-threaded ROCm support lands.

Reviewed By: henrylhtsang

Differential Revision: D107927032

fbshipit-source-id: bf19d134ba27d773eb222afa142109c9521f293d
Summary:
Pull Request resolved: pytorch#5863

X-link: https://github.com/facebookresearch/FBGEMM/pull/2784

test_scatter_add_padded_tokens was failing as a TIMEOUT (not a kernel bug). Two causes,
both confirmed by running on GPU (B200 via RE):

1. torch.compile recompilation explosion. The Hypothesis sweep ran the compiled=True
   path by calling torch.compile(scatter_add_padded_tokens) per example. dynamo
   recompiled on every dtype/shape/expert-count combo (e.g. "tensor 'token_counts'
   dtype mismatch. expected Long, actual Int"; then "in_tokens ... Int vs BFloat16"),
   blew past config.recompile_limit (8), after which every call recompiled (~1-2 min
   each) -> timeout.
2. The eager sweep itself was too slow at max_examples=100: each example re-autotunes
   the triton kernel for a new (EP, E, T_BUCKET, D) key and runs an expensive Python
   reference over dim=5120.

Fixes:
- Split the compiled path into a dedicated test_scatter_add_padded_tokens_compiled that
  exercises torch.compile on a small fixed set of configs (one compile per dtype),
  compiled once via an lru_cache'd torch.compile(..., dynamic=True) singleton.
- The parametrized sweep (test_scatter_add_padded_tokens) now runs eager only, with
  max_examples reduced to 20.
- Make token_counts consistently int32 (bincount returns int64) for parity with the
  balanced path and the kernel.
- Extract the shared body into _run_scatter_add_padded_tokens.

Completes the MoE bucket for T191384137 (the shuffling off-by-one was fixed in
D107927038).

Reviewed By: henrylhtsang

Differential Revision: D107936151

fbshipit-source-id: ec5f31a4bdc69b14e41f6eb4992f82db3486e34c
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2774

Add an `enable_dirty_tracking` option to `SynchronizedShardedMap` and forward it to the pool constructor, and thread the same parameter through `InferenceFixedBlockPool` so the shared template instantiates for both pool types. Defaults to false, so existing behavior is unchanged. This lets callers (e.g. the FeatureEvict SSD writeback path) construct pools that track dirty blocks.

Reviewed By: EddyLXJ

Differential Revision: D107967620

fbshipit-source-id: 68bdbd2e391192b212fd4b177ca172356282f916
…rch#5857)

Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2779

Pull Request resolved: pytorch#5857

Fixes a ROCm/AMD GPU deadlock in get_pruning_lengths-style barrier divergence,
found by auditing fbgemm_gpu for the same pattern fixed in D107554507.

In quantize_float_to_mx4_kernel (src/quantize_ops/quantize_mx.cuh), threads
whose linear_tid >= total_elems hit an early `return` before the block-wide
__syncthreads() calls. The kernel is launched with multiple groups per block
(blockDim.y = num_groups_per_block = MAX_THREADS / mx_group_size) and
gridDim_x = div_round_up(total_num_groups, num_groups_per_block), so when
total_num_groups is not a multiple of num_groups_per_block the last block
contains whole trailing groups whose threads all return early while the active
groups wait at __syncthreads() -> deadlock on ROCm (latent UB on NVIDIA).

This diff removes the early return and instead masks per-thread work with an
`active = linear_tid < total_elems` flag: the input load, the smem combine
writes, and the output write are guarded, while all threads fall through to
every __syncthreads(). Inactive lanes contribute the minimum biased exponent
(0) so the max reductions are unaffected. Behavior is identical for active
groups. The sibling dequantize_mx4_to_float_kernel has no barrier after its
early return and is left unchanged.

## Where the hanging barrier actually is (not apparent from the diff)

Unlike the gen_ai amax/quantize twin (D107946896), the barriers here are in the
kernel body, but which ones hang and under what condition is still subtle:

- Unconditional block-wide barriers: __syncthreads() at the even/odd 4-bit
  combine steps (post-fix lines ~205 and ~212). These are ALWAYS reached, so they
  are the primary hang points.
- Conditional block-wide barriers: __syncthreads() at lines ~141 and ~159 are
  only compiled in the has_multiple_warps_in_group == true template instantiation
  (mx_group_size > WARP_SIZE); for mx_group_size <= WARP_SIZE only the
  unconditional pair applies.
- (The fbgemm_gpu::syncwarp() at line ~148 is warp-scoped, not a block barrier,
  and is not the hang point.)

Why a subset of the block skips them (the deadlock condition):
- The block is 2D: blockDim = (mx_group_size, num_groups_per_block) with
  num_groups_per_block = MAX_THREADS / mx_group_size, so a block holds MULTIPLE
  groups along threadIdx.y. The __syncthreads() calls are block-wide (all
  mx_group_size * num_groups_per_block threads) even though they are only
  logically needed within a single group's smem region.
- Because total_elems = total_num_groups * mx_group_size, the early-return
  condition linear_tid >= total_elems is WHOLE-GROUP: an entire threadIdx.y row
  is either fully active or fully inactive (never partial).
- gridDim_x = div_round_up(total_num_groups, num_groups_per_block). When
  total_num_groups is not a multiple of num_groups_per_block, the LAST block
  contains whole trailing (inactive) groups whose threads all returned, while the
  active groups in the same block reach the block-wide __syncthreads() and wait
  forever -> ROCm hang.

Corollary / dependency: with num_groups_per_block == 1 (one group per block) the
bug would not trigger, since the early return would be block-uniform. It is the
multi-group-per-block launch that turns the per-group early return into a
partial-block barrier divergence.

Reviewed By: henrylhtsang

Differential Revision: D107946777

fbshipit-source-id: 29f6cca1441a49828e1b753c9f214feb0eb0d4aa
…rch#5864)

Summary:
Pull Request resolved: pytorch#5864

First foundational diff of the BC-safe TBE type migration (config types only).

- tbe/config/embedding_config.py becomes the canonical home for the config
  types; adds get_new_embedding_location() free-fn parity with the
  EmbeddingLocation.from_device_and_clf() classmethod.
- Pins __module__ of the four TorchScript/pickle identity-serialized config
  types (EmbeddingLocation, PoolingMode, ComputeDevice, RecordCacheMetrics) back
  to fbgemm_gpu.split_table_batched_embeddings_ops_common so already-published
  TorchScript graphs and RankAGI training-TBE pickles still resolve/repackage.
  torch.package never rewrites a class __module__, so the pin survives interning.
- split_table_batched_embeddings_ops_common.py drops its duplicate config-type
  definitions and re-exports them from the leaf, keeping the legacy import path
  working. Cache/SSD types remain defined here and migrate in follow-up diffs.
- BUCK: split_table_batched_embeddings_ops_common deps += tbe:tbe_config.

This is the safe, acyclic version of the config portion of D103477971 (which
caused SEV S669532). See plan fbgemm_tbe_type_migration_bc_safe sections A.2/A.4.

Reviewed By: henrylhtsang

Differential Revision: D107684319

fbshipit-source-id: 42a887f29612c8bcf432838aea64c0a6bab8b7ce
Summary:
Pull Request resolved: pytorch#5865

X-link: https://github.com/facebookresearch/FBGEMM/pull/2786

Second foundational diff of the BC-safe TBE type migration (cache types).

- tbe/cache/cache_config.py is the canonical home for CacheAlgorithm, CacheState,
  MultiPassPrefetchConfig, UVMCacheStatsIndex, DEFAULT_ASSOC. Pins
  CacheAlgorithm.__module__ back to split_table_batched_embeddings_ops_common so
  already-published scripted TorchScript graphs / torchrec CacheParams pickles
  still resolve and repackage (torch.package never rewrites a class __module__).
- tbe/cache/__init__.py guards the heavy .split_embeddings_cache_ops import with
  try/except so the new lightweight :tbe_cache_config target loads cleanly.
- split_table_batched_embeddings_ops_common.py drops its duplicate cache-type
  definitions and re-exports them from the leaf; construct_cache_state becomes a
  thin forwarder to CacheState.construct(). Legacy import path preserved.
- BUCK: add lightweight :tbe_cache_config (cache __init__ + cache_config.py);
  :tbe_cache_ops now omits cache_config.py and deps :tbe_cache_config;
  ops_common deps += :tbe_cache_config. Acyclic (leaf-or-shim -> tbe_cache_config
  -> tbe_config; ops_common -> tbe_cache_config).

Safe, acyclic, identity-preserving version of the cache portion of D103477971
(which caused SEV S669532). See plan fbgemm_tbe_type_migration_bc_safe A.2/A.4.

Reviewed By: henrylhtsang

Differential Revision: D107684313

fbshipit-source-id: 8d0e1e0a62544beca8ed634a5e0f5bc51802417b
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2791

Pull Request resolved: pytorch#5871

The `group-index-select-2d-bench` subcommand declared the CLI options
`--input-dims`, `--input-strides`, and `--index-dim` but never implemented the
body that uses them, so passing the flags was a silent no-op: every invocation
ran the default homogeneous synthetic shape regardless of the ragged flags.

This diff implements the ragged path so the bench actually measures the
per-group production shapes (matching the echen40 reference scripts), and
unifies the forward + backward benchmarking step for both families:

- Add `_gen_synthetic_group_inputs` and `_gen_ragged_group_inputs` helpers,
  each returning `(input_group, indices_group)`.
- Branch on `--input-dims` (empty/whitespace normalizes to the synthetic path;
  `--index-dim` is required for ragged; `--input-strides` is optional and
  defaults to contiguous strides per group).
- Unify forward/backward on `torch.ops.fbgemm.group_index_select_dim0` with
  `torch.cat(output_group, dim=1)`; drop the single-tensor PyTorch
  `torch.index_select` reference (cannot represent heterogeneous columns).
- Align the D96328337 run harness: pass `--input-precision`/`--sort-indices`
  for ragged configs and record `sort_indices=false` (echen40 parity).

The measured kernel (group_index_select_or_add_2d_kernel) is unaffected by the
concat axis, so the existing synthetic A/B results remain valid.

Reviewed By: spcyppt

Differential Revision: D108184412

fbshipit-source-id: fef51b3d51cebcc0d6df89de1a3474dbc4f08f8a
…ytorch#5869)

Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2789

Pull Request resolved: pytorch#5869

block_bucketize_test had 14+ FAILING test_faketensor__ / test_aot_dispatch_dynamic__
opcheck variants (populate_bucketized_permute*, block_bucketize_sparse_features_inference).
Two root causes:

1. block_bucketize_test.py was MISSING `import fbgemm_gpu.sparse_ops` -- every sibling
   sparse test file has it, and fbgemm_gpu/__init__ only imports it in OSS. Without it
   the Python meta/FakeTensor impls in sparse_ops.py were never registered in the test
   process, so every faketensor/aot_dispatch opcheck variant errored
   ("could not find the abstract impl ... import fbgemm_gpu.sparse_ops"). The xfail'd
   variants "passed" via that error; the non-xfail'd ones (populate, inference) failed.
2. populate_bucketized_permute had NO meta at all, and the inference / 2d_weights metas
   returned unbucketize_permute (output 5) unconditionally, but the real CPU/CUDA kernels
   only return it when `sequence` is set (sparse_block_bucketize_features.cu:956).

Fixes:
- Add `import fbgemm_gpu.sparse_ops` to block_bucketize_test.py so the metas load.
- Add populate_bucketized_permute_meta (returns empty_like(bucket_mapping), matching
  native_empty_like in sparse_ops_cpu.cpp:1117) and register it via impl_abstract.
- Gate output 5 on `sequence` in block_bucketize_sparse_features_inference_meta and
  block_bucketize_sparse_features_2d_weights_meta (same fix as D107927031 for the base op).
- With the metas now correctly loaded, 22 previously-xfail'd block_bucketize_sparse_features
  faketensor/aot_dispatch entries became unexpected successes -> removed them from
  sparse/failures_dict.json (kept the 2 BlockBucketize2DWeightsTest *_vs_original entries
  that still genuinely fail).

Part of the fbgemm_dev test-health remediation for T191384137.

Reviewed By: henrylhtsang

Differential Revision: D108108673

fbshipit-source-id: 00ccaa84d537bf14b52e652bf4f1e68e7cfd33c7
…h#5870)

Summary:
Pull Request resolved: pytorch#5870

Same root cause as D108108673: block_bucketize_2d_weights_test.py was missing
`import fbgemm_gpu.sparse_ops`, so the Python meta/FakeTensor impls were never
registered for that test process and every faketensor/aot_dispatch opcheck variant
errored ("could not find the abstract impl"). The xfail'd variants "passed" via the
error; the genuine ones failed.

Adding the import (which all sibling sparse test files already have) loads the metas --
including the block_bucketize_sparse_features_2d_weights_meta sequence-gating fix from
D108108673. With the metas loaded, all 16 previously-xfail'd BlockBucketize2DWeightsTest
faketensor/aot_dispatch entries became unexpected successes, so they are removed from
sparse/failures_dict.json:
- fbgemm::block_bucketize_sparse_features_2d_weights: 16 -> 0
- fbgemm::block_bucketize_sparse_features: 2 -> 0 (the *_2d_weights_vs_original entries,
  which opcheck the base op from the 2DWeights test, also now pass)

Part of the fbgemm_dev test-health remediation for T191384137.

___

Differential Revision: D108114892

fbshipit-source-id: f64e19559ff505850014422b0ae3de409819cad0
…5875)

Summary:
Pull Request resolved: pytorch#5875

X-link: https://github.com/facebookresearch/FBGEMM/pull/2794

Add ROCm-specific `subwarp_reduce_add` template function using `__shfl_xor` intrinsics for subwarp reductions, and redefine `GROUP_REDUCE_ALL_SUM` to use it on ROCm. This replaces the NVIDIA `warpReduceAllSum` path which requires `shfl_sync_mask` not available on AMD GPUs. Affects both warp and CTA backward kernels via the shared optimizer device kernel template.

| T | Ds / D | B | Baseline (No flag) | This diff (No flag) | Δ % (No flag) | Baseline (With flag) | Diff (With flag) | Δ % (With flag)
| 2 | 8 | — | 65 | 65 | 0.00% | 65 | 65 | 0.00%
| 10 | 128 | — | 394 | 376 | 5.00% | 394 | 376 | 5.00%
| 14 | 12/24/128 | — | 373 | 360 | 3.00% | 374 | 357 | 5.00%
| 18 | 20/128 | — | 309 | 298 | 4.00% | 308 | 298 | 3.00%
| 22 | 20/24/128 | — | 424 | 405 | 4.00% | 425 | 405 | 5.00%
| 1 | 8 | 2048 | 102 | 100 | 2.00% | 103 | 101 | 2.00%
| 1 | 8 | 4096 | 180 | 177 | 2.00% | 181 | 177 | 2.00%
| 1 | 8 | 131072 | 1485 | 1492 | 0.00% | 1483 | 1487 | 0.00%
| 1 | 128 | 2048 | 115 | 112 | 3.00% | 114 | 114 | 0.00%
| 1 | 128 | 4096 | 203 | 200 | 1.00% | 222 | 217 | 2.00%
| 1 | 128 | 131072 | 1800 | 1790 | 1.00% | 1900 | 1738 | 9.00%

Reviewed By: q10

Differential Revision: D107779306

fbshipit-source-id: c0f4dffa411c05005141297731f19633281139d6
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2795

Third foundational diff of the BC-safe TBE type migration: make tbe.ssd.ssd_config the
canonical home for the SSD/KVZCH types and turn ops_common into a pure re-export shim.

- tbe/ssd/ssd_config.py is canonical for BackendType, EvictionPolicy, EnrichmentType,
  EnrichmentResponseFormat, EnrichmentPolicy, KVZCHParams, KVZCHTBEConfig (init-time only;
  no __module__ pinning required).
- tbe/ssd/__init__.py guards the heavy .common/.inference/.inference_serving/.training
  imports (re-raising anything but the specific absent submodule) so the new lightweight
  :tbe_ssd_config target loads cleanly under torch.package.
- split_table_batched_embeddings_ops_common.py is now a pure re-export shim: config + cache
  types eager, SSD/KVZCH lazy via __getattr__ to avoid a tbe.ssd -> .training -> ops_training
  import cycle; plus the construct_cache_state() forwarder. Legacy import path preserved.
- BUCK: add lightweight :tbe_ssd_config; :ssd_split_table_batched_embeddings_ops drops
  ssd_config.py and deps :tbe_ssd_config. ops_common deps += :tbe_ssd_config and is marked
  autodeps-skip so the tbe leaf config deps (only reachable lazily) stay pinned into every
  publisher's torch.package closure (SEV-prevention hardening, S669532/S672649). Acyclic:
  ops_common -> {tbe_config, tbe_cache_config, tbe_ssd_config}.

Completes the acyclic, identity-preserving redo of D103477971 (SEV S669532) for the
foundational fbgemm-internal layer.

Reviewed By: henrylhtsang

Differential Revision: D107684315

fbshipit-source-id: 3fdb8135d6be630b18a9ee91150c1ae4a76fe104
Summary:
Pull Request resolved: pytorch#5881

X-link: https://github.com/facebookresearch/FBGEMM/pull/2800

Tier-1 follow-up to D104903707 / D104937969 — apply the canonical
ROCm grid-overflow cap to a `split_embeddings_cache/` launch site
identified by the audit at:
`/home/bensonma415/.llms/plans/permute_metric_layout_etc_rocm_grid_overflow_audit.plan.md`

`lru_cache_insert_kernel` is launched with grid
`div_round_up(N, kMaxThreads / kWarpSize)` and block
`dim3(kWarpSize=32, kMaxThreads/kWarpSize=32) = 1024` threads, where
`N = cache_set_sorted_unique_indices.numel()`. Total threads ~= 32 * N.
For `N >= 2^27` (~134 M unique indices, reachable in large TBE
workloads), total threads exceed HIP's 2^32 hard limit, tripping
`KernelLauncher::checkThreadCountNotExceeded`.

The kernel already grid-strides over `n` (line 48), so capping the
host-side grid is correctness-preserving. NVIDIA codegen sees the
host-side `#ifdef USE_ROCM` block as a plain alias and remains a no-op
delta in PTX/SASS.

The fix targets only the `lock_cache_line=False` branch.
The `lock_cache_line=True` branch uses
`div_round_up(SM_cnt, ALL_TO_PREFETCH_SM_RATIO)` which is already
SM-bounded; the cap on its branch is a no-op.

Reviewed By: spcyppt

Differential Revision: D105282095

fbshipit-source-id: dde6db74cbff6113bb69aebe81d36268b2d599bf
Summary:
Pull Request resolved: pytorch#5879

X-link: https://github.com/facebookresearch/FBGEMM/pull/2798

dense_to_jagged_forward derives the output's leading dim (total_L) from
offsets.back().max() with no validation. A corrupted/garbage offset produced a
huge negative value (~-1.4e18) that flowed straight into at::empty and aborted
with an opaque "Trying to create tensor with negative dimension" error, seen in a
real ROCm training crash through fbgemm_gpu::dense_to_jagged.

Add a TORCH_CHECK_VALUE guard in the CPU and CUDA forward paths so the op fails
fast with the offending value and a clear message instead of an allocator abort.
The guard runs only in the concrete kernels; symbolic/torch.export tracing still
uses the Python abstract impl.

Reviewed By: haoyuz

Differential Revision: D108330675

fbshipit-source-id: 7a06c405f88106e6883d58e872222632b0d04347
Summary:
Pull Request resolved: pytorch#5886

X-link: https://github.com/facebookresearch/FBGEMM/pull/2807

Register `DispatchKey::Autograd` and `DispatchKey::Meta` for `dense_embedding_codegen_lookup_function` in the codegen template. Previously, the `{% if not dense %}` guard in `embedding_backward_split_host_template.cpp` skipped these registrations for the dense variant, causing `test_autograd_registration` and `test_faketensor` opcheck tests to fail.

The non-dense split ops already register the same function at both Autograd and Meta dispatch keys (the autograd.Function works even without autograd enabled, and redispatches to lower-level ops that have their own Meta kernels). The dense variant uses the same pattern.

This also reverts the `failures_dict_fast.json` xfail entries and `additional_decorators` workaround from `BackwardDeterminismTest` since they are no longer needed.

Reviewed By: q10

Differential Revision: D107814730

fbshipit-source-id: e67be3e28681e2ac8ac843e9b71fea1e4a081f53
… typing (pytorch#5834)

Summary:
Pull Request resolved: pytorch#5834

X-link: meta-pytorch/torchrec#4320

X-link: https://github.com/facebookresearch/FBGEMM/pull/2757

The SymInt magic-method typing change added strict annotations to the previously-untyped `torch._check`, `torch._check_is_size`, and `torch._constrain_as_size` helpers (`cond: bool | SymBool`, `i`/`symbol: IntLikeType`). Call sites that pass a `Tensor.item()` result into these helpers now fail type checking, because `Tensor.item()` is typed `bool | float | int`, which is not assignable to `IntLikeType` or `bool | SymBool`. The values are genuinely int/bool-like here (tensor sizes and boolean comparison results); the error comes purely from `item()`'s necessarily-broad return type.

This wraps the offending arguments in `typing.cast(int, ...)` / `typing.cast(bool, ...)`. `typing.cast` is erased at runtime, so behavior is unchanged. We deliberately do not use `int(...)` / `bool(...)`: under `torch.compile` / `torch.export` these `.item()` values are unbacked `SymInt` / `SymBool`, and forcing `int()` / `bool()` would trigger `GuardOnDataDependentSymNode` and alter runtime behavior.

Reviewed By: bobrenjc93

Differential Revision: D107556825

fbshipit-source-id: b3be6de347c996fde3e602e63145331e6be70cdc
adityas-meta and others added 29 commits June 30, 2026 12:41
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2832

Pull Request resolved: pytorch#5913

Add observability for the two queue depths in the RES streaming pipeline. (1) `weights_to_stream_queue_` (MPSC stream queue in fbgemm) emits `stream_mpsc_depth` after every dequeue. (2) `tensor_to_dedup_queues_[i]` (per-shard dedup queues in training_ps) emit `dedup_depth.shard_{i}` per shard plus a `dedup_depth.total` rollup, bumped at the top of `co_stream_tensors`. Backpressure today is invisible until a trainer crashes; these signals let oncall see saturation trends and per-shard hot-spots in advance.

Both surfaces use OBC via `TrainingPsOdsLogger::bumpKeyGauge` (emits P50+P99 — the right stats for depth percentiles), on the `raw_embedding_streaming` category. The handler already owned `ods_logger_`; the fbgemm-side `RawEmbeddingStreamer` reuses the `TrainingPsOdsLogger` member added by the parent diff D107811590 (constructed only when streaming is enabled). No fb303: OBC reaches ODS via the host-level agent without per-process export config. (An earlier revision used an fb303 `ResGauges.h` helper for the fbgemm gauge; removed per review feedback on the sibling silent-failure diff.)

Stacked on D107811590 (silent-failure ODS counters), which introduces the shared OBC logger member on `RawEmbeddingStreamer`. Both feed the master observability initiative T269497764.

Design note: per-shard cardinality is N OBC keys x 3 stats; `num_dedup_threads_` is configurable but typically small (4-8); `.total` is for at-a-glance, per-shard for hot-spot debug.

Is it caused by our diff? No. Two cases, both exonerating:

  1. If the ffmpeg error is in an FBGEMM OSS-CI job (all the FAILED ones): our fbgemm code is entirely behind #ifdef FBGEMM_FBCODE, which the
  OSS build never defines — so OSS compiles a byte-identical translation unit to pre-diff. An ffmpeg build/link issue there is the OSS
  environment (the established FBGEMM OSS-CI flake), not our code.
  2. If it's in a citadel/ci_workflows WARNING (videorecs_ranking, silvertorch, etc.): those are tagged WARNING_PREEXISTING_ISSUE — CI itself
  confirms they failed on the base revision before your change. Pre-existing = not us.

  Plus the clinchers: our change is OBC counter logging (bumpKey/bumpKeyGauge) in training_ps + the RES streamer — zero source/build/runtime
  path to ffmpeg (a third-party video codec lib). And the MVAI e2e ran green on this exact binary.

  So: the FFMPEG signal is not caused by your diff — it's either OSS-CI infra or a pre-existing base-rev warning.

Reviewed By: FriedCosey

Differential Revision: D108312495

fbshipit-source-id: 81381e7ca1609c98ca13cb530d60924981335c3a
)

Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2878

Pull Request resolved: pytorch#5966

The pytorch/test-infra build matrix (generate_binary_build_matrix.yml@main) now emits
CUDA 13.2 (cu132) coordinates. FBGEMM does not support 13.2 yet, so every cu132 job in
build_wheels_linux_x86 fails in the test-infra validate step with
"ERROR: Unknown distribution ${DISTRIBUTION}".

Filter cu132 out in the filter-matrix step by adding a `gpu_arch_version:13.2` filter
group, until 13.2 is supported. Mirrors the intent of D104335566.

Reviewed By: q10

Differential Revision: D110247364

fbshipit-source-id: 31e636ee6a36408f4102c225c4a0aa043d2df582
…smatch (pytorch#5963)

Summary:
Pull Request resolved: pytorch#5963

X-link: https://github.com/facebookresearch/FBGEMM/pull/2875

Add `__attribute__((visibility("hidden")))` to the `KernelLauncher` struct in `kernel_launcher.cuh`. This prevents the dynamic linker from exporting `launch_kernel` weak symbols, forcing each `.so` to use its own copy.

**Root cause:** On CUDA 12.8 with sm_90+, the dynamic linker resolves `index_select_ops.so`'s `launch_kernel` to the main binary's copy (from `embedding_ops_training_gpu` via `embedding_ops`). This causes `__cudaPushCallConfiguration` (in the main binary) and `__cudaPopCallConfiguration` (in `index_select_ops.so`) to use different statically-linked CUDA runtime copies with separate TLS stacks. Pop finds an empty stack, returns non-zero, and `find_long_segments` kernel is silently skipped — causing zero gradients for runs >= 32 in `batch_index_select_dim0` backward.

**Why this fix works:** `visibility("hidden")` prevents `launch_kernel` from being exported, so the dynamic linker cannot resolve it cross-module. Each `.so` uses its own copy — push and pop stay in the same TLS. This is structurally guaranteed regardless of the dependency tree or compiler inlining behavior.

**Why not the other fixes:**
- D103575890 (pre-build args at call site): Targeted fix for `find_long_segments` only — other kernels with the same pattern would need similar fixes.
- D103575878 (`launch_kernel_with_transform`): Works empirically but depends on compiler inlining `launch_kernel<..., PackedTensorAccessor32...>` — not structurally guaranteed.

Reviewed By: q10

Differential Revision: D104175840

fbshipit-source-id: 9d3aa18dcaf794b6659ab6d4af913ac18cf8aa5f
…pytorch#5968)

Summary:
Pull Request resolved: pytorch#5968

X-link: https://github.com/facebookresearch/FBGEMM/pull/2880

Fixes 100+ `*-type-checking` failures blamed on D109465368 (the backout of D107684317).

Reviewed By: gchalump

Differential Revision: D109954014

fbshipit-source-id: 9ffe31cde174edd05052040d22c78ef4b3c4035a
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2873

Pull Request resolved: pytorch#5961

Register aggregate ops for DRAM_SSD composite backend metrics: ssd_num_lookups, ssd_num_hits, ssd_num_writes, dram_num_ids, and ssd_hit_rate.

Reviewed By: EddyLXJ

Differential Revision: D110075583

fbshipit-source-id: dfe2f789592ef2173bed5ebc4e59080c91fef55f
Summary:
Pull Request resolved: pytorch#5965

## Context
This stack of diffs improves FBGEMM embedding table storage by integrating KVZCH SSD caching into `EmbeddingRocksDB`, enabling a hybrid **DRAM + SSD** approach. It adds support for metadata-only operations, cache lifecycle management, and better stats/performance—aiming to make large embedding table storage/retrieval more efficient.

## This Diff
**[KVZCH][SSD] Return zero optimizer state for DRAM_SSD embedding cache**

### Summary
This diff ensures **zero optimizer state is returned specifically for `DRAM_SSD` when running in _embedding cache mode_**.

### Key Changes
- Adds `embedding_cache_mode` to `SSDTableBatchedEmbeddingBags` (in `kv_backend_test.py`) to toggle embedding cache behavior.
- Introduces `_generate_zero_optimizer_states` (in `training.py`) to construct zero-filled optimizer-state tensors based on `optimizer.state_size_table`.

### Why this is correct / when it applies
- **Only applies to embedding cache mode for `DRAM_SSD`.**
- In embedding cache mode, the **embedding values are not backpropagated**, so their **optimizer state is always 0**.

Reviewed By: EddyLXJ

Differential Revision: D110243528

fbshipit-source-id: 8ed689b3b1facdf87942d7c3d3cd617505187499
…ytorch#5954)

Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2884

Fixes a use-after-free window between the eviction loop and inplace update
/ inference reads in the DramKV embedding cache.

## The bug
- Eviction **freed blocks first** (mempool lock), then **erased keys from the
  shard map** (`wlock`) — leaving a window where a freed block was still
  reachable via the map.
- Inplace update **dropped its shard `rlock` before writing** to weight blocks,
  so the evict loop could free a block mid-write.

## The fix (two coupled changes)
1. **Erase keys before freeing blocks**: pick victims (mempool lock) → erase
   keys (`wlock`, excludes readers) → free blocks once unreachable.
2. **Hold the shard `rlock` across the inplace hit writes** (lookup + write).

While a reader holds the `rlock`, it blocks the evict loop's `wlock`, so the
key cannot be erased and the block cannot be freed underneath an in-flight
write.

Pull Request resolved: pytorch#5954

Reviewed By: henrylhtsang

Differential Revision: D110231575

Pulled By: q10

fbshipit-source-id: c440dd88fbc2f20e1a2e41f2524c5552127c125a
Summary:
Pull Request resolved: pytorch#5973

X-link: https://github.com/facebookresearch/FBGEMM/pull/2886

HIP enforces a hard limit of 2^32 total threads per launch (unlike CUDA, which
silently wraps; see ROCm/hip#2253).
`jagged_softmax_forward_cuda` launches `jagged_softmax_kernel` with grid
`dim3(D, min(B, kMaxBlockYDim=65535), 1)` and block 128. Total launch threads
= `D * min(B, 65535) * 128`; for the canonical embedding shape
`D >= 513, B = 65535` that is 4,295,032,320 > 2^32, which trips
`KernelLauncher::checkThreadCountNotExceeded` on ROCm.

The kernel already grid-strides over both `b` (line 36) and `d` (line 45),
so capping the host-side x-dim grid is correctness-preserving. y is already
clamped to `kMaxBlockYDim = 65535`. This change applies the same one-line
`#ifdef USE_ROCM` cap pattern as parent fixes D104903707 / D104937969 in
`sparse_ops/`.

Audit: /home/bensonma415/.llms/plans/jagged_and_more_rocm_grid_overflow_audit.plan.md

Reviewed By: henrylhtsang

Differential Revision: D105096879

fbshipit-source-id: a673834245cdc1fc6bcbc9098ffb4b77c3f1c79d
Summary:
Pull Request resolved: pytorch#5974

X-link: https://github.com/facebookresearch/FBGEMM/pull/2887

HIP enforces a hard limit of 2^32 total threads per launch (unlike CUDA, which
silently wraps; see ROCm/hip#2253).
`jagged_softmax_backward_cuda` launches `jagged_softmax_backward_kernel`
with grid `dim3(D, min(B, kMaxBlockYDim=65535), 1)` and block 128. Total
launch threads = `D * min(B, 65535) * 128`; for `D >= 513, B = 65535`
that is 4,295,032,320 > 2^32 and trips
`KernelLauncher::checkThreadCountNotExceeded` on ROCm.

The kernel already grid-strides over both `b` (line 37) and `d` (line 46),
so capping the host-side x-dim grid is correctness-preserving. y is already
clamped to `kMaxBlockYDim = 65535`. Same one-line `#ifdef USE_ROCM` cap
pattern as parent fixes D104903707 / D104937969 in `sparse_ops/`.

Audit: /home/bensonma415/.llms/plans/jagged_and_more_rocm_grid_overflow_audit.plan.md

Reviewed By: henrylhtsang

Differential Revision: D105097144

fbshipit-source-id: a2cfd3d9721b638081a94f8def79ccd01615a1a3
…torch#5976)

Summary:
Pull Request resolved: pytorch#5976

X-link: https://github.com/facebookresearch/FBGEMM/pull/2888

PyTorch dropped CUDA 12.8 (cu128) starting with torch 2.12 -- 2.12 and 2.13 publish no cu128 wheels. Verified on download.pytorch.org (test channel): torch-2.13 has cu126=108 and cu130=108 wheels but cu128=0 (torch-2.12 cu128=0; cu128's last version was 2.11 with 42 wheels).

FBGEMM's release workflows still offered "12.8.1" as a CUDA option, a leftover from the CUDA-12.8 era. Building/publishing a cu128 FBGEMM wheel for the v1.8.0 release (torch 2.13) is impossible -- there is no torch cu128 to build against -- so the option can only ever fail. Remove it so the release matrix reflects PyTorch and nobody can trigger a dead cu128 build.

Changes (options + genai build/test matrices):
  - fbgemm_gpu_release_cuda.yml:  [ "12.6.3", "12.8.1", "13.0.2" ] -> [ "12.6.3", "13.0.2" ]
  - fbgemm_gpu_release_genai.yml: same drop in the cuda-version-publish option and both build/test cuda-version matrices.

Default stays 13.0.2. Net release CUDA set: cu126 (legacy) + cu130 (stable) -- matching torch 2.13's PyPI matrix.

PyTorch source (authoritative):
  - "Introducing CUDA 13.2 and Deprecating CUDA 12.8 (Release 2.12)", Andrey Talman (PyTorch Dev Infra), 2026-04-01:
    https://fb.workplace.com/groups/671272153461300/permalink/1976674139587755/
    "CUDA 12.8 is being deprecated and removed from CI/CD pipelines and binary build matrices ... CUDA 13.0 remains the stable variant published to PyPI ... CUDA 12.6 remains available as the legacy build."
  - RFC: pytorch/pytorch#178665 (CUDA support matrix for Release 2.12)
  - dev-discuss: https://dev-discuss.pytorch.org/t/introducing-cuda-13-2-and-deprecating-cuda-12-8-release-2-12/3337
  - Policy: pytorch/pytorch RELEASE.md#pytorch-cuda-support-matrix (legacy / stable / experimental tiers)

PyTorch's torch 2.13 CUDA tiers: 12.6 legacy, 13.0 stable, 13.2 experimental. FBGEMM v1.8.0 ships 12.6 + 13.0; 13.2 (experimental) is deferred until the FBGEMM cu132 build is nightly-green (tracked separately; cu132 was filtered in D110247364).

___

Differential Revision: D110492570

fbshipit-source-id: 03d2bccee832f22ad0e7a0d3b028afdfa72a7e51
Summary:
Pull Request resolved: pytorch#5849

X-link: https://github.com/facebookresearch/FBGEMM/pull/2767

## What
Parallelizes the per-table loop in the CPU TBE forward kernel
(IntNBitTableBatchedEmbeddingBagsCodegen::forward) across tables. The
per-table loop is embarrassingly parallel — each table reads its own weight
slice and writes a disjoint slice of the output — so fanning tables out across
threads gives near-linear speedup on table-heavy inference models.

Two env knobs control it:
- FBGEMM_TBE_MAX_NUM_THREADS (default 1): the thread-count cap. =1 keeps the unchanged
sequential behavior; =N>1 distributes tables over up to N OpenMP threads with
dynamic scheduling (good load balancing when table sizes are skewed).
- FBGEMM_TBE_MIN_TABLES_PER_THREAD (default 16): work-granularity for the per-call thread
count (see the work-size guard below).

## Work-size guard (skip threading for small few-table lookups)

A uniform thread count *regresses* small few-table TBE lookups: splitting a
~7–13 table loop across threads adds OpenMP fork/join + cross-call contention
with no parallelism benefit. (Measured on dpa: its three small remote_ro_event
lookups slowed down under threading, while its single 358-table remote_ro
lookup sped up.) choose_table_threads() therefore both GATES and GRADES the
per-call thread count on the work size via a single formula:

    N = clamp(work_units / G, 1, cap)   (cap=FBGEMM_TBE_MAX_NUM_THREADS, G=FBGEMM_TBE_MIN_TABLES_PER_THREAD)

A call with fewer than 2*G tables runs serial; larger calls scale one thread per
G tables, up to the cap. With the default G=16 the threading onset is 32 tables,
matching the gate validated in ICE A/B testing. Set FBGEMM_TBE_MIN_TABLES_PER_THREAD=1 to
restore unconditional threading.

The pure decision (choose_table_threads_impl) is split into
fbgemm_gpu/utils/embedding_cpu_threading.h so it can be unit tested without
touching the process environment.

## Default behavior is unchanged

When FBGEMM_TBE_MAX_NUM_THREADS<=1 (the default) choose_table_threads short-circuits to 1
before FBGEMM_TBE_MIN_TABLES_PER_THREAD is even read, and the helper takes an early return
that runs the loop body sequentially with no try/catch wrapper and no
thread-local-state guard. So the default path is functionally identical to the
pre-change code: same iteration order, the DEVICE-placement TORCH_CHECK in its
original per-table position, same error semantics, and the same generated machine
code for the body. The only always-on changes are mechanical and
behavior-preserving (loop-local `weights` pointer, int64 loop index).

## Design

- Raw OpenMP (#pragma omp parallel + omp for, schedule(dynamic)) rather than
  at::parallel_for, so TBE gets its own thread count independent of the global
  intra-op pool / OMP_NUM_THREADS (predictors run with OMP_NUM_THREADS=1).
- The cap and granularity are read once from the env vars and cached (thread-safe
  static init); the per-call thread count from choose_table_threads() is clamped
  to the number of tables.

## Correctness

- Removed the function-scoped `weights_acc` pointer, which every iteration
  overwrote — a data race once the loop is parallel. Replaced with a loop-local
  pointer (identical pointer value). Every other variable in the loop body is
  already loop-local, and each table writes a disjoint output slice
  (output_acc + D_start), so results are bitwise-identical to the sequential
  path.
- The per-table DEVICE-placement TORCH_CHECK stays in its original position. In
  the threaded path it — like any other throw from the loop body (kernel
  errors, at::arange checks) — is captured (first one wins) and rethrown after
  the join, so no exception escapes the OpenMP region.
- Worker threads restore the caller's at::ThreadLocalState (dispatch keys,
  grad/inference mode, autocast, ...), so ATen calls inside the loop (e.g.
  at::arange in the nobag path) run with the correct thread-local context.

## Verification

- Builds clean (mode/opt); confirmed OpenMP is actually enabled for this
  target by inspecting the compiled object — gen_*_codegen_cpu.cpp.pic.o
  references __kmpc_fork_call / omp_get_num_threads (the pragma is NOT a no-op).
- New unit tests in this diff:
  - test/tbe:embedding_cpu_threading_test — C++ tests of the pure
    choose_table_threads_impl decision: cap=1 (no env var) is ALWAYS serial
    regardless of work/granularity; onset at 2*G=32; grading scales to the cap;
    G=1 threads every call.
  - test/tbe:nbit_forward_threading — runs the real CPU int-nbit forward in
    separate processes under FBGEMM_TBE_MAX_NUM_THREADS in {unset, 1, 2, 4} (and
    FBGEMM_TBE_MIN_TABLES_PER_THREAD) and asserts the outputs are BITWISE identical
    (torch.equal) — i.e. table-threading does not change the result.
- nbit_forward CPU unit tests pass with FBGEMM_TBE_MAX_NUM_THREADS=4, including
  test_nbit_forward_cpu_with_table_sharing (non-monotonic weights_offsets).

## Benchmark (INT4, B=512, D=128, E=100K, L=20, FP16/SUM, iters=100, 3-run avg)

| Tables | Threads | Avg us | BW (GB/s) | Speedup | Efficiency |
| 8      | 1       |  1,436 |      4.38 |     --- |        --- |
| 8      | 2       |  1,042 |      6.03 |   1.38x |        69% |
| 8      | 4       |    844 |      7.46 |   1.70x |        43% |
| 8      | 8       |    773 |      8.14 |   1.86x |        23% |
| 32     | 1       |  4,797 |      5.25 |     --- |        --- |
| 32     | 2       |  2,830 |      8.90 |   1.69x |        85% |
| 32     | 4       |  2,003 |     12.60 |   2.40x |        60% |
| 32     | 8       |  1,633 |     15.49 |   2.94x |        37% |
| 64     | 1       | 10,132 |      4.97 |     --- |        --- |
| 64     | 2       |  6,767 |      7.44 |   1.50x |        75% |
| 64     | 4       |  4,864 |     10.35 |   2.08x |        52% |
| 64     | 8       |  4,033 |     12.50 |   2.51x |        31% |

2 threads is the efficiency sweet spot (69-85%); efficiency falls off at higher
counts due to fixed fork/join overhead per call. This matches the production
recommendation FBGEMM_TBE_MAX_NUM_THREADS=2 (+7.3% QPS measured in ICE). Microbenchmark
kernel speedups overpredict end-to-end QPS (Amdahl: CPU TBE is a small fraction
of total inference latency).

Reviewed By: helloguo, q10

Differential Revision: D102867249

fbshipit-source-id: d98151ff7b9bd6af34636dee227b920775efa58e
Summary:
Unblock the OSS CI docs/lint/ci jobs, which all failed in conda env setup.

1) setup_miniconda: drop --update-deps from the base conda update.

The docs (build-docs), lint (run-lint), and ci_cpu/ci_cuda jobs -- everything that calls setup_miniconda -- failed because the base conda destroyed itself during env setup. Every subsequent `conda` call died with:

  ModuleNotFoundError: No module named 'conda'

surfacing as `conda clean` failing (lint) and as broken conda post-link scripts when installing graphviz/doxygen (docs: gdk-pixbuf/gtk3 LinkError). Root cause:

  conda update -n base -c conda-forge --override-channels --update-deps -y conda

conda-forge periodically publishes a base `conda` whose --update-deps solve bumps base Python to a version conda itself is not built against, orphaning conda's own site-packages. This is independent of the job's matrix Python version -- it happens in the base env before the matrix Python is used. (An earlier attempt to pin the docs/lint matrices to 3.13 was a no-op and was reverted.) Dropping --update-deps leaves the base interpreter intact while still updating conda. The gtk3/gdk-pixbuf docs failure was a downstream symptom of the same broken base conda (its post-link scripts invoke base conda), so this one change fixes both.

2) Drop the dead JSS hyperlink from the Xor128() doc comment in QuantUtilsAvx2.h.

With base conda healthy, the docs build completes and only the Sphinx linkcheck fails, on the Xor128() reference which linked JSS's deprecated URL scheme (https://www.jstatsoft.org/v08/i14/paper) that the runner cannot reach. Remove the hyperlink and keep a plain-text citation for Marsaglia's "Xorshift RNGs" (JSS 8(14), 2003; doi:10.18637/jss.v008.i14). No live link means nothing for linkcheck to probe, so no linkcheck_ignore entry is needed.

Note: the release wheel builds are unaffected -- they use test-infra's conda setup (setup-miniconda: false), not this function.

Reviewed By: q10

Differential Revision: D110251102

fbshipit-source-id: edd2782887bbd5225bba3c687ec39241fb28a4f0
…ch#5978)

Summary:
Pull Request resolved: pytorch#5978

Addresses AMD benchmarking feedback on the group_index_select_2d benchmark:

1. The reported per-kernel numbers are derived from the exported Kineto trace.
   group_index_select_2d_bench wrapped the entire run (warm-ups + measured) in a
   bare profile(), so every iteration -- warm-ups included -- landed in the trace
   and inflated the sample count and standard deviation. Switch to a
   schedule-based profiling pass (schedule(wait=0, warmup=W, active=A) with
   prof.step()) that records ONLY the measured iterations, mirroring the existing
   index_select_dim0 pattern in the same file.
2. Make warm-up / measured iteration counts configurable via --num-warmups and
   --iters, and raise the default warm-ups from 10 to 50 for more reliable
   small-config measurements.

Reviewed By: cthi

Differential Revision: D109389728

fbshipit-source-id: 52cd7c39ff14310b8c6975aa0a674a0705b86e25
Summary:
X-link: https://github.com/facebookresearch/FBGEMM/pull/2892

HIP enforces a hard limit of 2^32 total threads per launch (unlike CUDA, which
silently wraps; see ROCm/hip#2253).
`jagged_acc_weights_and_counts_cu` launches `accumulate_weights_and_counts_kernel`
with grid `ceil(total_elements / kMaxThreads=1024)` and block 1024. Total
launch threads ~= `total_elements`; for `total_elements > 2^32` the launch
trips `KernelLauncher::checkThreadCountNotExceeded` on ROCm.

The kernel already grid-strides over `i` at lines 300-301
(`for (int i = bid * kMaxThreads + tid; i < total_elements;
i += kMaxThreads * gridDim.x)`), so capping the host-side grid
unconditionally on ROCm is correctness-preserving. Same one-line
`#ifdef USE_ROCM` cap pattern as parent fixes D104903707 / D104937969 in
`sparse_ops/`.

Audit: /home/bensonma415/.llms/plans/jagged_and_more_rocm_grid_overflow_audit.plan.md

Reviewed By: cthi

Differential Revision: D105097581

fbshipit-source-id: 3eef8c8372163cfb8c9f320889971ada6e8c661e
@avbokovoy
avbokovoy force-pushed the abokovoi/group-index-sort-and-cache-opt branch from 5844b46 to 475c44d Compare July 6, 2026 16:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.