Skip to content

fork の並列化の高並列 (32〜64 スレッド) スケーラビリティ監査 #95

Open
2 of 6 issues completed

Description

@shinaoka

目的

fork 独自の並列化・分割・閾値・スレッド配分の変更が、高並列 (p = 32〜64 以上、複数 CCD/NUMA) で破綻しないかを洗い直します (メンテナ指示)。

きっかけは #90 です。block Householder apply の列分割では、列タスクごとに V 全体を読み直すので、V の読み込み量が p に比例します。8T の Zen5c でもすでに 8〜16% 遅いセルがあります。

方法

評価の観点

  1. 共有オペランドの読み込み量が p に比例する
  2. p 個の部分バッファを逐次に合算する
  3. タスク数の固定上限や、p と無関係な分割数で使われないスレッドが出る
  4. 粒度が細かすぎる、または閾値が p を考慮しない
  5. 同期・競合が p に比例する
  6. NUMA/CCX をまたぐ共有データ

結果表

「確認」の列: 確 はコードで確認済み、推 は推測 (未計測)。

場所 / PATCHSET 行 分割の形 p に対するコスト 危険度 upstream / OpenBLAS との違い 推奨 確認 issue
householder.rs:654-797 一般形 block apply の列分割 / "Thread-budget column split" n_tasks = min(p, K) の列タスクが、各自 V^H C_j と C_j -= V W_j を実行 V の読み込み ≈ 2·p·m·k。部分和はなし 高 upstream は min(p, K, 4) 列タスク × 内側 p/4 で、V の読み直しは最大 4 倍。OpenBLAS (dlarfb → BLAS-3) は V を 1 回 pack して共有 修正 確 (8T で実害あり) #90
utils/cost.rs の全ゲート / "Parallel floors …" 3 行、"Architecture keyed …" 判定は estimate ≥ 3·s·cost(min(p,6))、通れば全幅 floor が p によらず一定、1 スレッドあたりの仕事は estimate/p、実際の dispatch cost は p とともに増える 中〜高 (全体の土台) upstream は固定の要素数閾値 (p 非依存) で、fork は 8T 計測に合わせて floor を下げた。OpenBLAS はサイズに比例してスレッド数を減らす 計測 → 修正 確 (表と判定) / 推 (64 幅のコスト) #91
linalg/mod.rs:77 copy_across_layouts、matref.rs:1631 to_owned / "Row-major QR/LU/col-piv QR", "Row-major to_owned" 列を p 分割、ゲートなし。to_owned は Seq 書き戻しの false sharing は ncols/p < 8 (f64) で発生。直列 first-touch は O(mn) 中〜高 upstream にはこの経路がない 修正 確 (分割・ゲートなし) / 推 (実害) #92
evd/tridiag.rs:390-461 n × p の部分和を逐次合算 合算 p·rem 対 並列部 rem²/2p。損益分岐 p* ≈ √(rem/2) 高 (絶対性能) / fork 起因は低〜中 構造は upstream と同じで、fork は入口を下げた 計測 → 修正 確 / 推 (p*) #93
matmul/mod.rs:1000-1056 matvec_colmajor m × p の部分和を毎回確保して逐次合算 p·m 中 upstream 構造。OpenBLAS の dgemv_n は行分割 計測 確 #93
sparse/linalg/matmul.rs:107 Seq でも m × p workspace を呼び出しスレッドが memset m·p (直列) 中〜高 upstream 構造。fork のゲートは幅を直していない 修正 確 #93
householder.rs:279-317 upgrade の deep 分岐 / "Row-split reduction in the compact-WY factor's deep half" 行を p 分割、n×n×p の部分和を逐次合算 合算 p·n²/2。rows ≥ 4p なので仕事の約 1/8 以下 低 fork 固有 問題なし (監視) 確 #93
matmul/triangular.rs:733 三角積の x86 カーネル ゲートなしで gemm(.., p) 小さな積でも p 全員が同期 中 upstream も同じ。fork の floor 統一で取りこぼし 修正 確 #94
matmul/mod.rs:1162 K==1 rank_update ゲートなしで列を p 分割 同期 ∝ p 低〜中 upstream 構造 修正 確 #94
native GEMM / spicy_matmul / "Native gemm thread budget in flops" 1 section と数えて p 全体を渡す 実際のセクション数は 2·⌈k/kc⌉ 中 upstream は 32³ MAC で無条件 (fork の方が保守的) 計測 確 (sections = 1) / 推 (カーネル内) #94
householder.rs:520-536 階数 1 の腕を一般形へ / "Threaded rank-one Householder apply" N=1 の apply を列分割 v (m 要素) の読み込みは p 倍だが小さい。タスク数は min(p, K) で、パネル幅 K ≤ 16 のため p = 64 では内側 p/K 低 upstream は N=1 で Seq 問題なし (#90 の修正で一緒に見直す) 確 #90
utils/mod.rs:164 for_each_with_par 内側の幅は p / n_jobs の切り捨て 最大 n_jobs - 1 スレッドが遊ぶ 低 fork の helper 問題なし (余りの配分は将来) 確 —
utils/mod.rs:35 join_raw の幅 / "Width split in join_raw" p - p/2 と p/2 正しい なし upstream は両側に p - p/2 (過剰) 問題なし 確 —
LU 部分ピボット trailing floor / swap stage 再帰レベルごとにゲート、swap は列幅比例の join_sized 判定は #91 に依存 低〜中 upstream は常に par。OpenBLAS getrf_parallel は look-ahead + 融合 #91 で計測 確 #91
bidiag / Hessenberg / full-piv LU / col-piv QR / BK の 1 step 1 region step ごとに p 幅の region n 回の barrier。fork の floor は upstream より低い 低〜中 LAPACK は BLAS-3 化 (laqps, labrd) #91 で計測 確 / 推 #91
D&C (bidiag SVD / tridiag EVD) の join と merge GEMM 2 分木で幅を半分ずつ 葉は約 n/128 (SVD) / n/64 (EVD) 個で、p > 葉数なら遊ぶ。secular は直列 中 (絶対性能) / fork 起因はなし upstream と同じ構造。fork はゲート追加でむしろ改善。LAPACK も gemm 外は直列 問題なし (将来の課題) 確 —
Schur, QZ, gen_hessenberg, sparse cholesky/lu/qr, Cholesky par の受け渡しのみ — なし — 問題なし 確 —

横断的な所見: NUMA/CCX を意識したスレッド配置や first-touch は、fork にも upstream にもありません。#92、#93 の直列確保・初期化がその典型です。

計測の順序 (#90 の計測が終わってから)

  1. 並列ゲートの dispatch cost が幅 6 で頭打ち、通れば常に全幅: p=32〜64 で floor 付近の region が過剰に開く #91-1: 空 section の dispatch cost を幅 2〜64 で計測 (outside / in-pool / Team)。他すべての前提です。
  2. row-major コピー (copy_across_layouts / to_owned): ゲートなし・列分割で書き戻しが false sharing・直列 first-touch #92: row-major コピー (perf c2c)
  3. p 個の部分バッファを逐次合算する reduction (tridiag, matvec_colmajor, 疎×疎, compact-WY upgrade) の高並列スケーリング #93: tridiag / eigh のスケーリング → matvec、疎×疎
  4. cost ゲートなしで全幅を渡す経路: 三角積の x86 カーネル、K==1 の rank_update、GEMM のセクション数過少計上 #94 と 並列ゲートの dispatch cost が幅 6 で頭打ち、通れば常に全幅: p=32〜64 で floor 付近の region が過剰に開く #91-2: floor 付近のスケーリング (GEMM、LU、Cholesky、EVD/SVD 前段)

いずれも 1/8/16/32/64T。broccoli (64 コア) でスケーリング曲線を取り、回帰ゲートは evo で campaign protocol (boost off) で取ります。

子 issue

🤖 Generated with Claude Code

Activity

  1. self-assigned this
    on Oct 28, 2026
  2. shinaoka commented on Oct 28, 2026

    @shinaoka
    MemberAuthor

    PR #52 (merge 5e45c57) の高並列監査

    git diff 5e45c57^1 5e45c57 を読みました。計測はしていません。

    結論: revert は不要で、前方修正で足ります。 #52 は、#90 の型 (各タスクが同じ大きなオペランドを p 回読み直す) も、新しい p 比例の部分バッファも持ち込んでいません。逆に、bidiag 本体の高並列での形はむしろ良くなっています。

    変更 並列の形 p に対するコスト 危険度 推奨 確認
    blocked bidiag (svd/bidiag.rs の bidiag_blocked_phase / _panel、dlabrd 型、パネル 16、残り 128 以下は unblocked) パネルごとに region_pays((m-p)(n-p), 2) で判定。各 step の積は 2 本 (y = A^H u は matvec_rowmajor の行分割、x = A r は matvec_colmajor)。補正項はすべて Par::Seq。trailing update はパネルごとに matmul 2 本 パネル内の積の数は旧 unblocked と同じで、列分割 for_each の fused 更新 (各タスクが長さ m のベクトル 3 本を読むので、ベクトルの読み込みは 3·m·p) が GEMM 2 本に置き換わった。X/Y の scratch は p に依存しない 低 (改善) 問題なし 確
    └ x = A r の matvec_colmajor m × p の部分和を毎回 MemBuffer::new で確保して逐次合算 合算 p·m 対 並列部 m·n/p。損益分岐 p ≈ √n 中 (#52 以前から: 旧 unblocked の z2 = A22 wk^H が同じ経路で、step あたりの呼び出し回数も同じ) #93 で扱う 確
    unblocked の tail (残り ≤ 128) 旧来の for_each(nthreads) 列分割をそのまま使用 tall (m = 4096 × 128) で p = 64 だと 1 タスク 2 列。ベクトルの読み込み 3mp が行列本体 128m を上回る 低 (構造は以前と同じで、適用範囲は tail のみに縮小) 計測で確認 確 / 推 (実害)
    upgrade_householder_factor の NARROW_BASIS (n ≤ 8 なら呼び出しスレッドで内積) 並列をやめて直列化 直列 rows·n²/2、列を最大 n 回読む。rows と p を見ない固定規則 低 (m = 4096 では計測で速い。m が 1e5 を超える tall で 64T だと直列部分が残る) 前方修正候補 (rows·n² の floor を足す) 確 / 推
    dqds (values-only、直列) 並列なし O(n²·sweeps) が直列の Amdahl 項。8T では threaded D&C より速い (worklog: n = 512 で 6.6 対 13.3 ms) 低 (LAPACK dlasq1 と同じ構造) 計測で確認 (64T・大きい n で D&C と逆転しないか) 確 / 推
    caller-held worker group (pub use spindle、crate doc、Par::Rayon doc) ライブラリ側のコード変更なし。ドキュメントで with_lock(n) を推奨 with_lock(64) だと、直列区間 (dqds、Amdahl 部) で最大 63 スレッドが一定時間 spin してから眠る 低 (助言のみ) 計測で確認 (spin が SMT の兄弟スレッドや他ソケットに与える影響) 推
    work_rows (列 stride のパディング) 並列に無関係 — なし 問題なし 確

    計測計画 (#90 の計測の後)

    🤖 Generated with Claude Code

  3. shinaoka commented on Oct 28, 2026

    @shinaoka
    MemberAuthor

    Plan for the children of this audit (monitoring side, maintainer delegated the order on 2026-10-29):

    1. Measurements first, no code: Block Householder apply re-reads the whole basis V once per column task; traffic grows with the thread count #90 scaling (running on broccoli), then 並列ゲートの dispatch cost が幅 6 で頭打ち、通れば常に全幅: p=32〜64 で floor 付近の region が過剰に開く #91's dispatch cost at widths 2 to 64 on broccoli. Every gate decision in row-major コピー (copy_across_layouts / to_owned): ゲートなし・列分割で書き戻しが false sharing・直列 first-touch #92 to cost ゲートなしで全幅を渡す経路: 三角積の x86 カーネル、K==1 の rank_update、GEMM のセクション数過少計上 #94 depends on 並列ゲートの dispatch cost が幅 6 で頭打ち、通れば常に全幅: p=32〜64 で floor 付近の region が過剰に開く #91's numbers.
    2. Implementation after threadteam is the only parallel route: drop Par::Rayon, the spindle path and the Rayon cost model #97 merges (it rewrites the dispatch layer these issues touch), one PR each, in this order: 並列ゲートの dispatch cost が幅 6 で頭打ち、通れば常に全幅: p=32〜64 で floor 付近の region が過剰に開く #91 (dispatch cost model and width choice), row-major コピー (copy_across_layouts / to_owned): ゲートなし・列分割で書き戻しが false sharing・直列 first-touch #92 (row-major copies: gate, write-back partitioning, first touch), cost ゲートなしで全幅を渡す経路: 三角積の x86 カーネル、K==1 の rank_update、GEMM のセクション数過少計上 #94 (ungated full-width paths, GEMM section count), p 個の部分バッファを逐次合算する reduction (tridiag, matvec_colmajor, 疎×疎, compact-WY upgrade) の高並列スケーリング #93 (O(p) partial-sum reductions; tridiag and matvec measured first), Block Householder apply re-reads the whole basis V once per column task; traffic grows with the thread count #90 (shared-V block apply: pack once or 2-D split).
    3. Evidence per PR: scaling at 1/8/16/32/64 threads on broccoli (EPYC 7702P, boost off, performance governor) with the OpenBLAS arm, plus the regression gate on evo (campaign protocol, boost off). Judge by mechanism per the Parallel Scaling rule; no constants tuned to one host.
    4. Implementation runs on primerose; measurements on broccoli and evo, never two measuring jobs on one host at once.
  4. shinaoka commented on Oct 29, 2026

    @shinaoka
    MemberAuthor

    The finding that lu 1024 does not scale (faer#99, broccoli comment, finding 2) is tracked as tensor4all/faer-benchmark#36, fix in tensor4all/faer-benchmark#37. The cause is the harness, not faer: since faer-benchmark#35 the fork lu and solve rows call lu_in_place with Par::Seq. faer's LU gates and team dispatch are parallel on main (counters in the issue). Not part of #91.

  5. shinaoka commented on Oct 29, 2026

    @shinaoka
    MemberAuthor

    Per-process serial mode at 16T (faer#99 re-measurement): tracked in #107 (probe on branch fix/team-serial-mode, broccoli 16T run requested).

  6. shinaoka commented on Oct 29, 2026

    @shinaoka
    MemberAuthor

    Follow-up from #109's evo gate: #111 (no heap allocation inside per-step parallel sections).

  7. shinaoka commented on Oct 29, 2026

    @shinaoka
    MemberAuthor

    MKL / OpenBLAS / fork at high thread counts on broccoli — the two questions

    oneAPI MKL 2026.2 is installed here and the campaign's link-mkl arm builds and runs (it links the
    explicit layers libmkl_intel_lp64 + libmkl_intel_thread + libmkl_core + libiomp5). Two notes on
    running it: mkl_set_num_threads() segfaults with this build on this host, so the budget has to be
    set through MKL_NUM_THREADS (read back through mkl_get_max_threads, which works); and MKL_VERBOSE=1
    reports oneMKL 2026 Update 2 ... Intel(R) Architecture processors, Lnx 1.59GHz lp64 intel_thread with
    Dyn:1 FastMM:1 NThr:8 on the call — the generic Intel-architecture path, no AVX-512 selection on
    Zen2, and nothing was overridden for the AMD host.

    Whole run: EPYC 7702P (Zen2, SMT off, boost 0, governor performance), fork 7e5e9e3, f64, per-cell
    whole-L3-domain idle guard, 2 process sets at 64T and 1 set elsewhere (another user's processes sit at
    5-9 % on single CPUs today, so wide cells cannot pass the 5 % gate and are recorded with the reading).

    (1) Small sizes: does the fork get much slower at high threads? (fast-mode ms, 64T/1T)

    op n faer 1T faer 64T faer 64T/1T MKL OpenBLAS
    gemm 32 0.0031 0.0031 0.98x 10.0x 1.04x
    gemm 128 0.1464 0.0187 0.13x 0.44x 0.33x
    lu 32 0.0138 0.0140 1.01x 1.00x 1.01x
    lu 128 0.2016 0.2817 1.40x 0.92x 1.00x
    qr_factor 32 0.0316 0.0322 1.02x 1.00x 1.00x
    qr_factor 128 0.4995 0.4652 0.93x 0.81x 1.84x
    cholesky 32 0.0049 0.0049 0.99x 0.99x 1.02x
    cholesky 128 0.0929 0.0950 1.02x 1.00x 1.38x

    No small size blows up. Seven of the eight cells are 0.93-1.02x, i.e. 64 threads cost what one
    thread costs; the one cell over 1.2x is lu 128 at 1.40x (0.20 -> 0.28 ms), which is visible but
    is 0.08 ms. For scale: MKL's small-size behaviour is far worse than ours at n=32, where its threading
    overhead alone makes gemm 10x slower than at 1T (0.0067 -> 0.10 ms), and OpenBLAS manages 1.84x on
    qr 128. At these sizes the fork is the most thread-insensitive of the three.

    One caveat that matters more than any number above: the raw medians showed gemm 128 at 40x slower
    at 64T (0.147 -> 6.0 ms). That is the #107 per-process mode, not a size effect — its repetitions went
    0.019, 0.019, 15.98, 11.98, 15.98, 0.022, 0.021 ms, i.e. the mode inflates a cell by up to 800x and
    it hits small sizes as hard as large ones. Every number in these tables is the fast level (fastest
    repetition per process, fastest process per cell); the mode hit 6 of the 64T cells here.

    (2) Large sizes: does the fork scale like MKL? (faer/MKL = speedup ratio; <0.8 marked)

    op n T faer ms faer x MKL ms MKL x faer/MKL
    gemm 2048 16 44.87 14.8x 47.17 13.0x 1.14
    gemm 2048 32 25.96 25.6x 41.36 14.8x 1.73
    gemm 2048 64 20.24 32.8x 25.98 23.6x 1.39
    gemm 4096 16 364.12 14.5x 331.64 14.4x 1.01
    gemm 4096 32 192.61 27.5x 172.83 27.7x 0.99
    gemm 4096 64 224.81 23.5x 161.73 29.6x 0.80
    lu 2048 16 47.37 5.4x 26.28 10.5x 0.51
    lu 2048 32 41.08 6.2x 20.51 13.4x 0.46
    lu 2048 64 44.30 5.7x 39.13 7.0x 0.82
    lu 4096 16 229.60 8.8x 160.48 12.3x 0.71
    lu 4096 32 178.78 11.2x 120.19 16.4x 0.68
    lu 4096 64 451.97 4.4x 163.20 12.1x 0.37
    qr 2048 16 59.59 10.3x 60.64 10.7x 0.96
    qr 2048 32 47.78 12.9x 81.31 8.0x 1.61
    qr 2048 64 60.44 10.2x 67.66 9.6x 1.06
    qr 4096 16 513.75 9.4x 338.42 21.1x 0.45
    qr 4096 32 264.69 18.3x 208.85 34.2x 0.53
    qr 4096 64 201.34 24.0x 212.07 33.7x 0.71
    cholesky 2048 16 24.58 7.8x 23.60 7.4x 1.05
    cholesky 2048 32 16.35 11.7x 17.83 9.8x 1.20
    cholesky 2048 64 256.77 0.7x 14.21 12.3x 0.06 (mode, unusable)
    cholesky 4096 16 288.63 7.7x 169.12 9.5x 0.81
    cholesky 4096 32 157.58 14.1x 90.82 17.7x 0.79
    cholesky 4096 64 70.39 31.5x 35.15 45.8x 0.69

    Where we match or beat MKL: gemm everywhere, qr at 2048, cholesky at 2048. gemm 2048 is 1.1-1.7x
    better than MKL's scaling and gemm 4096 is parity (1.01 / 0.99 / 0.80).

    Where we do not: lu at every size (0.46-0.82, and 0.37 at 4096/64T: 4.4x against MKL's 12.1x —
    this is exactly the subject of #123), qr at 4096 (0.45-0.71), and cholesky at 4096 for
    32/64T (0.79/0.69). OpenBLAS is far off on everything except gemm, so MKL is the right yardstick here.

    The cholesky 2048 64T cell is listed for completeness only: every repetition there stayed in the #107
    mode (256 ms against 16 ms at 32T), so it says nothing about scaling. Nothing was measured on a
    partition this run.

    Raw: per-cell CSVs and the two drivers (fbench for the small grid, three standalone drivers for the
    large one, since the harness's own reference check costs 131 s at n=2048 and cannot be used there).

  8. shinaoka commented on Oct 30, 2026

    @shinaoka
    MemberAuthor

    Folded into the umbrella #168 (parallel structure of the factorizations). This issue is re-scoped or closed against its items.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions