Skip to content

Retry tiled LLM.int8 outlier detection with an empty-mask fast path #15

Description

@heiheiha798

Salvage context

This is the single permitted salvage attempt for issue #14, following its B300 no-go result. It starts independently from the latest upstream main; the source experiment branch is evidence only and must not be continued, rebased, or stacked.

The source experiment replaced eager O(rows * K) LLM.int8 outlier discovery with a private tiled FP16 CUDA detector that writes an O(K) int32 column mask. On B300, the detector's core mechanism was exact and fast whenever outliers existed:

  • 320 baseline/candidate comparisons were exact for quantized rows, row statistics, ordered CUDA int64 outlier columns, and unchanged inputs.
  • All 80 one/five/sparse/dense direct cells favored the detector by at least 23.6%; their pattern-median speedups were 1.46x, 1.47x, 1.46x, and 1.43x.
  • Threshold-enabled Linear8bitLt improved 1.21x for decode, 1.17x for a batched case, and 1.09x for prefill; a resident decode stack improved 1.06x.
  • Prefill peak allocation fell from about 67.1 MB to 16.8 MB.

The complete gate failed for one localized reason: the candidate always called dynamic argwhere(mask), even when the mask was empty. Every direct regression over 3% was a no-outlier cell (15 of 20); the no-outlier median was 0.925x and the worst cell was 0.802x. The resident prefill stack was also flat at 0.9965x.

Bounded hypothesis

Keep the measured detector unchanged at 256 columns per CTA and a fixed 32-row tile, but have that same kernel also produce one O(1) device found flag:

  1. Initialize the int32[K] mask and one device int32 found scalar to zero.
  2. When a detector thread finds an outlier, set its column flag and set found to one with race-free, idempotent device operations.
  3. After the detector, evaluate only the O(1) found scalar in Python.
  4. If false, construct the existing empty CUDA int64 tensor without calling argwhere.
  5. If true, call the existing argwhere(mask).view(-1) path.

This deliberately reintroduces one host-visible scalar synchronization, but it does not restore the eager O(rows * K) abs, boolean mask, or global reduction. The experiment must determine whether avoiding empty argwhere fixes no-outlier regressions without erasing the measured nonempty and public-path gains.

Exact scope

  • Use the same private FP16 tiled detector and O(K) mask design from the source experiment.
  • Add only the O(1) found output and empty-mask branch.
  • Keep row tile 32 and 256 columns per CTA fixed. Do not retry row tiles, add architecture/shape/data thresholds, or introduce a dispatch table.
  • Use an idempotent atomic or equally local race-free aggregation inside the same kernel. Do not add a separate reduction kernel.
  • Keep threshold == 0, kInt8VectorQuant, quantization math, row statistics, multi-row output zeroing, mixed matmul, schemas, autograd, default backend, and public API unchanged.
  • Keep the current upstream FP16 scope. Do not copy or stack the unmerged BF16 work in upstream PR Add BF16 support for int8_vectorwise_quant / LLM.int8 activation quant bitsandbytes-foundation/bitsandbytes#1985.
  • Preserve HIP/ROCm source and build compatibility; B300 supplies the performance decision, not permission to break shared sources.

Expected production surface remains limited to the existing CUDA backend integration points: csrc/kernels.cu / .cuh, csrc/ops.cu / .cuh, csrc/pythonInterface.cpp, bitsandbytes/backends/cuda/ops.py, and focused tests.

No fusion, public schema, wider INT8 redesign, new architecture, dtype expansion, general fallback framework, or second bounded fix is in scope.

Correctness gates

Build isolated baseline and candidate variants from the same latest upstream commit. Require exact torch.equal results for quantized rows, row statistics, and ordered outlier columns, including the empty CUDA int64 dtype/device/rank contract. Inputs must remain raw-bit unchanged.

Repeat the source matrix, including:

  • rows around the 32-row tile through 2048;
  • K around 256-column boundaries plus 3584, 4096, 8192, and 14336;
  • thresholds 0.1, 2.0, 3.0, and 6.0;
  • no, one, repeated-column, five, sparse, and dense outliers;
  • exact positive/negative threshold values, immediate-below values, first/last rows and columns, NaN, and Inf;
  • actual candidate public-op provenance and repeated launches.

threshold == 0 must still return outlier_cols is None. Threshold casting must match current A.abs() >= threshold semantics. Run the focused vectorwise/COO tests, threshold-enabled Linear8bitLt tests, applicable fullgraph/dynamic-output compile tests, supported CUDA builds, shared-source compatibility checks, and full pre-commit.

B300 validation and unchanged acceptance gate

Use one B300 allocation and separately extracted baseline/candidate builds with identical software and the official CUDA 13 x64-compatible target input 75;80;86;89;90;100;120. Assert the exact loaded libraries and distinct native entry points. Interleave at least 30 host-wall samples per cell after at least 200 warmups, synchronizing after calls so dynamic-output cost is included.

Measure the original direct matrix over rows {1, 8, 64, 512, 2048}, K {3584, 4096, 8192, 14336}, and no/one/five/1%-sparse/dense patterns. Keep no-outlier, dense, and repeated-column cases as veto cells: the scalar branch can add host latency, and dense inputs can contend on the shared found flag.

Profile decode and prefill to show that the candidate still removes the O(rows * K) eager intermediates and global reduction. Report the scalar synchronization honestly, along with launches, allocation, bandwidth, occupancy, and atomic behavior. The source detector used more temporary memory than baseline for decode because int32[K] exceeded the tiny rows=1 eager intermediates; do not hide or redefine that tradeoff.

Required public evidence remains:

  • threshold-enabled Linear8bitLt for decode and batched/prefill;
  • the same reproducible resident synthetic stack for decode and prefill, with setup and weight quantization excluded.

Proceed to a draft PR only if every correctness/build gate passes and all original performance gates hold:

  • at least four representative direct cells improve by at least 10%;
  • no primary direct cell regresses by more than 3%;
  • Linear8bitLt improves by at least 5% in decode and in at least one batched/prefill case, with no primary Linear cell regressing by more than 2%;
  • both resident-stack regimes improve by at least 3%, with no regime regressing by more than 2%;
  • profiling confirms the intended O(rows * K) work and temporary removal.

If the scalar synchronization erases the nonempty/public gains, dense flag contention causes a veto regression, either stack misses its gate, or any correctness/build check fails, record a final no-go. This salvage cycle receives no further retry or derivative.

Technical story and risks

The substantive result remains the tiled native detector that replaces several eager full-tensor passes and materially reduces prefill allocation. The found flag is a bounded completion of that kernel/system method, not a standalone micro-optimization.

Primary risks are explicit: the host-visible scalar check may consume the current 9.2% prefill-layer margin, atomics to one flag may hurt dense inputs, and decode still has low detector occupancy and a larger mask allocation than the eager baseline. The unchanged gates decide whether the completed design is broadly useful enough for upstream.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions