Skip to content

Pass the output type of tforeach positionally - #177

Merged
maleadt merged 2 commits into
JuliaFolds2:masterfrom
maleadt:tb/specialize-outputtype
Oct 26, 2026
Merged

maleadt merged 2 commits into
JuliaFolds2:masterfrom
maleadt:tb/specialize-outputtype

Conversation

@maleadt

@maleadt maleadt commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

tforeach calls tmapreduce(...; kwargs..., init = nothing, outputtype = Nothing). Passed through keyword arguments, Nothing is stored in the keyword NamedTuple as a DataType, so _tmapreduce gets compiled with OutputType unknown: for every output type at once. A scheduler implementation that branches on OutputType === Nothing then compiles its reduction path for every tforeach body too.

tforeach now calls a positional entry point, tmapreduce_positional, which tmapreduce shares, with Nothing as a constant. Behaviour doesn't change: tforeach still ignores init and outputtype.

With ThreadTeams.jl's team scheduler this takes a new loop body's first tforeach from about 100 to 80 ms of compilation.

This also fixes ThreadTeams.jl's Enzyme tests on Julia ≥ 1.12, which 0.8.8 broke (a CallingConventionMismatchError in its Enzyme rule for run_chunks; they pass with this PR).

@codecov-commenter

codecov-commenter commented Oct 21, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.86%. Comparing base (cba9127) to head (f0e43eb).
⚠️ Report is 162 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #177      +/-   ##
==========================================
+ Coverage   90.24%   92.86%   +2.62%     
==========================================
  Files           3        9       +6     
  Lines          82      729     +647     
==========================================
+ Hits           74      677     +603     
- Misses          8       52      +44     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@MasonProtter

MasonProtter commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

So one potential side-effect of this is that it slows down the serial fallback path when T is runtime resolved, because it introduces a dispatch layer that was previously skipped.

e.g. here's what I see

using OhMyThreads, BenchmarkTools
tref = Ref{DataType}(Float64)   # concrete type; the value is only known at run time
xs1 = 1:1; xs = 1:1000

@benchmark tmapreduce(sin, +, $xs1; outputtype = $tref[])   # serial, run-time T
@benchmark tmapreduce(sin, +, $xs;  outputtype = $tref[])   # parallel, run-time T
@benchmark tmapreduce(sin, +, $xs;  outputtype = Float64)   # parallel, literal T
┌──────────────────────┬────────────────────┬──────────────────────────┬───────────────────┐
│                      │       master       │         PR #177          │     PR + fix      │
├──────────────────────┼────────────────────┼──────────────────────────┼───────────────────┤
│ serial, run-time T   │ 11.8 ns, 1 alloc   │ 807 ns, 5 allocs (240 B) │ 11.8 ns, 1 alloc  │
├──────────────────────┼────────────────────┼──────────────────────────┼───────────────────┤
│ parallel, run-time T │ 11.1 µs, 33 allocs │ 12.8 µs, 37 allocs       │ 9.9 µs, 33 allocs │
├──────────────────────┼────────────────────┼──────────────────────────┼───────────────────┤
│ parallel, literal T  │ 10.2 µs, 30 allocs │ 9.8 µs, 30 allocs        │ 9.6 µs, 30 allocs │
└──────────────────────┴────────────────────┴──────────────────────────┴───────────────────┘

the PR + fix line is a potential workaround Claude suggested

@inline function tmapreduce_positional(f::F, op::OP, Arrs, outputtype::Type,
        scheduler, init, kwargs) where {F, OP}
    # ... scheduler setup, checks, serial fallback: unchanged ...
    else
        _tmapreduce_typed(f, op, Arrs, outputtype, _scheduler, mapreduce_kwargs)
    end
end

# function barrier: only the parallel path depends on the output type
@noinline function _tmapreduce_typed(f::F, op::OP, Arrs, ::Type{OutputType}, scheduler,
        mapreduce_kwargs) where {F, OP, OutputType}
    return _tmapreduce(f, op, Arrs, OutputType, scheduler, mapreduce_kwargs)
end

Passed through keyword arguments, `outputtype = Nothing` is stored as a
`DataType`, so `_tmapreduce` was compiled for every output type at once and
a scheduler's implementation compiled the reduction path for every
`tforeach` body, a loop without a result. `tforeach` now calls a positional
entry point that `tmapreduce` shares, with `Nothing` as a constant.

With ThreadTeams' team scheduler this takes the first loop of a new body
from about 100 to 80 ms.
@maleadt

maleadt commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

So one potential side-effect of this is that it slows down the serial fallback path when T is runtime resolved, because it introduces a dispatch layer that was previously skipped.

Thanks, reproduced (6 ns → 470 ns). _tmapreduce already specializes on the output type, so it's enough to drop the type parameter from tmapreduce_positional without adding another barrier. I pushed a fix, the serial case is back to 6 ns, and tforeach still passes Nothing as a constant.

@maleadt
maleadt merged commit 0c0b65e into JuliaFolds2:master Oct 26, 2026
9 checks passed
maleadt added a commit to vchuravy/ThreadTeams.jl that referenced this pull request Oct 26, 2026
0.8.9 passes its output type to `Implementation._tmapreduce` the way the
schedulers expect again (JuliaFolds2/OhMyThreads.jl#177), so the Enzyme
rules work with it. 0.8.8 broke them (#71), and 0.8.7 has nothing over
0.8.9.
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.

4 participants