Skip to content

Add optional TBLIS CPU contraction provider - #1344

Merged
shinaoka merged 17 commits into
mainfrom
feature/issue-1332-tblis-cpu-internal
Jul 15, 2026
Merged

Add optional TBLIS CPU contraction provider#1344
shinaoka merged 17 commits into
mainfrom
feature/issue-1332-tblis-cpu-internal

Conversation

@Ryo-wtnb11

@Ryo-wtnb11 Ryo-wtnb11 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Closes #1332.

Summary

  • Adds an optional TBLIS CPU contraction provider for dense dot_general.
  • Keeps TBLIS additive: cpu-faer or cpu-blas remains the fallback and linalg provider, and default features are unchanged.
  • Separates deployment into cpu-tblis-runtime (cpu-tblis remains its compatibility alias) using tblis-ffi dynamic loading, and cpu-tblis-linked using a statically linked source build.
  • Stores the source/link glue as independently versioned t4a-tblis-src under third_party/, excluded from the tenferro workspace and consumed through the neutral tblis-src dependency alias.
  • Supports f32, f64, Complex32, and Complex64, including conjugated and accumulated paths, with fallback for unsupported shapes, layouts, devices, and grouped GEMM.
  • Pins TBLIS to eb719e718976572e0ab53975f4e0c799faeb35f2, uses bundled BLIS, and links the source-backed route statically by default.

Loader and package boundary

  • tblis-ffi 0.2.6 panics when runtime discovery fails. The runtime route temporarily catches only the one-time pre-FFI loader probe, does not replace the process-global panic hook, and marks the compatibility bridge as an explicit audit exception. The linked route does not use this bridge.
  • RESTGroup/tblis-rs#4 adds fallible try_dyload_lib() while retaining the existing panic wrapper. The tenferro exception can be removed after a release containing that API is adopted.
  • t4a-tblis-src carries the Apache-2.0 source-glue license and attribution plus the pinned native projects license files and redistribution notes.
  • No crates.io publication was performed. First publication of t4a-tblis-src requires separate explicit user approval. AGENTS.md now forbids automatic publication of any new package and requires metadata and package-content validation before every publish.

Verification

  • cargo test --workspace --release
  • cargo llvm-cov --workspace --release --json --output-path coverage.json
  • python3 scripts/check-coverage.py coverage.json — 159/159 checked files passed
  • cargo doc --workspace --no-deps
  • python3.11 scripts/check-docs-site.py — 13 workspace library crates and four guide dependency snippets passed
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo clippy --manifest-path ext/tropical/Cargo.toml --all-targets -- -D warnings
  • Feature-specific Clippy and focused tests for both cpu-tblis-runtime and cpu-tblis-linked
  • Standalone tests, doctests, Clippy, metadata inspection, cargo package --list, and cargo package for third_party/t4a-tblis-src
  • python3 scripts/repository-rules-review.py --base origin/main --head HEAD ... — pass, no findings
  • cargo fmt --all --check, standalone package formatting, and git diff --check

Benchmarks

Quick local release benchmarks. Threads were pinned with TBLIS_NUM_THREADS=1, RAYON_NUM_THREADS=1, OMP_NUM_THREADS=1, and for BLAS runs OPENBLAS_NUM_THREADS=1.

Command:

CARGO_TARGET_DIR=/private/tmp/tenferro-tblis-blas-bench \
OPENBLAS_FC=/opt/homebrew/bin/gfortran \
LIBRARY_PATH=/opt/homebrew/lib/gcc/current:/opt/homebrew/lib/gcc/15 \
DYLD_LIBRARY_PATH=/opt/homebrew/lib/gcc/current:/opt/homebrew/lib/gcc/15 \
TBLIS_NUM_THREADS=1 RAYON_NUM_THREADS=1 OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 \
cargo bench -p tenferro-cpu --no-default-features \
  --features cpu-faer,cpu-tblis-linked,blas-openblas \
  --bench tblis_dot_general_provider -- --quick

N is the square matrix size. n is the size of each tensor axis in the higher-rank cases. The rank-5 case uses fixed batch size 4.

Case What it measures Formula Inputs Contract dims Output
f64_matrix_square_gemm Plain real GEMM where BLAS/faer should already be strong. C[i,j] = sum_k A[i,k] B[k,j] A,B: f64[N,N] lhs [1], rhs [0] C: f64[N,N]
c64_matrix_square_gemm_lhs_conj Complex GEMM with conjugated lhs. C[i,j] = sum_k conj(A[i,k]) B[k,j] A,B: c64[N,N] lhs [1], rhs [0] C: c64[N,N]
f64_rank4_packed_contract_axes Higher-rank contraction that is close to GEMM because contracted axes are packed together. C[a,b,c,d] = sum_{x,y} A[a,b,x,y] B[x,y,c,d] A,B: f64[n,n,n,n] lhs [2,3], rhs [0,1] C: f64[n,n,n,n]
f64_rank4_mixed_contract_axes Higher-rank contraction where contracted axes are separated by free axes. GEMM-only paths often need axis movement/materialization here. C[a,b,c,d] = sum_{x,y} A[a,x,b,y] B[c,y,x,d] A,B: f64[n,n,n,n] lhs [1,3], rhs [2,1] C: f64[n,n,n,n]
f64_rank5_batched_mixed_contract_axes Same mixed-axis pattern repeated over an explicit batch axis. C[b,c,d,e,a] = sum_{x,y} A[a,b,x,c,y] B[a,d,y,x,e] A,B: f64[4,n,n,n,n] batch lhs/rhs [0]; contract lhs [2,4], rhs [3,2] C: f64[n,n,n,n,4]
f64_rank4_row_major_view_mixed_contract_axes Borrowed row-major positive-stride views, checking whether providers can avoid canonicalizing to owned column-major inputs first. C[a,b,c,d] = sum_{x,y} A[a,x,b,y] B[c,y,x,d] borrowed A,B: f64[n,n,n,n], row-major strides [n^3,n^2,n,1] lhs [1,3], rhs [2,1] C: f64[n,n,n,n]

Default/Small Sample

Case Parameter faer BLAS TBLIS
f64_matrix_square_gemm N=32 3.42 us 1.98 us 5.66 us
f64_matrix_square_gemm N=64 12.71 us 19.77 us 19.52 us
f64_matrix_square_gemm N=128 86.88 us 99.27 us 101.31 us
c64_matrix_square_gemm_lhs_conj N=32 6.38 us 12.10 us 10.92 us
c64_matrix_square_gemm_lhs_conj N=64 53.72 us 60.93 us 53.35 us
c64_matrix_square_gemm_lhs_conj N=128 385.69 us 406.12 us 333.33 us
f64_rank4_packed_contract_axes n=4 2.02 us 0.88 us 3.40 us
f64_rank4_packed_contract_axes n=8 12.82 us 20.60 us 19.44 us
f64_rank4_mixed_contract_axes n=4 3.37 us 4.05 us 3.72 us
f64_rank4_mixed_contract_axes n=8 16.79 us 29.64 us 19.08 us
f64_rank5_batched_mixed_contract_axes n=4, batch 4 5.67 us 6.42 us 10.38 us
f64_rank5_batched_mixed_contract_axes n=8, batch 4 62.11 us 108.55 us 74.58 us
f64_rank4_row_major_view_mixed_contract_axes n=4 3.49 us 5.49 us 3.88 us
f64_rank4_row_major_view_mixed_contract_axes n=8 17.90 us 42.66 us 20.13 us

Large Sample

Run with TENFERRO_TBLIS_BENCH_MATMUL_SIZES=256,512 TENFERRO_TBLIS_BENCH_HIGHER_RANK_NS=16.

Case Parameter faer BLAS TBLIS
f64_matrix_square_gemm N=256 617.38 us 748.55 us 645.64 us
f64_matrix_square_gemm N=512 5.04 ms 4.91 ms 4.78 ms
c64_matrix_square_gemm_lhs_conj N=256 2.93 ms 2.77 ms 2.39 ms
c64_matrix_square_gemm_lhs_conj N=512 23.37 ms 22.08 ms 18.32 ms
f64_rank4_packed_contract_axes n=16 621.53 us 621.47 us 646.96 us
f64_rank4_mixed_contract_axes n=16 672.51 us 676.83 us 646.17 us
f64_rank5_batched_mixed_contract_axes n=16, batch 4 3.03 ms 3.10 ms 2.67 ms
f64_rank4_row_major_view_mixed_contract_axes n=16 935.45 us 954.64 us 651.67 us

Large Sample, 4 Threads

Run with TENFERRO_TBLIS_BENCH_MATMUL_SIZES=256,512 TENFERRO_TBLIS_BENCH_HIGHER_RANK_NS=16 and TBLIS_NUM_THREADS=4 RAYON_NUM_THREADS=4 OMP_NUM_THREADS=4 OPENBLAS_NUM_THREADS=4.

Case Parameter faer BLAS TBLIS
f64_matrix_square_gemm N=256 179.75 us 217.79 us 242.81 us
f64_matrix_square_gemm N=512 1.42 ms 1.57 ms 1.48 ms
c64_matrix_square_gemm_lhs_conj N=256 815.19 us 788.57 us 691.50 us
c64_matrix_square_gemm_lhs_conj N=512 6.30 ms 6.15 ms 5.07 ms
f64_rank4_packed_contract_axes n=16 201.67 us 213.21 us 239.14 us
f64_rank4_mixed_contract_axes n=16 237.86 us 284.31 us 237.75 us
f64_rank5_batched_mixed_contract_axes n=16, batch 4 1.21 ms 1.15 ms 884.72 us
f64_rank4_row_major_view_mixed_contract_axes n=16 528.24 us 542.63 us 240.78 us

Interpretation: TBLIS is not a broad small-size win. Larger complex, mixed-axis, batched, and view-heavy cases are more favorable, including at 4 threads, so this PR presents TBLIS as an optional provider rather than a universal replacement.

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Repository rules review

Repository rules review (c76f260b595c292aa5fbedfc87de80b06e6a0a8f...c9a91158a1f89eadc8ce7d413ac9c6e7dd283c0f)
Verdict: pass
No findings.

@Ryo-wtnb11
Ryo-wtnb11 force-pushed the feature/issue-1332-tblis-cpu-internal branch from 9ab6b04 to 22d20a0 Compare July 10, 2026 14:50
@Ryo-wtnb11

Copy link
Copy Markdown
Collaborator Author

Clarifying the TBLIS coverage contract for this PR.

CpuBackendKind::Tblis is an optional dot_general contraction provider, not a promise that every CPU operation or every mathematically TBLIS-capable contraction is executed by TBLIS. The dispatch contract in this PR is:

  • tenferro attempts TBLIS first for supported rank-N dot_general paths.
  • If the adapter cannot safely build a TBLIS plan, execution falls back to the compiled faer/BLAS provider.
  • This contraction fallback is separate from provider/placement validation; unsupported explicit placement must be a typed error, not a fallback.

Supported TBLIS attempt in this PR:

  • dot_general, dot_general_with_conj, dot_general_read, and dot_general_read_into_accum.
  • f32, f64, c32, and c64.
  • Owned tensors and positive-stride TensorRead views backed by host storage.
  • Higher-rank contractions and batch dimensions when labels, dimensions, and strides fit the TBLIS FFI types.

Intentional fallback cases include:

  • scalar-output inner products;
  • zero-size contractions;
  • rank-0 operands;
  • negative offsets;
  • non-positive strides, including reversed views;
  • inputs without accessible host storage;
  • dtype mismatch or dtypes outside the TBLIS scalar set;
  • any shape/stride/label case where the adapter cannot construct a valid TBLIS plan.

Reviewers should read results in three categories: TBLIS executed, TBLIS was intentionally not applicable and faer/BLAS fallback ran, or the request is an unsupported provider/placement combination and should error rather than fallback.

@shinaoka shinaoka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Blocking: CpuBackendKind::Tblis should not be introduced in this PR.

TBLIS only implements an optional fast path for dot_general; it is not a complete CPU backend. Making it a backend kind changes the scope of the provider selection and makes the fallback policy implicit. In particular, when both cpu-faer and cpu-blas are compiled, the current implementation silently chooses BLAS as the TBLIS fallback, rather than preserving the user-selected base provider.

Please keep CpuBackendKind for the complete base providers (Faer / Blas) and add TBLIS as a dot_general-specific dispatch option, e.g. DotGeneralProvider::TblisIfAvailable. Unsupported layouts/shapes and unavailable TBLIS should fall back to that backend's selected base provider.

Also separate TBLIS availability from execution policy: support mutually exclusive linked and runtime-loaded build modes, with runtime lazy loading as the normal path. The dynamic loader should retain the library and validated symbol table, report its search paths clearly, and permit an explicit Required mode for callers who do not want fallback.

This keeps the existing CPU backend model intact, avoids a hard-coded BLAS-over-faer fallback, and matches tenferro's runtime-loading direction.

Copy link
Copy Markdown
Member

Follow-up with a concrete direction for the requested redesign:

  1. Keep CpuBackendKind as the complete base-provider choice (Faer / Blas). It should continue to determine the normal CPU execution and linalg implementation.

  2. Make TBLIS an immutable, dot_general-specific policy at CpuBackend construction time, conceptually:

pub enum DotGeneralProvider {
    Native,
    PreferTblis,
    RequireTblis,
}

pub struct CpuBackendOptions {
    pub base_kind: CpuBackendKind,
    pub dot_general_provider: DotGeneralProvider,
}

PreferTblis attempts TBLIS only for a supported contraction and otherwise invokes the selected base_kind. RequireTblis should produce a diagnostic when the library is unavailable or the operation is unsupported. Do not silently turn a real TBLIS execution failure into a fallback: distinguish "not applicable" from a failed call.

  1. Separate availability/linkage from that policy. The two linkage strategies should be mutually exclusive Cargo features, for example:
  • cpu-tblis-dynamic: no source-build/link crate; lazy-load TBLIS at runtime.
  • cpu-tblis-linked: explicit/static native linkage for controlled HPC deployments.

The normal path should be the dynamic one, matching tenferro's runtime-loading direction. It should follow the cuTENSOR/cuBLAS pattern: try TENFERRO_TBLIS_PATH first, then platform defaults; retain the loaded Library together with a validated vtable; and include all attempted paths in load diagnostics. The linked mode must not invoke the dynamic loader.

  1. Keep the option fixed for one backend/executor lifetime, rather than mutating it after GEMM analysis caches have been populated. This makes provider attribution and benchmark results reproducible.

  2. Preserve the existing CPU-threading contract. In particular, TBLIS owns its native scheduling when selected; do not install an outer Rayon scope just because the base provider is faer.

Minimum acceptance tests should cover:

  • cpu-faer and cpu-blas both compiled, with base provider set to faer: a TBLIS-declined contraction must use faer, not BLAS.
  • The symmetric BLAS case.
  • Dynamic load failure: PreferTblis falls back, RequireTblis returns a useful error including attempted paths.
  • Unsupported layouts/shapes, zero-size tensors, scalar output, and grouped GEMM all preserve the selected base provider.
  • A successful dynamic-load case and the explicit linked mode.

This is intentionally a redesign, but it removes the hidden provider priority, avoids treating a partial accelerator as a full backend, and keeps dynamic loading as the default deployment model.

@Ryo-wtnb11

Copy link
Copy Markdown
Collaborator Author

Updated the PR for the review feedback.

What changed:

  • Removed CpuBackendKind::Tblis; CpuBackendKind is now only the complete base provider selector (Faer / Blas).
  • Added DotGeneralProvider::{Base, TblisIfAvailable, TblisRequired} and threaded it through CpuBackend and CpuExecSession.
  • TBLIS is now attempted only for dot_general paths, and fallback preserves the selected base provider instead of hard-coding BLAS.
  • Split TBLIS provider modes:
    • cpu-tblis -> runtime lazy loading via tblis-ffi/dynamic_loading.
    • cpu-tblis-linked -> linked/source path via tenferro-tblis-src.
    • runtime and linked modes are mutually exclusive.
  • Runtime TBLIS unavailability falls back under TblisIfAvailable; TblisRequired returns a typed InvalidConfig.
  • Updated tests, provider feature contract checks, passthrough features, and the benchmark setup to use base provider + dot-general policy.

Local checks run:

  • cargo check -p tenferro-cpu --no-default-features --features cpu-faer,cpu-tblis
  • TBLIS_SRC=https://github.com/MatthewsResearchGroup/tblis.git cargo check -p tenferro-cpu --no-default-features --features cpu-faer,cpu-tblis-linked
  • OPENBLAS_FC=/opt/homebrew/bin/gfortran LIBRARY_PATH=/opt/homebrew/lib/gcc/current:/opt/homebrew/lib/gcc/15 DYLD_LIBRARY_PATH=/opt/homebrew/lib/gcc/current:/opt/homebrew/lib/gcc/15 cargo check -p tenferro-cpu --no-default-features --features cpu-faer,cpu-blas,cpu-tblis
  • cargo check -p tenferro-cpu --no-default-features --features cpu-faer,cpu-tblis --bench tblis_dot_general_provider
  • TBLIS_SRC=https://github.com/MatthewsResearchGroup/tblis.git cargo check -p tenferro-cpu --no-default-features --features cpu-faer,cpu-tblis-linked --bench tblis_dot_general_provider
  • cargo test -p tenferro-cpu --no-default-features --features cpu-faer,cpu-tblis tblis_dot_general_falls_back -- --nocapture
  • TBLIS_SRC=https://github.com/MatthewsResearchGroup/tblis.git cargo test -p tenferro-cpu --no-default-features --features cpu-faer,cpu-tblis-linked tblis_dot_general -- --nocapture
  • cargo test -p tenferro-cpu --no-default-features --features cpu-faer --test provider_feature_contract -- --nocapture

@Ryo-wtnb11

Copy link
Copy Markdown
Collaborator Author

Follow-up pushed after CI feedback:

  • Removed the remaining stale CpuBackendKind::Tblis arm from tenferro-linalg linalg provider selection.
  • Updated docs/worklog references so public docs now describe TBLIS as a DotGeneralProvider policy, not a backend kind.
  • Re-ran cargo clippy --workspace --all-targets -- -D warnings locally; it now passes.
  • python3 scripts/test-check-docs-site.py passes locally. The broader docs script chain still cannot complete locally because the temporary crate check cannot resolve index.crates.io in this sandbox, but the stale docs references that were in this branch are removed.

Copy link
Copy Markdown
Member

CI 復旧と最終設計意図を記録します。

この修正の意図

  • TBLIS は CPU backend 全体を置き換えるものではなく、dot_general に限定した public expert policy DotGeneralProvider::{Base, TblisIfAvailable, TblisRequired} として追加しています。CpuBackendKind::{Faer, Blas} は基本 provider の選択として維持します。
  • TblisIfAvailable が fallback するのは discovery 時に利用不能な場合だけで、TBLIS 実行開始後の実エラーは隠しません。
  • cpu-tblis-runtimecpu-tblis compatibility alias)と cpu-tblis-linked を分離し、source helper third_party/t4a-tblis-src は tenferro workspace から除外しています。upstream release を待って tenferro 開発を止めないための配置で、著作権・ライセンス・由来は保持します。
  • tblis-ffi 0.2.6 の discovery panic は、一回限りの pre-FFI probe の境界だけで catch_unwind する暫定 compatibility bridge です。この箇所は Add fallible TBLIS dynamic loading RESTGroup/tblis-rs#4 の fallible loader API を採用するまでの明示的な監査例外であり、通常の library panic 許容範囲を広げるものではありません。
  • この PR は crates.io publish を行いません。新規 package の初回 publish には package ごとの明示的なユーザー承認と metadata / package contents / license audit が必要です。
  • AD semantics の追加・変更はこの PR の目的ではありません。caller-owned workspace を使う AD 向け expert API は別設計として扱います。

最終設計は issue #1332 にも追記しました: #1332 (comment)

CI 復旧

  • 最新 origin/mainc76f260b, CUDA 12.8 RunPod runtime 対応)を merge し、PR head c9a91158a1f89eadc8ce7d413ac9c6e7dd283c0f を push しました。
  • merge 後に cargo fmt --all --check、diff check、Python 3.11 の CI workflow contract tests 60件を通過しています。
  • 通常の CPU/faer、CPU/BLAS、clippy、coverage、docs、repository-rules checks は成功しています。
  • 自動 RunPod 起動は PR author が write role で、信頼境界が要求する maintain/admin に届かず起動不能でした。権限ポリシーは緩めず、admin actor から trusted main workflow を、PR head と merge SHA 8bb97f4469ea63a9024abf9475e2f7c79d160c6c を明示して dispatch しました。
  • 1回目の実GPU runは availability 選択された RTX 4090 host の driver 550(CUDA 12.4相当)と NVRTC 12.8 の不整合により CUDA_ERROR_UNSUPPORTED_PTX_VERSION となりました。全62 failure が同一原因で、PR差分由来ではありません: https://github.com/tensor4all/tenferro-rs/actions/runs/29384639605
  • 同一 SHA の再検証では RTX 4090 / driver 570.195.03 / CUDA 12.8 が選ばれ、CUDA tests 833/833、OpenXLA PJRT E2E、pod cleanup、PR head への required check 公開がすべて成功しました: https://github.com/tensor4all/tenferro-rs/actions/runs/29385399186
  • timeout していた PR-attached wrapper も再実行し、公開済み RunPod check を検出して成功しました: https://github.com/tensor4all/tenferro-rs/actions/runs/29384636427

したがって、CI security policy や required check を回避・緩和せずに、PR head と最新 main の組み合わせを実GPUで検証しています。

@shinaoka
shinaoka merged commit 50c6623 into main Jul 15, 2026
28 of 29 checks passed
@shinaoka
shinaoka deleted the feature/issue-1332-tblis-cpu-internal branch July 15, 2026 10: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.

[Feature]: Add optional TBLIS CPU tensor-contraction backend

2 participants