Skip to content

avx: even-odd decomposition for tensor contractions - #2010

Open
mohitt31 wants to merge 8 commits into
CEED:mainfrom
mohitt31:even-odd-avx-tensor-contract
Open

avx: even-odd decomposition for tensor contractions#2010
mohitt31 wants to merge 8 commits into
CEED:mainfrom
mohitt31:even-odd-avx-tensor-contract

Conversation

@mohitt31

Copy link
Copy Markdown

Summary

Exploit centro-symmetry of GLL-to-Gauss interpolation and gradient matrices to halve FMA count in 1D tensor contractions for the /cpu/self/avx backend.

  • Split symmetric (interp) / antisymmetric (grad) basis matrices into half-size even/odd components
  • Fold input, two half-contractions via existing blocked/remainder/single kernels, unfold output
  • Lazy cache (8 entries) keyed by (t_ptr, t_mode, B, J) with content validation — no setup-time cost, correctly handles assembly paths that reuse pointers with different data
  • Activates only when min(B,J) >= 4 and matrix passes symmetry check; standard path otherwise
  • Handles odd B/J, TRANSPOSE mode, both centro-symmetric and centro-antisymmetric cases

RFC: #2009

Benchmark results

Native x86_64 (Google Cloud Shell, gcc -O3 -march=native), 3D tensor contractions, q=p+1, 10k iterations.

ncomp=1 speedup (opt/blocked baseline / avx/blocked even-odd):

p interp grad interpT gradT
5 1.01x 1.12x 1.00x 1.06x
6 1.15x 1.15x 1.09x 1.11x
7 1.75x 1.84x 1.25x 1.58x
8 1.49x 1.58x 1.88x 1.76x
9 1.64x 1.75x 1.77x 1.61x
10 1.78x 1.60x 1.68x 1.77x
11 1.84x 1.92x 1.83x 1.84x
12 1.91x 1.89x 1.88x 1.95x

ncomp=3 speedup:

p interp grad interpT gradT
5 1.08x 1.14x 1.10x 1.12x
6 1.23x 1.21x 1.16x 1.17x
7 1.74x 1.90x 1.28x 1.60x
8 1.52x 1.61x 1.85x 1.64x
9 1.56x 1.70x 1.54x 1.62x
11 1.81x 1.88x 1.67x 1.87x
12 1.83x 1.84x 1.85x 1.77x

Crossover at p~5-6, reaching ~1.9x at p=12. Below p=4 the even-odd path is gated off (min dim threshold).

Test plan

  • All existing basis and operator tests pass with zero regressions against both /cpu/self/avx/blocked and /cpu/self/avx/serial
  • Tested interp and grad in forward and transpose modes across p=2..12, dim=1..3, ncomp=1 and ncomp=3
  • Verified correctness for symmetric (interp_1d) and antisymmetric (grad_1d) matrices
  • Edge cases: odd p (middle column fold), odd q (middle row unfold), C=1 serial path
  • Operator assembly (t566) verified — cache validates matrix contents on every hit

LLM usage disclosure

Used Claude (Anthropic) for implementation assistance: drafting the even-odd fold/unfold logic, cache data structures, symmetry detection, and debugging the middle-row unfolding for antisymmetric matrices with odd J. Also used for diagnosing the t566 assembly regression (stale cache when pointer reused with different contents).

Exploit centro-symmetry of GLL-to-Gauss interpolation and gradient
matrices to halve the FMA count in 1D tensor contractions for the
/cpu/self/avx backend.

For basis matrices where T[Q-1-q][P-1-p] == +T[q][p] (symmetric,
interp) or T[Q-1-q][P-1-p] == -T[q][p] (antisymmetric, grad), split
into half-size even/odd matrices t_e and t_o. Fold input, perform two
half-contractions using the existing blocked/remainder/single kernels,
then unfold output with symmetry-aware recombination.

Half-matrices are lazily computed and cached per (t_ptr, t_mode, B, J)
tuple on first apply, with an 8-entry cache. Even-odd path activates
only when min(B,J) >= 4 and the matrix passes the symmetry check;
otherwise falls through to the standard dispatch.

Correctly handles odd B (middle column in fold), odd J (middle row in
unfold), and TRANSPOSE mode. Both centro-symmetric and
centro-antisymmetric cases are covered.
The operator assembly path (ceed-preconditioning.c) reuses the same
t pointer with different contents across assembly calls. The even-odd
cache was keyed only on (t_ptr, t_mode, B, J), so a cache hit would
serve stale half-matrices when the underlying data changed.

Store a copy of t in each cache entry and memcmp on every hit. If the
contents changed, refresh the symmetry detection and recompute the
half-matrices. This fixes t566-operator (non-symmetric multi-component
mass matrix assembly) which was producing values off by a factor of 3.

@jeremylt jeremylt 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.

First comments added.

The performance comparison seems to compare opt/blocked to avx/blocked? You should compare avx/blocked on main to avx/blocked on this branch. Comparing to the opt in this branch checks how much handwritten AVX instructions help against whatever the compiler emits itself.

You should compare avx/serial on main and this branch.

let's please talk human to human for discussion in this review

Comment thread backends/avx/ceed-avx-tensor.c Outdated
Comment thread backends/avx/ceed-avx-tensor.c Outdated
Comment thread backends/avx/ceed-avx-tensor.c Outdated
Comment thread backends/avx/ceed-avx-tensor.c Outdated
Comment thread backends/avx/ceed-avx.h Outdated
Rename data structures: CacheEntry -> EvenOddEntry, cache -> entries,
CacheLookup -> EvenOddLookup, CachePopulate -> EvenOddPopulate. Use
descriptive variable names in fold/unfold loops (idx_lower, idx_upper,
idx_folded, idx_half, idx_out_lower, idx_out_upper, idx_out_mid).

Replace per-call memcmp with one-time validation. The operator assembly
path (ceed-preconditioning.c) reuses the same BTD_mat pointer with
different contents across (comp_in, comp_out) iterations. On the first
cache hit, verify contents match the stored copy. Stable basis pointers
(interp_1d, grad_1d) pass validation once and are trusted for all
future calls with zero overhead. Scratch buffers that change contents
are permanently invalidated and fall through to the standard path.

@jeremylt jeremylt 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.

thanks, a few more comments

Comment thread backends/avx/ceed-avx.h Outdated
Comment thread backends/avx/ceed-avx-tensor.c
Comment thread backends/avx/ceed-avx-tensor.c Outdated
Comment thread backends/avx/ceed-avx-tensor.c
- Rename validated -> is_validated per naming convention
- Restore wrapper functions (_4_8, _8_8) for fixed-size dispatch
- Replace StandardDispatch with direct wrapper calls
- Use sizeof(array) in memset for cleaner VLA zeroing
Comment thread backends/avx/ceed-avx-tensor.c Outdated
@jeremylt

Copy link
Copy Markdown
Member

Some of these comments still feel LLM generated. Can you please not use any LLMs to generate any replies to my human generated questions and comments? You are permitted to use LLMs in your personal development process as long as you disclose all of the usage, but in my personal review process I need to talk to the human who is responsible for the changes to make sure we're making the right choices for the codebase

Comment thread backends/avx/ceed-avx-tensor.c
Comment thread backends/avx/ceed-avx-tensor.c
Comment thread backends/avx/ceed-avx-tensor.c Outdated
Comment thread backends/avx/ceed-avx-tensor.c Outdated
Comment thread backends/avx/ceed-avx-tensor.c Outdated
@zatkins-dev

Copy link
Copy Markdown
Collaborator

@mohitt31 Do you plan to continue with this PR? If not, I will go ahead and implement it with the changes requested and credit you in the commits.

@mohitt31

mohitt31 commented Sep 4, 2026

Copy link
Copy Markdown
Author

Hey guys, sorry for the delay. I had my mid-sem exams. I'm still working on this.
I read all the comments and Zach's plan. Moving the even-odd logic to the interface is a good idea. I will push a new update in a few days. I will do exactly what you guys suggested:

1.Add CeedSymmetryType enum
2.Do detection and half-matrix storage in the Basis
3.Put ApplyEvenOdd at the interface level
4.Change the avx backend back to normal
5.I will also change the tolerance to CEED_EPSILON and fix the other small things.

And Jeremy , I got your point about LLM replies. Sorry for that. I'l write everything myself from now on.

@zatkins-dev

Copy link
Copy Markdown
Collaborator

No worries, thanks for the update!

Move the centro-symmetry detection, half-matrix computation, and
even-odd tensor contraction from the AVX backend into the interface
layer so every CPU backend benefits from the optimization.

- Add CeedSymmetryType enum to types.h
- Add lazy-init decomposition getters in ceed-basis.c
- Add CeedTensorContractApplyEvenOdd in ceed-tensor.c with correct
  handling of antisymmetric matrices in transpose mode
- Use even-odd in ref backend for interp, collocated grad, and
  underintegrated grad paths
- Strip even-odd code from avx backend (now uses interface-level path)
Comment thread backends/ref/ceed-ref-basis.c Outdated
Comment on lines +62 to +67
CeedSymmetryType interp_symmetry, grad_symmetry;
const CeedScalar *interp_1d_even = NULL, *interp_1d_odd = NULL;
const CeedScalar *grad_1d_even = NULL, *grad_1d_odd = NULL;

CeedCallBackend(CeedBasisGetEvenOddDecompositionInterp1D(basis, &interp_symmetry, &interp_1d_even, &interp_1d_odd));
CeedCallBackend(CeedBasisGetEvenOddDecompositionGrad1D(basis, &grad_symmetry, &grad_1d_even, &grad_1d_odd));

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.

This should be called in CeedBasisCreateTensorH1 so this decomposition is only computed once.

We'll need to modify the CeedBasis_Ref object to

typedef struct {
  bool             is_collocated;
  CeedScalar      *collo_grad_1d;
  CeedSymmetryType interp_symmetry;
  CeedScalar      *interp_1d_even, *interp_1d_odd;
  CeedSymmetryType grad_symmetry;
  CeedScalar      *grad_1d_even, *grad_1d_odd;
  CeedSymmetryType collo_grad_symmetry;
  CeedScalar      *collo_grad_1d_even, *collo_grad_1d_odd;
} CeedBasis_Ref;

And then CeedbasisDestoryTensor_Ref would need the update

static int CeedBasisDestroyTensor_Ref(CeedBasis basis) {
  CeedBasis_Ref *impl;

  CeedCallBackend(CeedBasisGetData(basis, &impl));
  CeedCallBackend(CeedFree(&impl->collo_grad_1d));
  CeedCallBackend(CeedFree(&impl->interp_1d_even));
  CeedCallBackend(CeedFree(&impl->interp_1d_odd));
  CeedCallBackend(CeedFree(&impl->grad_1d_even));
  CeedCallBackend(CeedFree(&impl->grad_1d_odd));
  CeedCallBackend(CeedFree(&impl->collo_grad_1d_even));
  CeedCallBackend(CeedFree(&impl->collo_grad_1d_odd));
  CeedCallBackend(CeedFree(&impl));
  return CEED_ERROR_SUCCESS;
}

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.

ohhh, I see what you did

You stuck this on the basis object. That can work, but then we'd still want the collo_grad_1d_even and collo_grad_1d_odd here in the backend code

Comment thread interface/ceed-basis.c

@return An error code: 0 - success, otherwise - failure

@ref Backend

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.

really, @ref Developer

Comment thread interface/ceed-basis.c

@return An error code: 0 - success, otherwise - failure

@ref Backend

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.

Same here - this isn't ever used in the backends

Comment thread include/ceed/types.h
CEED_SYMMETRY_SYMMETRIC = 2,
/// Centro-antisymmetric: t[j][b] = -t[J-1-j][B-1-b]
CEED_SYMMETRY_ANTISYMMETRIC = 3,
} CeedSymmetryType;

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.

We'll want to add a CeedSymmetryTypes array for pretty-printing

@jeremylt

jeremylt commented Sep 8, 2026

Copy link
Copy Markdown
Member

Note - CI requests style fixes. Also, this change needs to be summarized in CHANGELOG.md

The sweep only covered the top-left quadrant, so pairs formed by an upper
row and a right-hand column were never compared. Matrices that are
centro-symmetric in one quadrant but not the other were reported as
symmetric, which silently produced wrong results in the even-odd path.

Sweep the top half of the rows over all columns instead, which visits
every pair at least once.
Restore the minimum dimension the AVX version had before this moved to the
interface. Below 4 the fold and unfold cost more than the halved
contraction saves, and the extra rounding step showed up as a tolerance
failure in the multigrid doctests, where the coarse basis is 6x3.
@zatkins-dev

Copy link
Copy Markdown
Collaborator

Should we add a way to disable this functionality? I feel like CeedBasisUseEvenOdd_Ref should take the CeedBasis as an argument and check if it has been disabled. We should probably have a function interface (say, CeedBasisSetUseEvenOdd) and an environment variable (maybe CEED_BASIS_USE_EVEN_ODD) so that it can be adjusted on a per-basis or global basis.

That would make benchmarking considerably easier, which we should also do prior to merging.

@jeremylt

Copy link
Copy Markdown
Member

Should we add a way to disable this functionality? I feel like CeedBasisUseEvenOdd_Ref should take the CeedBasis as an argument and check if it has been disabled. We should probably have a function interface (say, CeedBasisSetUseEvenOdd) and an environment variable (maybe CEED_BASIS_USE_EVEN_ODD) so that it can be adjusted on a per-basis or global basis.

I think this is a good idea. See this commit for something similar: c7c5b1a

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants