Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8b16443
Convert storage lanes in the contiguous pack and vector store fast paths
lkdvos Sep 28, 2026
096ab7f
Mixed operand eltypes with a compute type and an optional accumulator
lkdvos Sep 29, 2026
b9ad2b1
Mixed-domain kernels: complex x real and real x complex on the real S…
lkdvos Sep 29, 2026
88826e2
Add benchmark/bench_mixed.jl: mixed-domain kernels vs promoted vs rea…
lkdvos Sep 29, 2026
42ad4bd
Accumulate a narrow C in a compute-type panel across kc blocks
lkdvos Sep 29, 2026
293e212
Merge branch 'mixed' into mixed-t4
lkdvos Sep 29, 2026
a3d4189
Select the mixed-domain kernels automatically for a complex T with on…
lkdvos Sep 29, 2026
565ad27
bench_mixed.jl: time the promoted path through the named planar kernel
lkdvos Sep 29, 2026
87abcd8
Allocate the compute-type panel of C through the workspace allocator
lkdvos Sep 29, 2026
e5854c3
Reject a non-Float32/Float64 accumulator in _compute_type
lkdvos Sep 29, 2026
02b6cd4
Recheck the panel decision and capacity when a hinted path is taken
lkdvos Sep 29, 2026
00b31f9
Rewrap two long lines; drop a redundant _dense_lanes clause
lkdvos Sep 29, 2026
51a0306
Name RealComplexKernel's accumulator layout in _planar_acc_index
lkdvos Sep 29, 2026
179c545
test_mixed_kernel.jl: reuse one CR/RC shape pair per precision
lkdvos Sep 29, 2026
1558b71
test_mixed.jl: a deterministic round-once test for the panel of C
lkdvos Sep 29, 2026
8492cfd
Drop the mixed-domain kernels' redundant constructor checks
lkdvos Sep 29, 2026
3a6b53f
Fold the panel hint check into _hint_holds; the panel helpers take th…
lkdvos Sep 29, 2026
dfd9b08
Check a named mixed-domain kernel's real side by dispatch on its type
lkdvos Sep 29, 2026
892793c
tensoroperations.jl: keep _qs_check_eligible's name and layout
lkdvos Sep 29, 2026
dc7621c
README: list the mixed-domain kernels; drop the stale promotion wording
lkdvos Sep 29, 2026
966d37c
test_kernel_selection.jl: drop the same-eltype golden shapes from the…
lkdvos Sep 29, 2026
9cd431a
Mixed tests: merge the two allocation testsets; trim restated constants
lkdvos Sep 29, 2026
785b264
Trim the eltype rejection lists to one case per check
lkdvos Sep 29, 2026
27f9ab0
bench_mixed.jl: shorten the header
lkdvos Sep 29, 2026
6ae02cc
Forward the mixed-domain kernels' accumulate without restating its si…
lkdvos Sep 29, 2026
44f6371
Validate the backend's accumulator once, in its inner constructor
lkdvos Sep 29, 2026
7f68104
Restore pre-existing kernel comments the lane conversion did not inva…
lkdvos Sep 29, 2026
129bee5
Merge origin/main: line-by-line packing (#11) into mixed eltypes
lkdvos Sep 29, 2026
e1fb81a
test_mixed.jl: pin kc in the panel allocation test
lkdvos Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,24 @@ Implemented:
kernel-specific panel formats and contiguous fast paths.
- Microkernels: `ScalarKernel` (reference), `SIMDKernel` (default for real
eltypes), `PlanarKernel` (split-complex, default for complex eltypes),
`OneMKernel` (the 1m method) and `FMAddSubKernel` (interleaved complex, used
for small-M complex contractions on AVX-512). Any of them can be named via
`OneMKernel` (the 1m method), `FMAddSubKernel` (interleaved complex, used
for small-M complex contractions on AVX-512) and `ComplexRealKernel`/
`RealComplexKernel` (a complex times a real operand on the real kernel,
default for those). Any of them can be named via
`plan_contract(...; kernel = ...)`.
- Register shapes derived from the detected ISA, demoted when `M` is too short
for a full tile or when a smaller tile keeps stores into `C` unit-stride.
- A BLIS five-loop (`NC`/`KC`/`MC`) nest reusing packed panels, with blocking
from an analytical model of the detected cache sizes (fixed constants when
they are undetected); plus automatically selected paths for dot-product,
outer-product and read-B-in-place shapes.
- `Float32`/`Float64`/`ComplexF32`/`ComplexF64`; `conjA`/`conjB` and each
operand's `StridedView.op` are applied during packing.
- `Float32`/`Float64`/`ComplexF32`/`ComplexF64`, mixed freely (a complex input
needs a complex output). The compute type is the promoted eltype, or the
precision chosen with `accumulator = Float32`/`Float64`
(`plan_contract(...; accumulator)`, `QuasiStridedBackend(; accumulator)`);
operands are converted during packing and the result is rounded to
`eltype(C)` once. `conjA`/`conjB` and each operand's `StridedView.op` are
applied during packing.
- Zero steady-state allocation on Julia >= 1.11 for `execute!` on a reused
plan and for `tensorcontract!` through the backend (which pools workspaces
per task). On Julia 1.10 `SIMDKernel`'s accumulator is not kept in
Expand All @@ -127,11 +134,10 @@ Implemented:
Not implemented:

- Anything but contraction: `tensoradd!`/`tensortrace!` under this backend are
forwarded to `StridedNative()`, so mixed `@tensor` networks still run.
- Mixed eltypes (including real with complex; promotion is TensorOperations'
`promote_contract` job), other eltypes, non-strided operands, an output
aliased with an input, and a conjugated output view: `tensorcontract!` throws
an `ArgumentError` instead of falling back to another backend.
forwarded to `StridedNative()`, so whole `@tensor` networks still run.
- Other eltypes, a complex input with a real output, non-strided operands, an
output aliased with an input, and a conjugated output view: `tensorcontract!`
throws an `ArgumentError` instead of falling back to another backend.
- Being the default backend: `StridedBLAS()` is faster on most shapes.
- Batch axes, traces/diagonals, threading, GPU execution, the 3m complex
method, autotuning.
Expand Down
62 changes: 62 additions & 0 deletions benchmark/bench_mixed.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Mixed-domain GEMM, single core: complex x real (CR) and real x complex (RC)
# through the mixed-domain kernel, the planar kernel (the real operand promoted)
# and the real GEMM of equal FMA count (2M x N x K for CR, M x 2N x K for RC).
# The inner real tile is the host's default real shape, so `real/mixed` is the
# fraction of real efficiency reached.
#
# julia --project=benchmark benchmark/bench_mixed.jl [--dtypes ComplexF64,ComplexF32]
# [--sizes 512,2048] [--reps 5] [--outdir DIR]

include(joinpath(@__DIR__, "harness.jl"))

using QuasiStrided: ComplexRealKernel, RealComplexKernel, PlanarMethod, target_profile,
_derived_shape, _kernel_from_shape

const RUN_DTYPES = parse_dtypes(argopt("dtypes", "ComplexF64,ComplexF32"))
const SIZES = parse_ints(argopt("sizes", "512,2048"))
const REPS = argopt("reps", 5)
const OUTDIR = outdir()
const CSV_PATH = joinpath(OUTDIR, "bench_mixed.csv")

function time_plan(C, A, B; kw...)
plan = plan_contract(StridedView(C), StridedView(A), (1, 2), StridedView(B), (2, 3), (1, 3); kw...)
return median_time_s(() -> execute!(plan, 1, 0); reps = REPS)
end

function run(io)
print_env_header(stdout, "bench_mixed.jl")
println(io, "dtype,case,n,kernel,t_mixed,t_promoted,t_real")
rng = MersenneTwister(1)
for T in RUN_DTYPES, n in SIZES
R = real(T)
MRr, NRr, W = _derived_shape(target_profile(), R)
planar = _kernel_from_shape(_derived_shape(target_profile(), T), T, PlanarMethod())
cases = (
(
"CR", ComplexRealKernel(Val(MRr ÷ 2), Val(NRr), T, Val(W)),
randn(rng, T, n, n), randn(rng, R, n, n), (2n, n),
),
(
"RC", RealComplexKernel(Val(MRr), Val(NRr ÷ 2), T, Val(W)),
randn(rng, R, n, n), randn(rng, T, n, n), (n, 2n),
),
)
for (case, kernel, A, B, (Mr, Nr)) in cases
C = zeros(T, n, n)
tm = time_plan(C, A, B; kernel)
tp = time_plan(C, A, B; kernel = planar)
tr = time_plan(zeros(R, Mr, Nr), randn(rng, R, Mr, n), randn(rng, R, n, Nr))
tag = "$(mr(kernel))x$(nr(kernel))/W$(lanewidth(kernel))"
@printf(
"%-10s %s %5d %-10s mixed %8.4f s promoted %8.4f s real %8.4f s promoted/mixed %.2f real/mixed %.2f\n",
T, case, n, tag, tm, tp, tr, tp / tm, tr / tm
)
println(io, join((T, case, n, tag, tm, tp, tr), ','))
flush(stdout)
end
end
return
end

open(run, CSV_PATH, "w")
println("wrote ", CSV_PATH)
2 changes: 2 additions & 0 deletions src/QuasiStrided.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ include("microkernels/simd.jl")
include("microkernels/planar.jl")
include("microkernels/onem.jl")
include("microkernels/fmaddsub.jl")
include("microkernels/mixed.jl")

# --- Planning: labels, conjugation, kernel and blocking choice, the plan ---
include("planning/labels.jl")
Expand Down Expand Up @@ -72,6 +73,7 @@ end
:public, :contract!, :plan_contract, :execute!, :ContractPlan,
:ContractWorkspace, :Blocking, :default_blocking,
:ScalarKernel, :SIMDKernel, :PlanarKernel, :OneMKernel, :FMAddSubKernel,
:ComplexRealKernel, :RealComplexKernel,
:target_profile, :cache_topology,
:TargetProfile, :CacheLevel
)
Expand Down
20 changes: 13 additions & 7 deletions src/execution/barrier.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,28 +48,32 @@ struct _NestPath{UNPACKED_B, AFF, SPLIT} end

# N's B flag stays `false` when B is read in place: that path builds no B
# sliver axis, so the flag would only split specialisations. A split group is
# enumerated in another order, so its ramp flags are `false`.
# enumerated in another order, so its ramp flags are `false`. `panel`: C is
# `_PanelPath`'s dense panel, whose M and N maps are ramps.
@inline function _nest_path(
unpacked_b::Bool, mgroup::AxisGroup, ngroup::AxisGroup, kgroup::AxisGroup,
split_a::Bool, split_b::Bool
split_a::Bool, split_b::Bool, panel::Bool = false
)
split_b &= !unpacked_b
return _nest_path_from_flags(
unpacked_b,
!split_a && _is_ramp_map(mgroup, 1), !split_a && _is_ramp_map(mgroup, 2),
!unpacked_b && !split_b && _is_ramp_map(ngroup, 1), !split_b && _is_ramp_map(ngroup, 2),
panel ? _PANEL_PATHS : _NEST_PATHS, unpacked_b,
!split_a && _is_ramp_map(mgroup, 1), !split_a && (panel || _is_ramp_map(mgroup, 2)),
!unpacked_b && !split_b && _is_ramp_map(ngroup, 1), !split_b && (panel || _is_ramp_map(ngroup, 2)),
_is_ramp_map(kgroup, 1), _is_ramp_map(kgroup, 2), split_a, split_b
)
end

# The nest run against a dense panel of C in the compute type.
struct _PanelPath{P <: _NestPath} end

# Table lookup rather than `_NestPath{u, aff, split}()` built at runtime (a
# dynamic type application) or a 512-leaf branch tree (slow to infer).
@inline function _nest_path_from_flags(flags::Vararg{Bool, 9})
@inline function _nest_path_from_flags(table::Vector{Any}, flags::Vararg{Bool, 9})
index = 1
for i in 1:9
index += flags[i] << (i - 1)
end
return @inbounds _NEST_PATHS[index]
return @inbounds table[index]
end

const _NEST_PATHS = let
Expand All @@ -81,6 +85,8 @@ const _NEST_PATHS = let
table
end

const _PANEL_PATHS = Any[_PanelPath{typeof(p)}() for p in _NEST_PATHS]

# The dot path at lane width `W`; `MATB`: the matrix operand is B (`Qm == 1`).
struct _DotPath{MATB, W} end

Expand Down
2 changes: 1 addition & 1 deletion src/execution/dot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ end
gptr::Ptr{T}, vstorage::SV, vbase::Int, vkbuf::Vector{Int}, kblock::Int, transform::F
) where {T, SV, F}
@inbounds for t in 0:(kblock - 1)
z = transform(vstorage[vbase + vkbuf[t + 1] + 1])::T
z = convert(T, transform(vstorage[vbase + vkbuf[t + 1] + 1]))
unsafe_store!(gptr + sizeof(T) * t, z)
if T <: Complex
unsafe_store!(gptr + sizeof(T) * (kblock + t), Complex(imag(z), real(z)))
Expand Down
Loading
Loading