Skip to content

並列ゲートの dispatch cost が幅 6 で頭打ち、通れば常に全幅: p=32〜64 で floor 付近の region が過剰に開く #91

Closed

Description

@shinaoka

問題

fork の並列 floor はすべて estimate >= PAYS(3) * sections * dispatch_cost(width) で判定しています。この cost モデルには、高並列 (p = 32〜64) で次の二つの前提の崩れがあります。

  1. dispatch cost を 6 より広い幅で測っていない。 utils/cost.rs:196 の width_model は幅 2/4/6 の 3 点を線形補間し、6 以上は一定値を返します。x86_64-avx512 の表は 8 スレッドの Ryzen AI 9 HX 470 (boost off) で測ったもので、outside 8.7 us、in-pool 3.5 us です (cost.rs:154-190)。p = 64 の 2 ソケット/複数 CCD のホストでも、同じ 3.5〜8.7 us で判定します。
  2. 判定が通ったら常に p 全体で走る。 pays_full / par_pays (cost.rs:740, :910) は「全幅か Seq か」の 2 択で、Rayon 経路には仕事量に応じた幅の選択 (width_for_by) がありません。Team 経路の一部 (kernel_width) だけが幅を選びます。
    • その結果、floor をわずかに超えた region (f64 GEMM でおよそ 74^3、serial で約 26 us) を p = 64 で開くと、1 スレッドあたりの仕事は約 0.4 us になります。
    • 実際の起床・barrier コストは CCD をまたぐと p とともに増えるはずなので (推測、未計測)、floor 付近の region は p = 64 で serial より遅くなりえます。

影響範囲は、PATCHSET の "Parallel floors from the dispatch cost" / "remaining regions" / "join regions and the block apply" / "Architecture keyed serial rates and Rayon costs" が置き換えたすべてのゲートです。代表例:

  • native GEMM (matmul/mod.rs:1473、spicy_matmul): セクション数を 1 と数えていますが、カーネル内では kc ブロックごとに pack と job queue の 2 セクションが回ります (2·⌈k/kc⌉、カーネル内部は 0.1.22 からの推測)。
  • LU 部分ピボットの trailing update (lu/partial_pivoting/factor.rs:107-129)
  • bidiag / tridiag / Hessenberg の 1 step 1 region
  • full-pivoting LU、column-pivoting QR、Bunch–Kaufman の 1 step 1 region。fork の floor は upstream の固定値より低く、p = 64 では 1 スレッドあたり数百要素の step で barrier を払います。

upstream 0ecd846 も固定の要素数閾値 (p を無視) なので、「p を見ない」点は同じです。fork では floor を 8T の計測に合わせて下げたぶん、高並列での影響が強くなっています。OpenBLAS は小さなサイズでスレッド数を作業量に比例させて減らします (記憶による。コードは未確認)。

確認済み / 推測

  • 確認済み (コード): 幅 6 以上で一定であること、pays_full が全幅であること、Rayon 経路に幅の選択がないこと、測定ホストが 8 スレッドであること。
  • 推測: 64 幅の実 dispatch cost の値、floor 付近の region が p = 64 で serial より遅くなること。
  • 未読: threadteam の DispatchCosts::host_default はローカルに無く読めていません。cost.rs のコメントでは同じ表を mirror しています。

計測計画 (faer#90 の計測が終わってから。broccoli で計測中は走らせない)

  1. 空 section の dispatch cost を幅 2,4,6,8,16,32,64 で計測します。outside、spindle::with_lock 内、Par::Team の 3 経路、ピン留めあり/なし。これは他の子 issue すべての前提になります。
  2. floor 付近で 1/8/16/32/64T のスケーリングを測ります。par-trace で判定と実時間を照合し、p を増やして遅くなる区間を探します。
    • GEMM: f64/c64 の m=n=k ∈ {64, 96, 128, 192, 256}
    • LU 部分ピボット: F order の f64 128〜1024
    • eigh / svd_values: 128〜320
    • full_piv_lu / col_piv_qr: 256/384/512

推奨

計測で確認してから修正します。修正は次の二本です。

  • (a) anchor を実測した 8〜64 幅まで伸ばす (ホストクラスではなく実測テーブルにする)
  • (b) Rayon 経路でも幅を min(p, estimate / (PAYS · cost)) で絞る

親: #95

🤖 Generated with Claude Code

Activity

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

    @shinaoka
    MemberAuthor

    Design note for the fix (monitoring side, 2026-10-29): after #97 only the kernel path picks a narrower width. team::kernel::kernel_width searches 2..=size for the widest width that still pays on the extrapolated cost and runs on a sub-team of that width (with_fork; the other members stay parked). The other gates (par_pays_time call sites in the factorizations, for_each regions) are all-or-nothing at par.degree(): either the full team or sequential. The fix for this issue should give every region the same choice through one helper (widest paying width, then a sub-team of that width), so a region whose work only pays at 4 or 8 participants runs there instead of opening all 64 or none.

  3. shinaoka commented on Oct 29, 2026

    @shinaoka
    MemberAuthor

    Item 1 measured on broccoli (AMD EPYC 7702P, Zen2, 64 cores, SMT off, NUMA 1, 16 MiB L3 per 4-core CCX), boost 0, governor performance

    Measurement only, nothing changed. 2026-10-29 09:40-10:05 JST. Two instruments, both dispatching through
    the crates the library itself uses:

    • dprobe (untracked probe): one empty section, back to back, 1000 iterations after 300 warm-up,
      through spindle::for_each_raw inside spindle::with_lock (held), a caller-owned Rayon pool
      entered with install (outside), and TeamExec::run on a threadteam team (team). The process
      is pinned to a whole-CCX mask of exactly the section's width, excluding the observer CCX (the
      campaign's convention: the caller thread is one of the participants).
    • threadteam's own benches/dispatch_cost.rs (the Linux port from faer-benchmark
      origin/bench/awake-team-linux, unchanged, TT_POOL=64 TT_WIDTHS=…), i.e. the method the existing
      WidthCosts anchors came from
      : back-to-back sections whose shares carry 10 µs of work, the section
      cost being the per-call time minus those 10 µs. Pool 64, process on CPUs 0-63, nothing else on the
      host.

    1. Empty section, µs (median; the three caller routes)

    width outside (install) held (in pool) team (TeamExec::run)
    2 4.49 0.78 0.69
    4 4.61 1.03 0.83
    6 5.49 2.78 (anomaly, see below)
    8 6.74 3.17 1.92
    12 9.88 4.51 2.11
    16 13.22 6.01 2.58
    24 20.27 7.63 3.94
    32 29.71 8.76 5.34
    48 62.92 12.14 4.64
    60 78.61 14.00 9.44
    64 81.19 15.04 8.91

    2. Team section with 10 µs shares, µs (threadteam's instrument, the WidthCosts method)

    width 2 4 6 8 12 16 24 32 48 64
    team, load − 10 µs 1.42 1.99 2.33 2.74 3.56 4.08 4.29 4.84 6.40 9.33
    team, empty (hot) 1.33 2.11 2.53 2.95 3.93 4.58 4.62 5.04 6.04 10.33

    What this says about the premise

    Cost is not flat above width 6. Empty-section cost grows 14.6x from width 4 to 64 in the pool
    (1.03 → 15.04 µs), 17.6x from outside (4.61 → 81.19), and the team grows 6.7x from width 2 to 64
    (1.42 → 9.33 µs with the anchor method). The team row is sublinear but nowhere near flat
    (9.33 µs at 64 against 2.33 at 6).

    And the model this host actually uses is the flat generic row. Zen2 has AVX2+FMA and no
    AVX-512F, so HostClass::detect gives X86_64Avx2, which takes [15 µs; 3] in pool and
    [40 µs; 3] outside at every width (utils/cost.rs:172-194; threadteam::DispatchCosts:: host_default() agrees on this host). Against the measurements that is:

    width in-pool measured model outside measured model
    4 1.03 15 4.61 40
    8 3.17 15 6.74 40
    16 6.01 15 13.22 40
    32 8.76 15 29.71 40
    64 15.04 15 81.19 40

    So at mid widths the floor is 2.5-15x too conservative (regions that would pay stay serial on this
    host), and at 64 the in-pool row happens to land on the truth while the outside row is 2x too low
    (over-splitting) — the issue's concern shows up at the top end, and its opposite at the bottom end.
    The 64-wide direction also confirms the per-thread share worry: a 6-wide section's cost is used as
    the 64-wide cost, so a region whose serial estimate is ~45 µs passes the gate at 64 threads and gives
    each of 64 participants under a microsecond.

    An observation I am not concluding from

    The TeamExec route intermittently shows a ~0.62 ms section instead of the ~1-9 µs in the tables:
    same width, same pinning, run to run (widths 6, 32, 60 at different attempts, and reproducibly at
    width 6 in one configuration), with a bimodal distribution (p10 ≈ 623 µs, p90 ≈ 646 µs in one
    sample; 1.6 µs in the next run of the same cell). A fresh Team::exec(w).team(…) per call is what
    the probe pays and what a faer caller pays; threadteam's own bench, which reuses one Exec, never
    shows it. It looks like a wake/park path (the tree-wake note in the campaign's awake-team record
    quotes 55-60 µs per parked core), but I have not attributed it and it needs its own measurement
    rather than a guess. It is reported here because a caller that pays it would see 0.6 ms per section.

    Scope

    Items 2 (floor-adjacent scaling with par-trace) is not measured here. No code changed; the
    probes are untracked in faer-benchmark/probes/hh91/. The row data for
    tensor4all/threadteam-rs#6 is posted there.

  4. 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