Skip to content

Commit 89e61c5

Browse files
Deprecate nthreads kwarg (#274)
* Refactor nthreads parameter handling in fit.jl Removed default value for nthreads and adjusted related logic. * Remove nthreads parameter from drop_singletons! function * Update default double_precision based on method Change default value of double_precision based on method. * Bump version to 1.13.0 and update FixedEffects compatibility Updated version and dependency compatibility for FixedEffects. * Remove deprecated nthreads argument and update solver Updated fit.jl to remove deprecated nthreads argument and adjust AbstractFixedEffectSolver instantiation. * Update multi-threading explanation in README Clarified the usage of multi-threading in FixedEffectModels. * Refactor fit function to remove nthreads parameter Removed 'nthreads' from the fit function call parameters. * update benchmarks
1 parent 5ae1a89 commit 89e61c5

8 files changed

Lines changed: 48 additions & 35 deletions

File tree

Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "FixedEffectModels"
22
uuid = "9d5cd8c9-2029-5cab-9928-427838db53e3"
3-
version = "1.12.2"
3+
version = "1.13.0"
44

55
[deps]
66
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
@@ -19,7 +19,7 @@ Vcov = "ec2bfdc2-55df-4fc9-b9ae-4958c2cf2486"
1919

2020
[compat]
2121
DataFrames = "0.21, 0.22, 1"
22-
FixedEffects = "2.3"
22+
FixedEffects = "2.6"
2323
PrecompileTools = "1"
2424
Reexport = "0.1, 0.2, 1"
2525
Statistics = "1"

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,7 @@ Finally, you can use [RegressionTables.jl](https://github.com/jmboehm/Regression
119119

120120
## Performances
121121

122-
### MultiThreads
123-
`FixedEffectModels` is multi-threaded. Use the option `nthreads` to select the number of threads to use in the estimation (defaults to `Threads.nthreads()`).
122+
`FixedEffectModels` is multi-threaded by default. Launch Julia with multiple threads to benefit from the speedup. You can verify how many threads are available in your session with `Threads.nthreads()`.
124123

125124
### GPUs
126125
The package has an experimental support for GPUs. This can make the package 2x-5x faster for complicated problems.

benchmark/benchmark.jl

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using DataFrames, Random, CategoricalArrays
22
@time using FixedEffectModels
3-
# 13s precompiling
3+
# 0.418712 seconds (742.49 k allocations: 45.500 MiB, 4.07% gc time, 1.07% compilation time)
44
# Very simple setup
55
N = 10000000
66
K = 100
@@ -12,21 +12,21 @@ y= 3 .* x1 .+ 5 .* x2 .+ cos.(id1) .+ cos.(id2).^2 .+ randn(N)
1212
df = DataFrame(id1 = id1, id2 = id2, x1 = x1, x2 = x2, y = y)
1313
# first time
1414
@time reg(df, @formula(y ~ x1 + x2))
15-
# 1.823s
15+
# 1.810739 seconds (14.05 M allocations: 1.583 GiB, 3.76% gc time, 84.28% compilation time: 91% of which was recompilation)
1616
@time reg(df, @formula(y ~ x1 + x2))
17-
# 0.353469 seconds (441 allocations: 691.439 MiB, 3.65% gc time)
17+
# 0.288344 seconds (712 allocations: 920.405 MiB, 18.71% gc time)
1818
@time reg(df, @formula(y ~ x1 + x2), Vcov.cluster(:id2))
19-
# 0.763999 seconds (2.96 M allocations: 967.418 MiB, 2.29% gc time, 54.39% compilation time: 5% of which was recompilation)
19+
# 0.528590 seconds (1.33 M allocations: 1.039 GiB, 8.76% gc time, 55.33% compilation time)
2020
@time reg(df, @formula(y ~ x1 + x2), Vcov.cluster(:id2))
21-
# 0.401544 seconds (622 allocations: 768.943 MiB, 3.52% gc time)
21+
# 0.268619 seconds (879 allocations: 997.916 MiB, 15.08% gc time)
2222
@time reg(df, @formula(y ~ x1 + x2 + fe(id1)))
23-
# 0.893835 seconds (1.03 k allocations: 929.130 MiB, 54.19% gc time)
23+
# 0.824536 seconds (3.09 M allocations: 1.426 GiB, 5.37% gc time, 61.39% compilation time: 3% of which was recompilation)
2424
@time reg(df, @formula(y ~ x1 + x2 + fe(id1)))
25-
# 0.474160 seconds (1.13 k allocations: 933.340 MiB, 1.74% gc time)
25+
# 0.356793 seconds (1.41 k allocations: 1.276 GiB, 19.31% gc time)
2626
@time reg(df, @formula(y ~ x1 + x2 + fe(id1)), Vcov.cluster(:id1))
27-
# 0.598816 seconds (261.08 k allocations: 1.007 GiB, 8.29% gc time, 9.21% compilation time)
27+
# 0.435500 seconds (495.96 k allocations: 1.381 GiB, 15.97% gc time, 10.72% compilation time)
2828
@time reg(df, @formula(y ~ x1 + x2 + fe(id1) + fe(id2)))
29-
# 1.584573 seconds (489.64 k allocations: 1.094 GiB, 2.10% gc time, 8.53% compilation time)
29+
# 1.367264 seconds (1.91 M allocations: 1.592 GiB, 5.74% gc time, 23.85% compilation time: 20% of which was recompilation)
3030

3131
# More complicated setup
3232
N = 800000 # number of observations
@@ -39,9 +39,7 @@ x2 = cos.(id1) + sin.(id2) + randn(N)
3939
y= 3 .* x1 .+ 5 .* x2 .+ cos.(id1) .+ cos.(id2).^2 .+ randn(N)
4040
df = DataFrame(id1 = id1, id2 = id2, x1 = x1, x2 = x2, y = y)
4141
@time reg(df, @formula(y ~ x1 + x2 + fe(id1) + fe(id2)))
42-
# 2.504294 seconds (75.83 k allocations: 95.525 MiB, 0.23% gc time)
43-
# for some reason in 1.10 I now get worse time (iter 200)
44-
# 4.709078 seconds (108.98 k allocations: 101.417 MiB)
42+
# 1.546023 seconds (19.89 k allocations: 119.673 MiB, 1.70% gc time)
4543

4644

4745

@@ -57,8 +55,8 @@ X1 = rand(n)
5755
ln_y = 3 .* X1 .+ rand(n)
5856
df = DataFrame(X1 = X1, ln_y = ln_y, id1 = id1, id2 = id2, id3 = id3)
5957
@time reg(df, @formula(ln_y ~ X1 + fe(id1)), Vcov.cluster(:id1))
60-
# 0.543996 seconds (873 allocations: 815.677 MiB, 34.15% gc time)
58+
# 0.311420 seconds (1.35 k allocations: 1.052 GiB, 22.30% gc time)
6159
@time reg(df, @formula(ln_y ~ X1 + fe(id1) + fe(id2)), Vcov.cluster(:id1))
62-
# 1.301908 seconds (3.03 k allocations: 968.729 MiB, 25.84% gc time)
60+
# 0.808992 seconds (3.52 k allocations: 1.272 GiB, 8.68% gc time)
6361
@time reg(df, @formula(ln_y ~ X1 + fe(id1) + fe(id2) + fe(id3)), Vcov.cluster(:id1))
64-
# 1.658832 seconds (4.17 k allocations: 1.095 GiB, 29.78% gc time)
62+
# 0.950808 seconds (4.75 k allocations: 1.496 GiB, 7.48% gc time)

benchmark/benchmark.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Code to reproduce this graph:
66
FixedEffectModels.jl v1.9.0 (Julia 1.9)
77
```julia
88
using DataFrames, CategoricalArrays, FixedEffectModels
9-
N = 10000000
9+
N = 10_000_000
1010
K = 100
1111
id1 = rand(1:(N/K), N)
1212
id2 = rand(1:K, N)

benchmark/benchmark_Metal.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using DataFrames, Random, Metal, FixedEffectModels
2+
Random.seed!(1234)
3+
# More complicated setup
4+
N = 8_000_000 # number of observations
5+
M = 400_000 # number of workers
6+
O = 50_000 # number of firms
7+
id1 = rand(1:M, N)
8+
id2 = [rand(max(1, div(x, 8)-10):min(O, div(x, 8)+10)) for x in id1]
9+
x1 = 5 * cos.(id1) + 5 * sin.(id2) + randn(N)
10+
x2 = cos.(id1) + sin.(id2) + randn(N)
11+
x3 = cos.(id1) + sin.(id2) + randn(N)
12+
y= 3 .* x1 .+ 5 .* x2 .+ cos.(id1) .+ cos.(id2).^2 .+ randn(N)
13+
df = DataFrame(id1 = id1, id2 = id2, x1 = x1, x2 = x2, x3 = x3, y = y)
14+
@time reg(df, @formula(y ~ x1 + x2 + x3 + fe(id1) + fe(id2)), maxiter = 200, double_precision = false)
15+
# 30.002852 seconds (66.42 M allocations: 4.917 GiB, 0.83% gc time, 16.25% compilation time: 6% of which was recompilation)
16+
@time reg(df, @formula(y ~ x1 + x2 + x3 + fe(id1) + fe(id2)), method = :Metal, maxiter = 200)
17+
# 16.634684 seconds (3.50 M allocations: 1.407 GiB, 0.69% gc time, 1.49% compilation time: <1% of which was recompilation)
18+
19+
20+

benchmark/result.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
using DataFrames, CSV, Gadflydf = CSV.read("/Users/matthieugomez/Dropbox/Github/FixedEffectModels.jl/benchmark/benchmark.csv", DataFrame)df."fixest (R)" = df."fixest (R)" ./ df."FixedEffectModels.jl (Julia)"df."lfe (R)" = df."lfe (R)" ./ df."FixedEffectModels.jl (Julia)"df."reghdfe (Stata)" = df."reghdfe (Stata)" ./ df."FixedEffectModels.jl (Julia)"df."FixedEffectModels.jl (Julia)" = df."FixedEffectModels.jl (Julia)" ./ df."FixedEffectModels.jl (Julia)"mdf = stack(df, Not([:Command, :Order]))mdf = rename(mdf, :variable => :Language)p = plot(mdf, x = "Command", y = "value", color = "Language", Guide.ylabel("Time (Ratio to Julia)"), Guide.xlabel("Command"), Scale.y_log10)draw(PNG("/Users/matthieugomez/Dropbox/Github/FixedEffectModels.jl/benchmark/fixedeffectmodels_benchmark.png", 8inch, 5inch, dpi=300), p)
1+
using DataFrames, CSV, StatsPlotsdf = CSV.read("/Users/matthieugomez/Dropbox/Github/FixedEffectModels.jl/benchmark/benchmark2.csv", DataFrame)df."fixest (R)" = df."fixest (R)" ./ df."FixedEffectModels.jl (Julia)"df."lfe (R)" = df."lfe (R)" ./ df."FixedEffectModels.jl (Julia)"df."reg / reghdfe (Stata)" = df."reg / reghdfe (Stata)" ./ df."FixedEffectModels.jl (Julia)"df."FixedEffectModels.jl (Julia)" = df."FixedEffectModels.jl (Julia)" ./ df."FixedEffectModels.jl (Julia)"mdf = stack(df, Not([:Command, :Order]))mdf = rename(mdf, :variable => :Language)p = @df mdf plot( :Command, :value, group = :Language, yaxis = :log10, xlabel = "Command", ylabel = "Time (Ratio to Julia)", legend = :top, seriestype = :scatter, # or :line / :path depending on what you want palette = :tol_light, dpi = 200, size=(8 * 100 * 2 /3, 5 * 100 * 2 /3))savefig("/Users/matthieugomez/Dropbox/Github/FixedEffectModels.jl/benchmark/fixedeffectmodels_benchmark.png")

src/fit.jl

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Estimate a linear model with high dimensional categorical variables / instrument
1111
* `weights::Union{Nothing, Symbol}` A symbol to refer to a columns for weights
1212
* `save::Symbol`: Should residuals and eventual estimated fixed effects saved in a dataframe? Default to `:none` Use `save = :residuals` to only save residuals, `save = :fe` to only save fixed effects, `save = :all` for both. Once saved, they can then be accessed using `residuals(m)` or `fe(m)` where `m` is the object returned by the estimation. The returned DataFrame is automatically aligned with the original DataFrame.
1313
* `method::Symbol`: A symbol for the method. Default is :cpu. Alternatively, use :CUDA or :Metal (in this case, you need to import the respective package before importing FixedEffectModels)
14-
* `nthreads::Integer` Number of threads to use in the estimation. If `method = :cpu`, defaults to `Threads.nthreads()`. Otherwise, defaults to 256.
1514
* `double_precision::Bool`: Should the demeaning operation use Float64 rather than Float32? Default to true if `method =:cpu' and false if `method = :CUDA` or `method = :Metal`.
1615
* `tol::Real` Tolerance. Default to 1e-6.
1716
* `maxiter::Integer = 10000`: Maximum number of iterations
@@ -53,15 +52,15 @@ function reg(df,
5352
weights::Union{Symbol, Nothing} = nothing,
5453
save::Union{Bool, Symbol} = :none,
5554
method::Symbol = :cpu,
56-
nthreads::Integer = method == :cpu ? Threads.nthreads() : 256,
55+
nthreads::Union{Integer, Nothing} = nothing,
5756
double_precision::Bool = method == :cpu,
5857
tol::Real = 1e-6,
5958
maxiter::Integer = 10000,
6059
drop_singletons::Bool = true,
6160
progress_bar::Bool = true,
6261
subset::Union{Nothing, AbstractVector} = nothing,
6362
first_stage::Bool = true)
64-
StatsAPI.fit(FixedEffectModel, formula, df, vcov; contrasts = contrasts, weights = weights, save = save, method = method, nthreads = nthreads, double_precision = double_precision, tol = tol, maxiter = maxiter, drop_singletons = drop_singletons, progress_bar = progress_bar, subset = subset, first_stage = first_stage)
63+
StatsAPI.fit(FixedEffectModel, formula, df, vcov; contrasts = contrasts, weights = weights, save = save, method = method, double_precision = double_precision, tol = tol, maxiter = maxiter, drop_singletons = drop_singletons, progress_bar = progress_bar, subset = subset, first_stage = first_stage)
6564
end
6665

6766
function StatsAPI.fit(::Type{FixedEffectModel},
@@ -72,8 +71,8 @@ function StatsAPI.fit(::Type{FixedEffectModel},
7271
@nospecialize(weights::Union{Symbol, Nothing} = nothing),
7372
@nospecialize(save::Union{Bool, Symbol} = :none),
7473
@nospecialize(method::Symbol = :cpu),
75-
@nospecialize(nthreads::Integer = method == :cpu ? Threads.nthreads() : 256),
76-
@nospecialize(double_precision::Bool = true),
74+
@nospecialize(nthreads::Union{Integer, Nothing} = nothing),
75+
@nospecialize(double_precision::Bool = method == :cpu),
7776
@nospecialize(tol::Real = 1e-6),
7877
@nospecialize(maxiter::Integer = 10000),
7978
@nospecialize(drop_singletons::Bool = true),
@@ -94,7 +93,9 @@ function StatsAPI.fit(::Type{FixedEffectModel},
9493
info("method = :gpu is deprecated. Use method = :CUDA or method = :Metal")
9594
method = :CUDA
9695
end
97-
96+
if nthreads !== nothing
97+
info("The keyword argument nthreads is deprecated. Multiple threads are now used by default.")
98+
end
9899
if save == true
99100
save = :all
100101
elseif save == false
@@ -105,11 +106,6 @@ function StatsAPI.fit(::Type{FixedEffectModel},
105106
end
106107
save_residuals = (save == :residuals) | (save == :all)
107108

108-
if method == :cpu && nthreads > Threads.nthreads()
109-
@warn "Keyword argument nthreads = $(nthreads) is ignored (Julia was started with only $(Threads.nthreads()) threads)."
110-
nthreads = Threads.nthreads()
111-
end
112-
113109
##############################################################################
114110
##
115111
## Parse formula
@@ -166,7 +162,7 @@ function StatsAPI.fit(::Type{FixedEffectModel},
166162

167163
n_singletons = 0
168164
if drop_singletons
169-
n_singletons = drop_singletons!(esample, fes, nthreads)
165+
n_singletons = drop_singletons!(esample, fes)
170166
end
171167

172168
nobs = sum(esample)
@@ -240,7 +236,7 @@ function StatsAPI.fit(::Type{FixedEffectModel},
240236
sumsquares_pre = [sum(abs2, x) for x in cols]
241237

242238
# partial out fixed effects
243-
feM = AbstractFixedEffectSolver{double_precision ? Float64 : Float32}(subfes, weights, Val{method}, nthreads)
239+
feM = AbstractFixedEffectSolver{double_precision ? Float64 : Float32}(subfes, weights, Val{method})
244240

245241
# partial out fixed effects
246242
_, iterations, convergeds = solve_residuals!(cols, feM; maxiter = maxiter, tol = tol, progress_bar = progress_bar)

src/utils/fixedeffects.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
##
55
##############################################################################
66

7-
function drop_singletons!(esample, fes::Vector{<:FixedEffect}, nthreads)
7+
function drop_singletons!(esample, fes::Vector{<:FixedEffect})
88
nsingletons = 0
99
ncleanpasses = 0
1010
caches = [Vector{UInt8}(undef, fes[i].n) for i in eachindex(fes)]

0 commit comments

Comments
 (0)