Skip to content

QR matrix decomposition and solver for Linalg - #402

Draft
vaijira wants to merge 108 commits into
tracel-ai:mainfrom
vaijira:linalg
Draft

QR matrix decomposition and solver for Linalg#402
vaijira wants to merge 108 commits into
tracel-ai:mainfrom
vaijira:linalg

Conversation

@vaijira

@vaijira vaijira commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Implementation of QR algorithm based on blocked accelerated householder taking advantage of matmul. There is a solver too.
When calling matmul we are not using most optimized strategies because when using tf32 precision decreases. But with Strategy::auto in matmuls the gain is around 40%.

Validate your PR with burn.

It is important that you make sure that you don't introduce any bugs in burn.

Instructions

  • Create a new branch or fork of the burn repo
  • Update the main Cargo.toml with this PR hash.
  • Fix any broken tests or compilation errors in burn.
  •  Submit a PR in burn with your fixes and link it here.

vaijira and others added 30 commits May 1, 2026 12:20
Initial implementation for QR decomposition by givens rotation and
by blocked accelerated householder reflector methods.
* Interpolate migration from burn

Co-authored-by: Copilot <copilot@github.com>

* Added interpolate benchmarks

Co-authored-by: Copilot <copilot@github.com>

* Fixed interpolate problems

* Fixed interpolate lib lint

* fix correctness

* export interpolate category

* adjust benchmark problems

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: louisfd <louisfd94@gmail.com>
* add TiledLayout in cubek-test-utils

* cargo fmt --all

* remove dead comments

* change test name and add simple implementation for layout

* rename test name

* refactor tests

* use RowMajorLayout instead of manually calculating indexes

* cargo fmt --all
* Update cubecl rev

* Update rev again

* Update version to 0.2.0

* Use cubecl commit until published

* Use published cubecl
* Interpolate migration from burn

Co-authored-by: Copilot <copilot@github.com>

* Added interpolate benchmarks

Co-authored-by: Copilot <copilot@github.com>

* Fixed interpolate problems

* Fixed interpolate lib lint

* fix correctness

* export interpolate category

* adjust benchmark problems

* add interpolation modes

Co-authored-by: Copilot <copilot@github.com>

* fix nearest backward algo

* Added benchmarks

* refactor interpolate benchmarks

* fix dtype clone clippy

* fix pr comments

* fix clippy format

* added main cublecl hash

* update cubecl hash

---------

Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: louisfd <louisfd94@gmail.com>
* Bump version to 0.3.0-pre.1

* Update cubecl rev
* migrate pool

* add forward tests

* added backward pool

* add lib wrappers for pool

* add backward references and tests

* edit cubek toml

* edit struct naming

* fix pr comments

* fix clippy lint
louisfd and others added 24 commits June 28, 2026 09:30
* Add support for f64 tracel-ai#198

* Remove if/else statements with function call.

* Add From methods for HostDataType from StorageType
* tile size refactor and interpolate bicubic fix

* fix cpu shared memory

* resample readme

* add comments

* fix clippy
…speedup)

nsys profiling at 2048x2048 showed 77% of GPU time in the panel
factorization: householder_kernel ran on a single thread (serial O(rows)
loop, 58us/launch) and apply_householder_kernel on at most 32 threads
(one per panel column, 82us/launch).

Changes to components/baht_tsqr.rs:
- householder_kernel: now uses a full cube with a shared-memory tree
  reduction for sigma and a parallel normalization. It also writes the
  reflector directly into v_buf column j (explicit 1 at the head),
  which removes copy_v_to_buf_kernel and the v_tmp buffer, saving one
  dispatch per column.
- apply_householder_kernel: one cube per remaining panel column, with a
  cube-wide tree-reduced dot product and parallel rank-1 update
  (82us -> ~3us per launch).
- build_t_tsqr_kernel: rows within each sequential column are now
  computed in parallel across the cube.

Matmul strategy notes (routines/baht_tsqr.rs):
- Strategy::Auto was ~30% faster but broke f32 correctness (0.2-16%
  errors): Auto resolves to SimpleCyclicCmma (tensor cores) and
  cubek-matmul's adjust_dtypes silently downgrades f32 stage/register
  types to tf32. There is no full-precision f32 tensor-core path.
  f64 is unaffected because no f64 CMMA exists and Auto falls back to
  SimpleUnit.
- f32 keeps full-precision unit strategies; strategy_tall upgraded from
  SimpleUnit to DoubleUnit (121ms vs 131ms at 2048x2048, tied at
  smaller sizes).

Correctness: 14/14 CUDA tests pass (f32 and f64, tolerance 2e-3), plus
QR reconstruction checks at 128, 512, 1024, 2048 square and 2048x512
in f32.

Benchmarks (CUDA f32, RTX 4070 Laptop, mean of 10 samples):
  128x128:    3.10ms  -> 1.55ms  (2.0x)
  512x512:    26.84ms -> 8.87ms  (3.0x)
  1024x1024:  94.55ms -> 28.8ms  (3.3x)
  2048x2048:  407.5ms -> ~121ms  (3.4x)
  2048x512:   152.1ms -> 26.9ms  (5.7x)

Gap to PyTorch/cuSOLVER f32 shrank from 5-44x to 2.5-9x. Remaining
bottleneck is the tall GEMMs (S = W^T*Q^T, Z = V*S) running on
full-precision unit matmuls at ~1-2 TFLOPS.

Also adds examples/profile_qr.rs, a standalone driver for profiling:
  cargo build --release -p cubek-linalg --example profile_qr --features cubecl/cuda
  nsys profile --stats=true target/release/examples/profile_qr [m] [n] [iters]
Review fixes ahead of the linalg PR:

- Normalize any input layout on-device: initialize() now runs cubecl's
  into_contiguous on a transposed view of A (tight row-major [n,m] of
  A^T == tight col-major [m,n] bytes), replacing the host round-trip
  (read_one + create_from_slice) that also silently produced wrong Q/R
  for row-major or pitch-padded inputs.
- Validate before launching: reject element-type mismatches between the
  generic entry points and the tensor dtype (new QRSetupError::TypeMismatch),
  and reject zero-column matrices (previously a divide-by-zero panic).
- Propagate errors instead of panicking: baht_tsqr::launch returns
  Result and internal matmul setup failures surface as QRSetupError::Matmul.
- Overflow-safe indexing: kernels compute flat offsets in usize
  (r_offset/v_offset params dropped; derived in-kernel from rows/col/j).
- Scratch buffers use client.empty instead of TensorHandle::zeros —
  every region is fully written before read, saving 8 fill dispatches
  per call; shared-memory tree reduction deduplicated into a
  tree_reduce_sum cube helper; dead is_col_major flag removed from
  back-substitution.
- Benchmark: col-major input (was factorizing byte-scrambled row-major
  data), reuse the stored client; profile_qr example gains a dtype arg
  (f32|f64) and a mean/median summary.
- Tests: reuse cubek-test-utils (TestInput builder, HostData comparator),
  panic on launch errors instead of swallowing them, add row-major input
  tests and 4 launch-guard tests (20 total, pass on CPU and CUDA);
  tests/suite/qr/tsqr.rs renamed to baht_tsqr.rs to match the routine.
- Deps: drop unused serde, half, pretty_assertions, benchmarks' rand;
  move num-traits to dev-dependencies.

CUDA f32 timings (RTX 4070 Laptop, mean of 10): 128²: 1.49ms (was 1.54),
512²: 8.16ms (was 8.73), 1024²: 22.3ms (was 29.9), 2048x512: 20.5ms
(was 27.8) — the removed host round-trip and fill dispatches account
for the 25%+ gains at larger sizes.
@vaijira vaijira changed the title Linalg QR and solver for Linalg Jul 11, 2026
@vaijira vaijira changed the title QR and solver for Linalg QR matrix decomposition and solver for Linalg Jul 13, 2026
vaijira added 4 commits July 21, 2026 18:44
Profiling baht_tsqr with nsys showed the Q^T update kernel at 2.7% of GPU
kernel time on 1024x1024 and 6.3% on 2048x512. Its two operands (Q^T and Z)
are both tight col-major [rows, rows] buffers indexed with the identical flat
offset, so it was a plain contiguous elementwise add running scalar under a
2D launch whose second dimension bought nothing.

Replace it with add_assign_kernel: Vector<F, N> operands and a 1D launch,
with the line size chosen via io_optimized_vector_sizes plus a divisibility
check (lands on 4). The kernel itself goes 17,517 -> 7,907 ns at 1024x1024
and 82,284 -> 48,689 ns at 2048x512. Total GPU kernel time drops 1.5% at
1024x1024 and 2.5% at 2048x512.

update_trailing_r is deliberately left scalar and 2D. Its operands are
transposed relative to each other (R col-major, Z row-major), and declaring
Z col-major to fix that moves 124 matmul instances from acc_size_4 to
acc_size_1 -- exactly the z_r matmul count -- because it de-vectorizes the
matmul's output write. Measured net GPU time was worse than leaving it
alone at both shapes, so the reasoning is recorded in a comment instead.

Verified on CUDA and wgpu (RTX 4070): 20/20 tests pass on both, f32 and f64.
build_t_tsqr_kernel costs a fixed ~22.9 us per tile regardless of matrix
shape (stddev 20 ns across 128 instances at 1024x1024, identical timing at
2048x512) for what is only ~5.5k FMAs. It builds T column by column, and
each of the `tile` serialized steps reads back columns written by earlier
steps, so building T directly in global memory put a global round trip
inside every step of the dependency chain.

Stage T and the Gram matrix in shared memory instead and write T out once
at the end. The working set is tile * tile elements (4 KiB at f32/tile=32),
so it fits easily. `tile` becomes a comptime parameter to size the shared
slices. Kernel goes 22,896 -> 20,595 ns, from 3.5% to 3.2% of GPU kernel
time at 1024x1024 -- roughly 0.35% end to end, which is below wall-clock
noise, so the nsys kernel-time figure is the measurement of record.
Profiling showed the matmuls at ~70% of GPU time, and within them only the
trailing-update GEMMs matter. Forcing strategy_tall to Strategy::Auto takes
1024x1024 from 23.5ms to 12.0ms, while forcing the Gram or W matmul to Auto
changes nothing end to end. Auto resolves to a tensor-core path that rounds
f32 inputs to tf32, which is why it was ruled out wholesale before.

Expose it as a knob instead of leaving the speed on the table:

- BahtTsqrStrategy gains `allow_tf32` (off by default), mapped into the
  blueprint and applied to strategy_tall only. Gram and W stay
  full-precision unconditionally since they cost nothing either way.
- New `qr_with_strategy()` entry point; `qr()` delegates with defaults, so
  the existing API and its accuracy are unchanged.
- profile_qr takes an optional 5th arg `tf32` so both paths are profilable.

The flag is gated on `supports_type(tf32)`. Without a tensor-core path
Strategy::Auto resolves to something *slower* than the full-precision unit
routine -- measured on wgpu, 24.1ms -> 30.5ms at 1024x1024 -- so honouring
the opt-in there would cost accuracy and speed. Post-gate wgpu measures
21.30/21.56/21.35ms with the flag against 21.30/21.24ms without: a genuine
no-op. Also a no-op for f64 (112.7 vs 112.4ms), which has no tf32 path.

CUDA f32 medians (RTX 4070 Laptop), full -> tf32:
  128x128:     1.34ms ->  1.26ms  (1.06x)
  512x512:     7.63ms ->  5.31ms  (1.44x)
  1024x1024:  22.84ms -> 11.90ms  (1.92x)
  2048x512:   22.01ms -> 11.40ms  (1.93x)
  2048x2048:  80.22ms -> 48.68ms  (1.65x)

The gain scales with size -- small problems are dominated by panel dispatch
overhead, not matmul -- so the flag is worth enabling for large
factorizations and roughly pointless below ~256.

Gap to PyTorch/cuSOLVER f32 on the same GPU narrows from 2.2-6.8x to
2.1-4.1x. (cuSOLVER uses tensor cores for f32 too, so the tf32 column is
the closer like-for-like comparison.)

Tests: adds test_tf32_opt_in (f32 and f64) driving qr_with_strategy at
157x157, asserted at 5e-2 -- the tolerance matching the documented ~1e-2
accuracy rather than pretending it is f32-accurate. 22/22 pass on CUDA and
wgpu. Also fixes two unnecessary-cast clippy warnings introduced by the
earlier vec_split helper.
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.