Skip to content

perf(lu): factor small square row-major matrices through the full-pivot dual (#314) - #342

Open
shinaoka wants to merge 5 commits into
mainfrom
314-flu-row-major-dual
Open

shinaoka wants to merge 5 commits into
mainfrom
314-flu-row-major-dual

Conversation

@shinaoka

@shinaoka shinaoka commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

Closes the first slice of #314 (Plan item 2 only). Head: 809eaa2 (code 63790b0; the last commit is docs only; see "Measured code" below for which commit the timings cover) on 314-flu-row-major-dual, base main @ b16d6b859ece9fe3b130757b02bc64281d82baa1.

What this changes

full_pivoting::factor::lu_in_place used to send every row-major input of at least 64·64 elements (tall or square) through the shared column-major copy (linalg::row_major_copy_pays), before its own transpose dual (lu_in_place_unblocked(.., transpose = true)) was ever tried. Now square matrices below 128 whose rows are contiguous (col_stride == 1; any row stride, so padded and reversed row-major views count) skip the copy and are factored in place through the dual. Every other shape, and every generic-stride view (neither stride 1, or column stride -1), keeps the copy as on main.

  • New guard row_major_takes_the_copy(m, n, row_stride, col_stride): the old condition (|row_stride| != 1 && m >= n && n > 1 && row_major_copy_pays(m, n)) minus col_stride == 1 && m == n && n < 128.
  • Pivot ties depend on the layout. The search runs on the column-major copy when row_major_takes_the_copy holds, otherwise on the input when |row_stride| < |col_stride| and on its transpose when not, and keeps the smallest column, then the smallest row, of that matrix. So a row-major input that is not copied breaks a tie by row first. The new square exclusion is not the only such case: wide row-major matrices and those below 64·64 elements were never copied (8×16 row-major with A[(1,2)] = A[(0,4)] = 2, max_rank = Some(1), Par::Seq: (0,4) with one transposition; column-major (1,2) with two). Documented on lu_in_place ("Pivot ties") with that dispatch, as Row-major parity: run every dense factorization natively on row-major input via transpose duality (no whole-matrix copy), at column-major performance #314 asks.
  • Fix: the SIMD search kept the wrong row on a tie within a column. Each column is searched with four interleaved accumulators (three in the fused update), merged with a strict >, so the accumulator merged first won a tie even with the larger row: zero 64×64 row-major with A[(2,6)] = A[(2,8)] = 2 pivoted on (2,8), column-major A[(6,2)] = A[(8,2)] on (8,2), on Apple M5 (two f64 lanes). best_score now also takes the other accumulator on an equal score with a smaller row. The code is inherited from upstream; the fix changes the tied pivot for every layout, to the order the scalar search and reduce_2d's comment already describe. Not timed (one merge per column per step).
  • row_major_copy_pays and the other families that share it (QR, column-pivoted QR, partial-pivot LU) are not touched.
  • PATCHSET.md: the full-pivoting note in the "Row-major LU, partial pivoting" row. Its sentence "full pivoting is deliberately not adapted" had been out of date since 10ee1c9.
  • Record: docs/worklogs/2026-11-09-full-piv-lu-row-major-dual.md. The full measurement comment: Row-major parity: run every dense factorization natively on row-major input via transpose duality (no whole-matrix copy), at column-major performance #314 (comment)

Measurement 2: the committed code (Apple M5 Max, 32c615f vs b16d6b8)

Measured code. This run measured 32c615f (clean committed tree, faer/src tree 38ce727). The head 63790b0 changes faer/src after that: the tie fix in best_score (one more mask per accumulator merge, once per column per step) and doc comments. The head itself was not re-timed; that the tie fix leaves these ratios unchanged is not measured.

  • Arms, each process reporting its own pair: head 32c615f gives A (column-major) and C (row-major through the dual); base b16d6b8 gives A and B (row-major through the copy). The ratio is taken within one process; the table gives each repetition and the median of three.
  • fbench 01a1cb8 with the one-line local set_threads patch (as in measurement 1), --reps 5 --prime-ms 250 --statistic median, family full_piv_lu, row faer-fork, the in_place line. f64 and c64, --threads 1 / 4.
  • Per cell and repetition, the two binaries ran back to back: head first on odd repetitions, base first on even. Cells cycled per repetition.
  • Gate as in measurement 1: a cell was kept only if no rustc, cargo above 5% CPU, or other target/ binary ran before and after it. 96 kept, 11 discarded and rerun (7 for rustc or cargo, 4 for other sessions' test binaries). 1-min load before a kept cell: 7.0–26.4. 18:41–19:01 JST, 2026-11-09. XProtect not recorded.
  • fbench verify on the head binary: all 64 cells per thread count ok (1T, 4T).

Result. On the 24 changed cells (64–127), median C/A is 0.888–1.017; median B/A on main is 0.978–1.343. The 128 and 256 cells run the same code on both revisions (the copy) and are the control: C/A 0.976–1.181, B/A 1.005–1.150.

Noise. Single repetitions swing far more than the medians: one control cell reads 0.650 and another 1.616, and the A arms of the two binaries in one repetition differ by up to 1.8× (last column). Three repetitions under this load are a scan, not a campaign comparison. candidate_compare.py was not run.

T dtype n A head µs head C/A per rep median base B/A per rep median A head/A base (same rep)
1 c64 64 83.5 0.869 / 1.005 / 0.968 0.968 1.375 / 1.067 / 1.137 1.137 0.724 / 0.942 / 1.032
1 c64 80 153.0 1.020 / 1.010 / 1.007 1.010 1.528 / 1.188 / 1.112 1.188 0.992 / 0.987 / 1.001
1 c64 96 264.1 0.992 / 1.001 / 0.952 0.992 1.141 / 1.098 / 1.091 1.098 1.004 / 1.008 / 1.050
1 c64 112 410.2 1.001 / 1.017 / 1.047 1.017 1.032 / 1.078 / 1.068 1.068 0.978 / 0.998 / 0.991
1 c64 120 512.2 1.002 / 0.983 / 0.973 0.983 1.073 / 1.071 / 1.116 1.073 0.992 / 1.019 / 1.020
1 c64 127 598.1 0.979 / 0.996 / 0.998 0.996 1.059 / 0.975 / 0.978 0.978 0.909 / 0.926 / 1.002
1 c64 128 670.0 0.650 / 1.062 / 1.089 1.062 1.497 / 1.074 / 1.054 1.074 1.284 / 1.000 / 0.989
1 c64 256 5256.2 0.897 / 1.027 / 1.036 1.027 0.889 / 1.066 / 1.040 1.040 1.079 / 1.005 / 0.990
1 f64 64 38.4 0.904 / 1.315 / 0.998 0.998 1.116 / 1.071 / 1.248 1.116 1.001 / 0.770 / 1.011
1 f64 80 89.8 0.988 / 0.606 / 0.997 0.988 1.262 / 1.381 / 1.205 1.262 1.025 / 1.479 / 0.992
1 f64 96 127.5 0.734 / 0.981 / 1.006 0.981 1.184 / 1.062 / 1.168 1.168 1.344 / 1.001 / 0.976
1 f64 112 154.8 1.008 / 1.017 / 1.032 1.017 1.159 / 1.170 / 1.167 1.167 0.998 / 1.000 / 0.996
1 f64 120 186.3 1.044 / 1.012 / 0.999 1.012 1.177 / 1.450 / 1.169 1.177 0.986 / 1.126 / 1.000
1 f64 127 221.0 1.009 / 0.886 / 1.019 1.009 1.173 / 1.149 / 1.115 1.149 0.994 / 0.998 / 0.954
1 f64 128 237.7 1.111 / 1.616 / 1.181 1.181 1.159 / 0.839 / 1.150 1.150 1.036 / 0.652 / 0.993
1 f64 256 2074.5 1.071 / 1.102 / 1.060 1.071 1.060 / 1.067 / 1.070 1.067 1.010 / 1.005 / 0.998
4 c64 64 91.5 0.995 / 0.905 / 1.010 0.995 1.447 / 1.076 / 1.296 1.296 1.131 / 1.109 / 0.997
4 c64 80 156.9 1.124 / 1.006 / 0.997 1.006 1.466 / 1.083 / 1.095 1.095 1.008 / 1.006 / 1.015
4 c64 96 328.0 0.888 / 1.006 / 0.826 0.888 1.355 / 1.062 / 1.196 1.196 0.989 / 0.992 / 1.264
4 c64 112 408.8 0.949 / 1.042 / 0.965 0.965 1.107 / 1.073 / 1.040 1.073 0.814 / 1.028 / 1.036
4 c64 120 441.9 0.812 / 1.013 / 0.979 0.979 0.988 / 1.069 / 1.048 1.048 1.273 / 1.005 / 1.024
4 c64 127 476.4 1.029 / 0.996 / 0.996 0.996 1.056 / 1.042 / 1.040 1.042 1.150 / 0.995 / 0.976
4 c64 128 539.1 1.029 / 1.057 / 1.190 1.057 1.302 / 1.024 / 1.035 1.035 1.156 / 0.988 / 0.988
4 c64 256 2447.7 0.666 / 1.022 / 1.034 1.022 0.824 / 1.005 / 1.007 1.005 1.194 / 0.959 / 1.036
4 f64 64 35.5 0.892 / 0.989 / 0.995 0.989 1.816 / 1.343 / 1.136 1.343 0.831 / 1.011 / 0.988
4 f64 80 62.2 0.997 / 0.997 / 0.991 0.997 1.462 / 1.166 / 1.195 1.195 1.005 / 1.003 / 1.015
4 f64 96 105.5 0.800 / 0.974 / 0.979 0.974 1.119 / 1.116 / 1.116 1.116 1.131 / 1.017 / 1.025
4 f64 112 156.2 1.113 / 0.990 / 0.894 0.990 0.959 / 1.074 / 1.081 1.074 0.795 / 0.999 / 1.121
4 f64 120 193.7 0.631 / 1.003 / 1.070 1.003 1.454 / 1.238 / 1.052 1.238 1.256 / 0.999 / 1.005
4 f64 127 232.3 0.996 / 1.157 / 0.967 0.996 1.304 / 0.921 / 1.090 1.090 1.067 / 0.546 / 1.041
4 f64 128 230.7 1.220 / 1.073 / 1.072 1.073 1.150 / 1.076 / 1.071 1.076 1.000 / 0.978 / 0.978
4 f64 256 1608.2 0.696 / 0.985 / 0.976 0.976 0.782 / 1.021 / 1.082 1.021 0.898 / 1.020 / 1.078
gate log (load average 1 / 5 / 15 min before and after each cell)
DISCARD r1 T1 f64 64 before 26.68 47.09 40.22 | after 25.19 46.44 40.03 | competing: issue_243_dynamic_update_preflight-7352aa8e53b96fb9 
KEEP r1 T1 f64 64 before 25.25 45.02 39.67 | after 25.25 45.02 39.67 
KEEP r1 T1 f64 80 before 25.25 45.02 39.67 | after 26.43 44.93 39.67 
KEEP r1 T1 f64 96 before 26.43 44.93 39.67 | after 26.43 44.93 39.67 
KEEP r1 T1 f64 112 before 15.47 35.19 38.06 | after 15.47 35.19 38.06 
KEEP r1 T1 f64 120 before 15.47 35.19 38.06 | after 14.63 34.69 37.86 
KEEP r1 T1 f64 127 before 14.63 34.69 37.86 | after 14.63 34.69 37.86 
KEEP r1 T1 f64 128 before 14.63 34.69 37.86 | after 13.62 34.15 37.65 
KEEP r1 T1 f64 256 before 13.62 34.15 37.65 | after 13.62 34.15 37.65 
KEEP r1 T1 c64 64 before 13.62 34.15 37.65 | after 13.62 34.15 37.65 
KEEP r1 T1 c64 80 before 13.62 34.15 37.65 | after 13.41 33.76 37.50 
DISCARD r1 T1 c64 96 before 13.41 33.76 37.50 | after 13.41 33.76 37.50 | competing: cargo 
KEEP r1 T1 c64 96 before 11.15 30.61 36.17 | after 11.38 30.34 36.04 
KEEP r1 T1 c64 112 before 11.38 30.34 36.04 | after 11.38 30.34 36.04 
KEEP r1 T1 c64 120 before 11.38 30.34 36.04 | after 11.19 29.98 35.88 
KEEP r1 T1 c64 127 before 11.19 29.98 35.88 | after 11.19 29.98 35.88 
KEEP r1 T1 c64 128 before 11.19 29.98 35.88 | after 11.25 29.68 35.74 
KEEP r1 T1 c64 256 before 11.25 29.68 35.74 | after 11.25 29.68 35.74 
DISCARD r1 T4 f64 64 before 11.25 29.68 35.74 | after 12.03 29.54 35.65 | competing: cargo rustc 
KEEP r1 T4 f64 64 before 23.66 30.50 35.73 | after 23.66 30.50 35.73 
DISCARD r1 T4 f64 80 before 23.66 30.50 35.73 | after 24.96 30.66 35.76 | competing: faer-9daf428e14f4dbf0 
KEEP r1 T4 f64 80 before 21.76 29.58 35.26 | after 21.76 29.58 35.26 
KEEP r1 T4 f64 96 before 21.76 29.58 35.26 | after 21.76 29.58 35.26 
KEEP r1 T4 f64 112 before 21.76 29.58 35.26 | after 20.82 29.26 35.11 
KEEP r1 T4 f64 120 before 20.82 29.26 35.11 | after 20.82 29.26 35.11 
KEEP r1 T4 f64 127 before 20.82 29.26 35.11 | after 20.03 28.96 34.97 
DISCARD r1 T4 f64 128 before 20.03 28.96 34.97 | after 20.03 28.96 34.97 | competing: public_qz_scratch-22a575bd00a6dc9a 
KEEP r1 T4 f64 128 before 11.87 25.00 33.08 | after 11.56 24.72 32.93 
KEEP r1 T4 f64 256 before 11.56 24.72 32.93 | after 11.56 24.72 32.93 
DISCARD r1 T4 c64 64 before 9.60 23.42 32.27 | after 9.71 23.21 32.15 | competing: rustc 
DISCARD r1 T4 c64 64 before 9.20 22.23 31.59 | after 9.20 22.23 31.59 | competing: rustc 
KEEP r1 T4 c64 64 before 10.12 20.09 30.14 | after 10.12 20.09 30.14 
KEEP r1 T4 c64 80 before 10.12 20.09 30.14 | after 9.63 19.82 29.99 
KEEP r1 T4 c64 96 before 9.63 19.82 29.99 | after 9.63 19.82 29.99 
KEEP r1 T4 c64 112 before 9.63 19.82 29.99 | after 9.74 19.67 29.88 
KEEP r1 T4 c64 120 before 9.74 19.67 29.88 | after 9.74 19.67 29.88 
KEEP r1 T4 c64 127 before 9.74 19.67 29.88 | after 10.08 19.58 29.78 
KEEP r1 T4 c64 128 before 10.08 19.58 29.78 | after 10.08 19.58 29.78 
KEEP r1 T4 c64 256 before 10.08 19.58 29.78 | after 9.99 19.40 29.66 
KEEP r2 T1 f64 64 before 9.99 19.40 29.66 | after 9.99 19.40 29.66 
KEEP r2 T1 f64 80 before 9.99 19.40 29.66 | after 9.99 19.40 29.66 
KEEP r2 T1 f64 96 before 10.15 19.28 29.56 | after 10.15 19.28 29.56 
KEEP r2 T1 f64 112 before 10.15 19.28 29.56 | after 10.15 19.28 29.56 
KEEP r2 T1 f64 120 before 10.15 19.28 29.56 | after 9.82 19.06 29.42 
KEEP r2 T1 f64 127 before 9.82 19.06 29.42 | after 9.82 19.06 29.42 
KEEP r2 T1 f64 128 before 9.82 19.06 29.42 | after 9.84 18.91 29.31 
DISCARD r2 T1 f64 256 before 9.84 18.91 29.31 | after 9.84 18.91 29.31 | competing: cargo rustc 
KEEP r2 T1 f64 256 before 10.19 18.41 28.89 | after 9.85 18.20 28.75 
KEEP r2 T1 c64 64 before 9.85 18.20 28.75 | after 9.85 18.20 28.75 
KEEP r2 T1 c64 80 before 9.85 18.20 28.75 | after 9.85 18.20 28.75 
KEEP r2 T1 c64 96 before 9.85 18.20 28.75 | after 9.78 18.05 28.64 
KEEP r2 T1 c64 112 before 9.78 18.05 28.64 | after 9.78 18.05 28.64 
KEEP r2 T1 c64 120 before 9.78 18.05 28.64 | after 9.24 17.80 28.48 
KEEP r2 T1 c64 127 before 9.24 17.80 28.48 | after 9.24 17.80 28.48 
DISCARD r2 T1 c64 128 before 9.24 17.80 28.48 | after 9.06 17.62 28.36 | competing: rustc 
KEEP r2 T1 c64 128 before 10.07 17.03 26.48 | after 10.07 17.03 26.48 
DISCARD r2 T1 c64 256 before 10.07 17.03 26.48 | after 9.66 16.83 26.36 | competing: issue_243_dynamic_update_preflight-e20fbcab3358d905 
KEEP r2 T1 c64 256 before 8.36 16.08 25.87 | after 8.36 16.08 25.87 
KEEP r2 T4 f64 64 before 8.36 16.08 25.87 | after 8.49 15.98 25.77 
KEEP r2 T4 f64 80 before 8.49 15.98 25.77 | after 8.49 15.98 25.77 
KEEP r2 T4 f64 96 before 8.49 15.98 25.77 | after 8.13 15.78 25.65 
KEEP r2 T4 f64 112 before 8.13 15.78 25.65 | after 8.13 15.78 25.65 
KEEP r2 T4 f64 120 before 8.13 15.78 25.65 | after 8.13 15.78 25.65 
KEEP r2 T4 f64 127 before 8.13 15.78 25.65 | after 7.80 15.58 25.52 
KEEP r2 T4 f64 128 before 9.06 14.57 24.46 | after 9.06 14.57 24.46 
KEEP r2 T4 f64 256 before 9.06 14.57 24.46 | after 8.65 14.40 24.34 
KEEP r2 T4 c64 64 before 8.65 14.40 24.34 | after 8.65 14.40 24.34 
KEEP r2 T4 c64 80 before 8.65 14.40 24.34 | after 9.00 14.37 24.28 
KEEP r2 T4 c64 96 before 9.00 14.37 24.28 | after 9.00 14.37 24.28 
KEEP r2 T4 c64 112 before 9.00 14.37 24.28 | after 8.92 14.27 24.18 
KEEP r2 T4 c64 120 before 8.92 14.27 24.18 | after 8.92 14.27 24.18 
KEEP r2 T4 c64 127 before 8.92 14.27 24.18 | after 8.92 14.27 24.18 
KEEP r2 T4 c64 128 before 8.92 14.27 24.18 | after 8.93 14.18 24.09 
KEEP r2 T4 c64 256 before 8.93 14.18 24.09 | after 8.93 14.18 24.09 
KEEP r3 T1 f64 64 before 8.93 14.18 24.09 | after 8.53 14.01 23.97 
KEEP r3 T1 f64 80 before 8.53 14.01 23.97 | after 8.53 14.01 23.97 
KEEP r3 T1 f64 96 before 8.53 14.01 23.97 | after 8.41 13.89 23.87 
KEEP r3 T1 f64 112 before 8.41 13.89 23.87 | after 8.41 13.89 23.87 
KEEP r3 T1 f64 120 before 8.41 13.89 23.87 | after 8.41 13.89 23.87 
KEEP r3 T1 f64 127 before 8.41 13.89 23.87 | after 8.06 13.73 23.76 
KEEP r3 T1 f64 128 before 8.06 13.73 23.76 | after 8.06 13.73 23.76 
KEEP r3 T1 f64 256 before 8.06 13.73 23.76 | after 8.29 13.68 23.68 
KEEP r3 T1 c64 64 before 8.29 13.68 23.68 | after 8.29 13.68 23.68 
KEEP r3 T1 c64 80 before 8.29 13.68 23.68 | after 8.11 13.56 23.58 
KEEP r3 T1 c64 96 before 8.11 13.56 23.58 | after 8.11 13.56 23.58 
KEEP r3 T1 c64 112 before 8.11 13.56 23.58 | after 8.11 13.56 23.58 
DISCARD r3 T1 c64 120 before 8.02 13.45 23.48 | after 8.02 13.45 23.48 | competing: rustc 
KEEP r3 T1 c64 120 before 7.80 13.03 23.10 | after 7.80 13.03 23.10 
KEEP r3 T1 c64 127 before 7.80 13.03 23.10 | after 7.73 12.93 23.00 
KEEP r3 T1 c64 128 before 7.73 12.93 23.00 | after 7.73 12.93 23.00 
KEEP r3 T1 c64 256 before 7.73 12.93 23.00 | after 7.92 12.88 22.93 
KEEP r3 T4 f64 64 before 7.92 12.88 22.93 | after 7.92 12.88 22.93 
KEEP r3 T4 f64 80 before 7.92 12.88 22.93 | after 7.60 12.73 22.81 
KEEP r3 T4 f64 96 before 7.60 12.73 22.81 | after 7.60 12.73 22.81 
KEEP r3 T4 f64 112 before 7.60 12.73 22.81 | after 7.60 12.73 22.81 
KEEP r3 T4 f64 120 before 7.60 12.73 22.81 | after 7.39 12.61 22.71 
KEEP r3 T4 f64 127 before 7.39 12.61 22.71 | after 7.39 12.61 22.71 
KEEP r3 T4 f64 128 before 7.39 12.61 22.71 | after 7.04 12.45 22.59 
KEEP r3 T4 f64 256 before 7.04 12.45 22.59 | after 7.04 12.45 22.59 
KEEP r3 T4 c64 64 before 7.04 12.45 22.59 | after 7.12 12.37 22.51 
KEEP r3 T4 c64 80 before 7.12 12.37 22.51 | after 7.12 12.37 22.51 
KEEP r3 T4 c64 96 before 7.12 12.37 22.51 | after 7.12 12.37 22.51 
KEEP r3 T4 c64 112 before 7.03 12.27 22.41 | after 7.03 12.27 22.41 
KEEP r3 T4 c64 120 before 7.03 12.27 22.41 | after 7.03 12.27 22.41 
KEEP r3 T4 c64 127 before 7.03 12.27 22.41 | after 7.11 12.20 22.33 
KEEP r3 T4 c64 128 before 7.11 12.20 22.33 | after 7.11 12.20 22.33 
KEEP r3 T4 c64 256 before 7.11 12.20 22.33 | after 7.58 12.21 22.27 
finished

Measurement 2 commands

# two fbench binaries, one per faer revision (fbench @ 01a1cb8 + the set_threads patch)
cargo build --release -p fbench --features fork-par-shape,fork-team \
  --config 'patch.crates-io.t4a-faer.path="<faer @ 32c615f>/faer"'    # head
cargo build --release -p fbench --features fork-par-shape,fork-team \
  --config 'patch.crates-io.t4a-faer.path="<faer @ b16d6b8>/faer"'    # base
# per r in 1..3, T in {1,4}, dtype in {f64,c64}, n in {64,80,96,112,120,127,128,256}, gated:
fbench run --threads $T --n $N --dtype $DT --family full_piv_lu --rows faer-fork \
  --reps 5 --prime-ms 250 --statistic median --csv $ARM-r$r-T$T-$DT-$N.csv
fbench verify --threads $T --n 64,80,96,112,120,127,128,256 --dtype f64,c64 --family full_piv_lu --rows faer-fork --csv verify.csv

Which sizes were timed. M5: 64, 80, 96, 112, 120, 127 (measurement 2, f64/c64, 1T/4T). Zen 5: 64 and 96 (the earlier #314 comment). The sizes in between were not timed; the guard takes them on the strength of their neighbours. 128 and above, and the tall shapes, come from measurement 1.

Measurement 1: boundary and large shapes (Apple M5 Max, f64, b16d6b8 + uncommitted switch)

Arms: A column-major input. B row-major input through the copy (main). C row-major input through the dual (main + a local, never committed FAER_FLU_COPY=off environment switch that skipped the copy branch on every shape). The switch is in no published revision, so arm C of this run cannot be rebuilt from a checkout; measurement 2 covers the committed code. Each value is the median of 15 samples (3 repetitions × --reps 5).

shape 1T B/A 1T C/A 4T B/A 4T C/A
64 1.244 1.009 1.138 1.039
128 1.150 1.007 1.059 1.005
256 1.065 1.038 1.042 0.986
512 1.036 1.039 1.082 1.087
1024 1.022 1.032 1.010 1.089
1024x64 1.217 1.461 1.177 2.058
4096x256 1.079 1.398 1.209 1.736

Absolute values (A / B / C, µs): 1T 64: 34.2 / 42.5 / 34.5; 1T 1024: 115,442 / 118,034 / 119,101; 4T 1024: 46,202 / 46,641 / 50,330. The full table, with min/median/max, is in the issue comment.

Why the boundary is square n < 128. The acceptance criterion is C within 1.05× of A.

  • On M5, n = 64 and 128 pass at both thread counts.
  • M5 at 4T leaves the band from 512 (1.087, 1.089).
  • Zen 5, measured earlier in Row-major parity: run every dense factorization natively on row-major input via transpose duality (no whole-matrix copy), at column-major performance #314 (1T/8T, f64/c64/f32), passes for 64 and 96 (0.94–1.04), but has n = 128 f64 1T at 1.114 and n ≥ 256 at 1.08–1.27.
  • Square 64–120 (64, 80, 96, 112, 120) was at 0.975–1.038 in f64 and 0.976–1.024 in c64 on M5, at 1T and 4T, in an extra scan run before the process gate below (1-min load about 7). Measurement 2 replaces it for these sizes and adds 127.
  • So 128 is the first size that fails on at least one machine. M5 at 1T alone stays at 1.03–1.04 up to 1024, so a single-thread-only rule could go further; that is not taken.

Why non-square shapes are excluded. In the same ungated scan on M5, f64 C/A (1T / 4T) is:

  • 80x64: 1.061 / 1.063
  • 96x64: 1.101 / 1.088
  • 128x64: 1.112 / 1.140

Tall shapes are 1.40–2.06× (table). The band ends as soon as the matrix stops being square.

Where the dual loses (per-stage timers inside lu_in_place_unblocked, local and not committed; run before the gate, at load about 5–7):

  • Squares: the fused pivot search and rank-1 update cost the same in both layouts (1024 1T: 104.4 vs 103.7 ms). The gap is the dual's row scaling A[(k, j)] *= inv, a strided O(n²) walk: 0.22 → 3.26 ms at 1024, 0.06 → 0.60 ms at 512.
  • Tall (4096x256 1T): the same scaling (0.39 → 5.04 ms), plus the long swap turning strided (1.25 → 7.63 ms), plus the wide trailing block (update 36.1 → 40.2 ms).

Measurement 1 conditions

host Apple M5 Max (18 logical: 6 + 12), 128 GB, macOS 26.7.2; threads cannot be pinned on macOS
toolchain rustc 1.96.0 (ac68faa20 2026-05-25), release
harness tensor4all/faer-benchmark @ 01a1cb8 fbench, family full_piv_lu, row faer-fork, the in_place line. Vendor not linked, with the one-line local set_threads patch (returns Ok for NAME == "none") also used in the Zen 5 comment
fork measured b16d6b8 + FAER_FLU_COPY switch (uncommitted)
threads --threads 1 / --threads 4 (faer::team::with_team; CSV rayon_threads = 1 / 4)
order per repetition and per cell, two processes back to back: on (B = order c, A = order f), then off (C = order c, A′ = order f). Cells are cycled per repetition, not grouped by arm
gate a 1-min load below 4 is not reachable on this host (idle baseline about 6: WindowServer, sysmond). A cell was kept only if no rustc, cargo above 5% CPU, or other target/ / bench binary ran before and after it. 2 of 44 cells were discarded and rerun (one rustc, one strided_basic test binary)
load average (1 / 5 / 15 min) over the kept cells 1 min 7.58–49.01, 5 min 29.23–54.65, 15 min 26.63–32.71. It decays from two other agents' builds that had just finished. The run was 17:50–18:01 JST, 2026-11-09
XProtect not recorded per cell (the gate did not look at it). Observed around the run: at 17:15 JST XprotectService at 94% CPU, and XProtectRemediatorPirrit at about 88% reported separately in that window. At 18:07 JST every XProtect process was at 0.0% CPU and no XProtectRemediator* process existed. Whether it ran during 17:50–18:01 is unknown
noise the two column-major controls (A, A′) of a cell agree within 1% at 1T, but differ by 5–15% at 4T on 256 and above (unpinned cores). The 4T large-shape ratios are the least reliable numbers here
load average before / after each cell (1 / 5 / 15 min)
KEEP r1 T1 64 before 49.01 54.65 32.71 | after 45.48 53.83 32.55
KEEP r1 T1 128 before 45.48 53.83 32.55 | after 45.48 53.83 32.55
KEEP r1 T1 256 before 45.48 53.83 32.55 | after 42.40 53.05 32.40
KEEP r1 T1 512 before 42.40 53.05 32.40 | after 42.40 53.05 32.40
KEEP r1 T1 1024 before 42.40 53.05 32.40 | after 34.13 50.70 31.92
KEEP r1 T1 1024x64 before 34.13 50.70 31.92 | after 34.13 50.70 31.92
KEEP r1 T1 4096x256 before 34.13 50.70 31.92 | after 32.12 50.00 31.78
KEEP r1 T4 64 before 32.12 50.00 31.78 | after 29.87 49.24 31.62
KEEP r1 T4 128 before 29.87 49.24 31.62 | after 29.87 49.24 31.62
KEEP r1 T4 256 before 29.87 49.24 31.62 | after 29.87 49.24 31.62
KEEP r1 T4 512 before 27.64 48.45 31.45 | after 27.64 48.45 31.45
KEEP r1 T4 1024 before 27.64 48.45 31.45 | after 24.81 47.17 31.19
KEEP r1 T4 1024x64 before 24.81 47.17 31.19 | after 24.81 47.17 31.19
KEEP r1 T4 4096x256 before 24.81 47.17 31.19 | after 23.47 46.52 31.05
KEEP r2 T1 64 before 23.47 46.52 31.05 | after 23.47 46.52 31.05
KEEP r2 T1 128 before 21.91 45.81 30.89 | after 21.91 45.81 30.89
KEEP r2 T1 256 before 21.91 45.81 30.89 | after 21.91 45.81 30.89
KEEP r2 T1 512 before 21.91 45.81 30.89 | after 20.55 45.14 30.74
KEEP r2 T1 1024 before 20.55 45.14 30.74 | after 18.07 43.80 30.43
KEEP r2 T1 1024x64 before 18.07 43.80 30.43 | after 17.11 43.17 30.29
KEEP r2 T1 4096x256 before 17.11 43.17 30.29 | after 16.30 42.57 30.15
KEEP r2 T4 64 before 16.30 42.57 30.15 | after 16.30 42.57 30.15
KEEP r2 T4 128 before 16.30 42.57 30.15 | after 15.39 41.94 30.01
KEEP r2 T4 256 before 15.39 41.94 30.01 | after 15.39 41.94 30.01
KEEP r2 T4 512 before 15.39 41.94 30.01 | after 14.80 41.38 29.88
KEEP r2 T4 1024 before 14.80 41.38 29.88 | after 13.83 40.30 29.63
KEEP r2 T4 1024x64 before 13.83 40.30 29.63 | after 13.83 40.30 29.63
KEEP r2 T4 4096x256 before 13.83 40.30 29.63 | after 13.37 39.76 29.50
KEEP r3 T1 64 before 13.37 39.76 29.50 | after 13.37 39.76 29.50
KEEP r3 T1 128 before 13.37 39.76 29.50 | after 13.34 39.32 29.40
KEEP r3 T1 256 before 13.34 39.32 29.40 | after 13.34 39.32 29.40
KEEP r3 T1 512 before 13.34 39.32 29.40 | after 12.83 38.78 29.27
DISCARD r3 T1 1024 before 12.83 38.78 29.27 | after 11.47 37.63 28.98 | competing: strided_basic-eba278932c11ffbb
KEEP r3 T1 1024 before 9.29 34.61 28.18 | after 9.68 33.46 27.88
KEEP r3 T1 1024x64 before 9.68 33.46 27.88 | after 9.68 33.46 27.88
DISCARD r3 T1 4096x256 before 9.68 33.46 27.88 | after 9.55 33.04 27.77 | competing: rustc
KEEP r3 T1 4096x256 before 8.31 31.62 27.35 | after 8.21 31.21 27.23
KEEP r3 T4 64 before 8.21 31.21 27.23 | after 7.87 30.76 27.09
KEEP r3 T4 128 before 7.87 30.76 27.09 | after 7.87 30.76 27.09
KEEP r3 T4 256 before 7.87 30.76 27.09 | after 7.88 30.38 26.98
KEEP r3 T4 512 before 7.88 30.38 26.98 | after 7.88 30.38 26.98
KEEP r3 T4 1024 before 7.88 30.38 26.98 | after 7.58 29.58 26.74
KEEP r3 T4 1024x64 before 7.58 29.58 26.74 | after 7.58 29.58 26.74
KEEP r3 T4 4096x256 before 7.58 29.58 26.74 | after 7.70 29.23 26.63

Measurement 1 commands

# fbench against the fork checkout with the local copy-bypass switch
cd faer-benchmark   # @ 01a1cb8, with the set_threads local patch
CARGO_TARGET_DIR=$SP/fb-target cargo build --release -p fbench \
  --config 'patch.crates-io.t4a-faer.path="<faer checkout @ b16d6b8 + switch>/faer"' \
  --features fork-par-shape,fork-team

# one cell, one repetition (the driver loops r = 1..3, T in {1, 4}, the seven shapes, gated as above)
FAER_FLU_COPY=on  fbench run --threads $T --n $SHAPE --dtype f64 --family full_piv_lu --rows faer-fork \
  --reps 5 --prime-ms 250 --statistic median --csv on.csv     # A (order f) and B (order c)
FAER_FLU_COPY=off fbench run --threads $T --n $SHAPE --dtype f64 --family full_piv_lu --rows faer-fork \
  --reps 5 --prime-ms 250 --statistic median --csv off.csv    # C (order c) and A' (order f)
# SHAPE in 64,128,256,512,1024,1024x64,4096x256; load: sysctl -n vm.loadavg before and after

Verification

Tests at 63790b0:

cargo test -p t4a-faer --test full_piv_lu_row_major_no_copy
cargo test -p t4a-faer --lib -- pivot_search full_pivoting test_row_major_adaptation
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 841 filtered out

Red → green for the tie fix: the two tie tests run with best_score reverted to the strict > (red), then with the fix (green), the sources touched before each build:

# red (fix removed)
test full_piv_lu_tie_order_depends_on_the_layout ... FAILED
test small_square_row_major_full_piv_lu_does_not_copy_the_matrix ... ok
thread 'full_piv_lu_tie_order_depends_on_the_layout' (142630843) panicked at faer/tests/full_piv_lu_row_major_no_copy.rs:227:5:
  left: (2, 8, 2)
 right: (2, 6, 2)
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test linalg::lu::full_pivoting::factor::tests::pivot_search_breaks_ties_by_column_then_row ... ok
test linalg::lu::full_pivoting::factor::tests::pivot_search_keeps_the_smaller_row_across_accumulators ... FAILED
thread 'linalg::lu::full_pivoting::factor::tests::pivot_search_keeps_the_smaller_row_across_accumulators' (142640139) panicked at faer/src/linalg/lu/full_pivoting/factor.rs:912:21:
  left: (2, 1, 2.0)
 right: (0, 1, 2.0)
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 847 filtered out; finished in 0.00s
# green (fix in place)
test full_piv_lu_tie_order_depends_on_the_layout ... ok
test small_square_row_major_full_piv_lu_does_not_copy_the_matrix ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test linalg::lu::full_pivoting::factor::tests::pivot_search_breaks_ties_by_column_then_row ... ok
test linalg::lu::full_pivoting::factor::tests::pivot_search_keeps_the_smaller_row_across_accumulators ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 847 filtered out; finished in 0.02s
  • New, faer/tests/full_piv_lu_row_major_no_copy.rs. A test-only GlobalAlloc records the calling thread's largest single allocation during lu_in_place.
    • 64, 96 and 127 square, and a padded 96x96 row-major view (strides (100, 1)), must stay below m·n·8.
    • 128x128, 256x32, and 96x96 views with strides (2, 192) and (192, 2) (copy kept) must reach it, so the counter is shown to see the copy.
    • full_piv_lu_tie_order_depends_on_the_layout pins the tie cases above: 64×64 and 8×16 in both layouts, 256×32 row-major (copied, so column first), and the two within-column ties.
  • New, pivot_search_keeps_the_smaller_row_across_accumulators (unit test in factor.rs): every row pair of a 64-row column, through the plain search and the fused update at three alignments, so it holds whatever the lane count is.
    • Runs on Par::Seq and on a 4-thread team.
    • It is an integration test because the lib test binary already has a #[global_allocator] (sparse::linalg::amd).
  • Reused, linalg::tests::test_row_major_adaptation_matches_the_column_major_one. Its full-pivot arm now asks the new guard whether the two orders must agree bit for bit (copy) or up to rounding (dual). Pivots and transposition counts must still match; they do because its fixtures hit no magnitude tie, which the test comment now says instead of "the same pivots". 127x127 is added.

Whole crate suite, cargo test -p t4a-faer --no-fail-fast, on this host (Apple M5 Max), summed over the test binaries:

revision passed failed ignored failing test
63790b0 (head) 958 1 163 lq_allocates_nothing_60x300
b16d6b8 (base, run once on 2026-11-09 from a separate worktree and target dir) 955 1 163 lq_allocates_nothing_60x300

The head's failure reports allocations [13, 12, 17, 17] (expected all zero); the base log kept only the test names and totals, not the counts. It is one of the host allocation failures recorded in docs/worklogs/2026-11-04-gevd-repeated-complex-pairs.md; the hosted CI run passes it. The +3 at head are this PR's tests.

failing / summary lines, head and base
# head 63790b0
test lq_allocates_nothing_60x300 ... FAILED
test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s
# base b16d6b8
test lq_allocates_nothing_60x300 ... FAILED
test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s

Formatting: the repo is not cargo fmt-clean under stable or nightly, so the changed lines were formatted by hand to rustfmt.toml (max width 80, tabs) and the new test file with nightly rustfmt; no file outside the change was reformatted.

Red check, with only the !(m == n && n < 128) clause removed from the guard:

test small_square_row_major_full_piv_lu_does_not_copy_the_matrix ... FAILED
64x64: an allocation of 32768 bytes, the whole matrix is 32768
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

Red check for the stride condition, with only col_stride == 1 && removed:

96x96, strides (2, 192): largest allocation 96 bytes, the copy is 73728

The consistency test passes both with and without the clause, because both paths are correct; the red check belongs to the allocation test.

Not done: upstream faer was not measured. Padded and reversed row-major views ((100, 1), (-100, 1)) are routed to the dual, which the allocation test shows for (100, 1) and the adaptation test covers for reversed rows (correctness); their speed was not measured, and the timed cells are contiguous row-major only. No tie test runs on a thread team; the parallel step merges column blocks with a strict > (earlier block, smaller column), which is read from the source, not tested. The dual kernel was not changed: a contiguous pivot-row scaling would plausibly close the square gap above 128, but that is not measured.

🤖 Generated with Claude Code

…ot dual (#314)

Full-pivoting `lu_in_place` took the shared row-major copy from 64 * 64
elements up, ahead of its own transpose dual. On square matrices below 128
the dual runs within 1.05x of a column-major input (f64 64x64 one thread:
1.01x on Apple M5 Max, 1.03x on Zen 5) while the copy costs 1.24x / 1.32x,
so those shapes now skip the copy (`row_major_takes_the_copy`).

From 128 up the dual's row scaling walks a strided row every step (1024 one
thread: 0.22 -> 3.26 ms for that stage alone, the update and pivot search
equal), and on tall shapes the long swap turns strided and the trailing
block wide (1.4-2.1x), so every other shape keeps the copy.

Tests: `faer/tests/full_piv_lu_row_major_no_copy.rs` bounds the largest
single allocation below m * n * 8 on 64/96/127 square and sees the copy on
128x128 and 256x32; the full-pivoting arm of the row-major adaptation test
asks the new guard for bitwise versus rounding agreement and covers 127x127.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shinaoka

shinaoka commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

GPT-6.1-sol adversarial review

CRITICAL: none
NEEDS_CHANGES: 2
UNVERIFIED: 2

Critical (must not merge)

none

Needs changes

  • The exception also disables adaptation for generic-stride matrices — faer/src/linalg/lu/full_pivoting/factor.rs:628. Reproduce with a valid 96×96 view having element strides (row_stride, col_stride) = (2, 192): the base copies it into contiguous storage, whereas this head skips the copy, selects transpose = false at line 651, and reaches the noncontiguous fallback at lines 310–323, which performs a matrix update followed by a separate pivot search instead of the fused SIMD kernel. This is outside the stated row-major transpose-dual change; neither dimension is contiguous, so the measured justification does not apply. Restrict the new exclusion to the layout it was measured for, preserving the previous dispatch for generic strides. The dispatch change is source-verified; its timing impact was not measured.

  • The “same pivots” description omits an observable tie-breaking change — faer/src/linalg/mod.rs:841–842. Reproduce with a zero-filled 64×64 f64 matrix, set A[(1, 2)] = A[(0, 4)] = 2.0, and factor both layouts using Par::Seq and max_rank = Some(1): the column-major search chooses (1, 2), requiring two transpositions, while the transpose dual chooses original coordinate (0, 4), requiring one. The source’s strict-maximum searches establish this case without rounding ambiguity. Issue Row-major parity: run every dense factorization natively on row-major input via transpose duality (no whole-matrix copy), at column-major performance #314 explicitly permits different pivot sequences on ties but requires documenting the behavior; replace the unconditional “same pivots” wording and document the layout-dependent tie order rather than implying the hashed-fixture comparison establishes general equivalence.

Unverified claims

  • The entire selected size range satisfies the performance criterion — docs/worklogs/2026-11-09-full-piv-lu-row-major-dual.md:10–18 and faer/src/linalg/lu/full_pivoting/factor.rs:579–581. The listed measurements sample particular sizes; they do not establish that every square size through 127 passes on both machines. In particular, 127 is added to correctness/allocation tests, not to the reported timing scan, which ends at 120. Reproduce the evidence gap by comparing the guard’s accepted sizes with the linked comment’s tables and boundary scan. The blanket range statement is unverified, not a demonstrated performance failure.

  • The submitted head meets the repository’s performance-adoption requirements — docs/worklogs/2026-11-09-full-piv-lu-row-major-dual.md:56–68. The recorded benchmark used b16d6b8 plus an uncommitted bypass switch, not clean head 033e346; the commands also require an uncommitted harness patch. Checking out the named base and setting FAER_FLU_COPY=off alone therefore cannot reproduce arm C because the switch implementation is absent from that revision. This does not establish clean-head acceptance under docs/adoption-protocol.md:36–59. The exact local patches and raw per-sample artifacts cannot read from the supplied diff or linked measurement comment; benchmark acceptance remains unverified.

Checked and fine

none

Not checked: runtime tests, the reported mutation run, or benchmark replay. At inspection, head 033e346 still had the CI build and coverage-test jobs in progress; completed checks did not establish their results.


Reviewed head: 033e346980752b5e2838122f5c5d82c749400dff at 2026-11-09T09:13:37Z. This review is valid only for that commit: if the head has moved, it is a review of a tree that is not being merged.
Automated (gpt_review.sh). This is an enumeration of counterexamples, not a verdict; the merge decision stays with a human.

shinaoka and others added 2 commits November 9, 2026 18:28
…dent tie order (#314)

Review of #342. The square n < 128 exclusion from the row-major copy also
applied to views with neither stride 1, e.g. 96x96 with strides (2, 192):
those skipped the copy and fell to the separate matrix update and pivot
search instead of the fused SIMD kernel. `row_major_takes_the_copy` now
takes the strides and skips the copy only when the rows are contiguous
(col_stride == 1); every other stride keeps the previous dispatch.

The transpose dual breaks a magnitude tie by the smallest row first, the
column-major search by the smallest column, so on ties the pivots and the
transposition count differ by layout (zero 64x64 with A[(1,2)] = A[(0,4)],
max_rank 1: (1,2) with two transpositions vs (0,4) with one).
`lu_in_place` documents it under "Pivot ties" and a test pins the case;
the adaptation test no longer says "the same pivots" unconditionally.

Tests: the allocation test adds 96x96 with strides (2,192) and (192,2)
(copy kept; fails at 96 bytes with the old guard) and a padded row-major
view (100,1) (no copy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d the clean-head run

The worklog and PATCHSET said square 64..=127 passes on both machines; the
timed sizes are 64, 80, 96, 112, 120 and 127 on Apple M5 Max and 64, 96 on
Zen 5, the rest is interpolated. The earlier run measured b16d6b8 plus an
uncommitted copy-bypass switch; the record now adds a paired run of the
committed 32c615f source against b16d6b8 (f64/c64, 1T/4T, three repetitions:
dual 0.89-1.02x a column-major input on the 24 changed cells) and names the
switch run as not reproducible from a revision. The lu_in_place tie note no
longer says the factors differ only on ties.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shinaoka

shinaoka commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

GPT-6.1-sol adversarial review

CRITICAL: none
NEEDS_CHANGES: 2
UNVERIFIED: 2

Critical (must not merge)

none

Needs changes

  • The documented tie-breaking guarantee is false even inside the newly enabled dual path — faer/src/linalg/lu/full_pivoting/factor.rs:621–627: on the local Apple SIMD build, factor a zero row-major 64×64 f64 matrix with A[(2,6)] = A[(2,8)] = 2, max_rank = Some(1), and Par::Seq; the first pivot is (2,8), not the promised (2,6). The inherited SIMD search merges four accumulators using strict score comparisons at factor.rs:123–127, discarding an equal-valued smaller index before reduce_2d can resolve the tie. The new test at faer/tests/full_piv_lu_row_major_no_copy.rs:172–221 does not exercise this case. Either narrow the documented guarantee or fix the accumulator reductions, including the three-accumulator update search at factor.rs:162–165, and add a regression case.

  • “Every other input” does not use column-then-row ordering — faer/src/linalg/lu/full_pivoting/factor.rs:624–629: reuse the test’s two nonzero entries, but factor an 8×16 row-major matrix with max_rank = Some(1) and Par::Seq; the first pivot is (0,4) with one transposition, whereas column-major chooses (1,2) with two. Although this matrix is neither square nor covered by the new exclusion, it bypasses the copy and reaches the existing transpose branch at factor.rs:684–703. Document the actual direct-versus-transposed dispatch rather than treating the new copy exclusion as the complete condition for layout-dependent ties.

Unverified claims

  • Column-major speed for every admitted row stride — faer/src/linalg/lu/full_pivoting/factor.rs:579–590,657–659: the guard admits padded and reversed row-major views, but the reported timing cells do not establish performance for those layouts; the padded allocation test establishes dispatch only. Reproduce the comparison against the base revision for 96×96 views with strides (100,1) and (-100,1), then compare their factorization times with the column-major control. The claimed speed equivalence for these admitted layouts remains unverified, not a demonstrated regression.

  • The local full-suite failure also occurs on the base revision — docs/worklogs/2026-11-09-full-piv-lu-row-major-dual.md:77–80: I did not independently reproduce the reported 957-pass/1-failure result or the corresponding base run. Reproduction requires running cargo test -p t4a-faer --no-fail-fast on both 32c615f and b16d6b8 on the affected host and comparing lq_allocates_nothing_60x300; the green hosted run does not establish that claim, because that test passed there.

Checked and fine

Run Tests run 42598027009 targets head 17270db2308c944e4aa82fa3e2b223a7331d0dbe: 883 tests passed, 162 skipped; both new tests and the adaptation test actually ran.
Not checked: fresh Cargo builds, full local suites, or benchmark reruns; the pivot reproductions linked the existing local PR-worktree build, so fresh-build provenance remains unchecked.


Reviewed head: 17270db2308c944e4aa82fa3e2b223a7331d0dbe at 2026-11-09T10:33:28Z. This review is valid only for that commit: if the head has moved, it is a review of a tree that is not being merged.
Automated (gpt_review.sh). This is an enumeration of counterexamples, not a verdict; the merge decision stays with a human.

@shinaoka

shinaoka commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

GPT-6.1-sol adversarial review

CRITICAL: none
NEEDS_CHANGES: 3
UNVERIFIED: 1

Critical (must not merge)

none

Needs changes

  • The documented tie order fails on the newly enabled path — faer/src/linalg/lu/full_pivoting/factor.rs:620–627. On this aarch64 host, factor a zero row-major 64×64 f64 matrix with A[(0,2)] = A[(0,8)] = 2, max_rank = Some(1) and Par::Seq: the first pivot is (0,8), not the documented (0,2); the column-major call returns (0,2). The inherited SIMD accumulator merge at lines 128–133 uses strict score comparisons and can discard the smaller index before reduce_2d sees it. This PR newly exposes that behavior to these row-major inputs, while its new test only exercises ties across searched columns. Cover ties within one searched column and either enforce the stated ordering or document the actual behavior.

  • The documentation misclassifies inputs that already use the transpose dual — faer/src/linalg/lu/full_pivoting/factor.rs:624–629. Repeat the PR’s two-entry example on a 64×128 matrix: column-major returns (row, column, transpositions) = (1,2,2), but row-major returns (0,4,1), contradicting “Every other input … uses the column-then-row order.” I also reproduced this at 4×8. Wide matrices bypass the copy and reach the transpose branch at lines 693–713 regardless of the new square exception. Describe tie orientation using the actual copy-and-stride dispatch, rather than only the newly changed square population.

  • The native row-major path still emits the generic-stride warning — faer/src/linalg/lu/full_pivoting/factor.rs:647–655. Build with --features perf-warn, install a logger accepting warnings, and factor a contiguous row-major 64×64 identity matrix: it emits Found matrix with generic strides. because col_stride.abs() == 1 makes the warning condition true. This is inherited behavior, but it remains on the newly enabled native path and misses Row-major parity: run every dense factorization natively on row-major input via transpose duality (no whole-matrix copy), at column-major performance #314’s explicit warning-removal acceptance criterion. Restrict this warning to genuinely generic strides and add a logging regression check.

Unverified claims

  • Performance acceptance across the guard’s population — docs/worklogs/2026-11-09-full-piv-lu-row-major-dual.md:17–26,82–101. The reported table’s medians and ranges recompute correctly, but they do not establish the claimed acceptance rule for every newly dispatched size and stride. The worklog explicitly identifies unmeasured sizes, reports unchanged-code controls reaching 1.181×, and states that candidate_compare.py was not run. The required improvement/non-regression result in docs/adoption-protocol.md:59–66 therefore remains unverified; reproduce it with the prescribed baseline/candidate comparison, including its noise checks.

Checked and fine

  • Local focused commands returned 2 integration tests passed and 7 library tests passed.
  • CI run 42598027009 names head 17270db2308c944e4aa82fa3e2b223a7331d0dbe: 883 passed, 162 skipped; both new integration tests and the adaptation test executed.
  • After the initial verification timed out, kache doctor --repair --verify completed: 61,935 valid entries, zero corrupted.

Not rerun: the full local suite, mutation checks, or benchmark timings. This review targets 17270db; concurrent uncommitted edits were excluded.


Reviewed head: 17270db2308c944e4aa82fa3e2b223a7331d0dbe at 2026-11-09T10:35:08Z. This review is valid only for that commit: if the head has moved, it is a review of a tree that is not being merged.
Automated (gpt_review.sh). This is an enumeration of counterexamples, not a verdict; the merge decision stays with a human.

shinaoka and others added 2 commits November 9, 2026 19:45
…accumulators (#314)

Second review of #342. The SIMD full-pivot search walks each column with four
interleaved accumulators (three in the fused update) and merged them with a
strict >, so an equal maximum held by the accumulator merged first won even
with the larger row: zero 64x64 row-major with A[(2,6)] = A[(2,8)] = 2 and
max_rank 1 pivoted on (2,8) on a two-lane build. best_score now also takes
the other accumulator on an equal score with a smaller row. Inherited from
upstream; it changes the tied pivot for every layout, to the order the
scalar search and reduce_2d's comment already describe.

The lu_in_place "Pivot ties" section described the copy exclusion as the
whole condition. It now states the dispatch: the copy when
row_major_takes_the_copy holds, else the input when |row_stride| <
|col_stride| and its transpose otherwise (an 8x16 row-major matrix is never
copied and picks (0,4), column-major (1,2)).

Tests: pivot_search_keeps_the_smaller_row_across_accumulators (every row
pair of a 64-row column, plain and fused search, three alignments; failed at
rows (0,2) before the fix) and the integration tie test (8x16, 256x32 copied,
and the within-column cases; returned (2,8,2) before the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uite run

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@shinaoka shinaoka self-assigned this Nov 9, 2026
@shinaoka

shinaoka commented Nov 9, 2026

Copy link
Copy Markdown
Member Author

GPT-6.1-sol adversarial review

CRITICAL: none
NEEDS_CHANGES: 1
UNVERIFIED: 2

Critical (must not merge)

none

Needs changes

  • The new complex-valued tie contract is false — faer/src/linalg/lu/full_pivoting/factor.rs:641–647. Reproduce: factor a zero 16×16 c64 matrix with A[(0,1)] = 3+4i, A[(2,1)] = 5+0i, max_rank = Some(1), and Par::Seq; both entries have magnitude 5, but on Apple M5 Max the contiguous column-major view returns pivot (2,1) instead of the documented (0,1), while a view of the same values with element strides (2,32) returns (0,1). The SIMD search scores individual real/imaginary components (faer-traits/src/lib.rs:4208–4229), whereas the fallback uses their absolute-value sum (factor.rs:285–302), so fixing accumulator ties does not establish the stated SIMD/scalar rule for complex inputs. This scoring discrepancy is inherited; the unconditional documentation is new. Document the actual scoring distinction and add a mixed-component complex regression test, or make the searches satisfy the promised rule.

Unverified claims

  • The submitted implementation meets the performance acceptance criterion — docs/worklogs/2026-11-09-full-piv-lu-row-major-dual.md:104–126. The timings cover 32c615f, not the submitted implementation containing the additional best_score comparisons. Reproduce the evidence gap with git diff 32c615f 809eaa2 -- faer/src/linalg/lu/full_pivoting/factor.rs: the executed search changed after measurement. The PR acknowledges this; its table therefore does not establish the final implementation’s ≤1.05× result. Intermediate sizes and padded/reversed layouts are also explicitly unmeasured.

  • Hosted CI establishes that the allocation failure passes on this head — docs/worklogs/2026-11-09-full-piv-lu-row-major-dual.md:97–102, .github/workflows/run-tests.yml:123–124. At inspection, run 42604051892, whose head_sha is 809eaa2991a0d11d936ce5ad11b4243e8eaa42c1, still had Collect coverage data in progress; the successful MSRV jobs run cargo check, not this test. Reproduce with gh api repos/tensor4all/faer/actions/runs/42604051892/jobs. The reported historical pass is not established by that unfinished run.

Checked and fine

  • At the reviewed checkout: the integration command passed 2 tests; the focused library command passed 8, with 841 filtered out.
  • kache doctor --repair --verify: 61,997 valid entries, zero corrupted entries; the stale GCC-shim warning remains.

Not checked: the full crate suite, fresh performance measurements, or completion of hosted CI. The complex pivot-contract mismatch was reproduced locally.


Reviewed head: 809eaa2991a0d11d936ce5ad11b4243e8eaa42c1 at 2026-11-09T10:55:10Z. This review is valid only for that commit: if the head has moved, it is a review of a tree that is not being merged.
Automated (gpt_review.sh). This is an enumeration of counterexamples, not a verdict; the merge decision stays with a human.

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.

1 participant