Skip to content

Commit 6d23721

Browse files
fix collinearity + update CUDA (#241)
* fix collinearity + update CUDA * update help * Update README.md * Update README.md * Update ci.yml * Update Project.toml * Update Project.toml * correct CUDA spelling
1 parent 851eca9 commit 6d23721

File tree

9 files changed

+63
-24
lines changed

9 files changed

+63
-24
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
fail-fast: false
1616
matrix:
1717
version:
18-
- '1.6' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'.
18+
- '1.9' # Replace this with the minimum Julia version that your package supports. E.g. if your package requires Julia 1.5 or higher, change this to '1.5'.
1919
- '1' # Leave this line unchanged. '1' will automatically expand to the latest stable 1.x release of Julia.
2020
os:
2121
- ubuntu-latest

Project.toml

Lines changed: 1 addition & 1 deletion
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.9.3"
3+
version = "1.9.4"
44

55
[deps]
66
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ reg(df, @formula(Sales ~ NDI + fe(State) + fe(Year)), Vcov.cluster(:State), weig
7878

7979
- The option `save` can be set to one of the following: `:none` (default) to save nothing, `:residuals` to save residuals, `:fe` to save fixed effects, and `:all` to save both. Once saved, they can then be accessed using `residuals(m)` or `fe(m)` where `m` is the estimated model (the object returned by the function `reg`). Both residuals and fixed effects are aligned with the original dataframe used to estimate the model.
8080

81-
- The option `method` can be set to one of the following: `:cpu`, `:gpu` (see Performances below).
81+
- The option `method` can be set to one of the following: `:cpu`, `:CUDA`, or `:Metal` (see Performances below).
8282

8383

8484
## Output
@@ -99,17 +99,27 @@ You may use [RegressionTables.jl](https://github.com/jmboehm/RegressionTables.jl
9999
### MultiThreads
100100
`FixedEffectModels` is multi-threaded. Use the option `nthreads` to select the number of threads to use in the estimation (defaults to `Threads.nthreads()`).
101101

102-
### Nvidia GPU
103-
The package has support for Nvidia GPUs (thanks to Paul Schrimpf). This can make the package an order of magnitude faster for complicated problems.
102+
### GPUs
103+
The package has an experimental support for GPUs. This can make the package an order of magnitude faster for complicated problems.
104104

105-
If you have a Nvidia GPU, run `using CUDA` before `using FixedEffectModels`. Then, estimate a model with `method = :gpu`. For maximum speed, set the floating point precision to `Float32` with `double_precision = false`.
105+
If you have a Nvidia GPU, run `using CUDA` before `using FixedEffectModels`. Then, estimate a model with `method = :CUDA`.
106106

107107
```julia
108108
using CUDA, FixedEffectModels
109+
@assert CUDA.functional()
109110
df = dataset("plm", "Cigar")
110-
reg(df, @formula(Sales ~ NDI + fe(State) + fe(Year)), method = :gpu, double_precision = false)
111+
reg(df, @formula(Sales ~ NDI + fe(State) + fe(Year)), method = :CUDA)
111112
```
112113

114+
The package also supports Apple GPUs with `Metal.jl`, although it does not really improve perfomances
115+
```julia
116+
using Metal, FixedEffectModels
117+
@assert Metal.functional()
118+
df = dataset("plm", "Cigar")
119+
reg(df, @formula(Sales ~ NDI + fe(State) + fe(Year)), method = :Metal)
120+
```
121+
122+
113123

114124
## Solution Method
115125
Denote the model `y = X β + D θ + e` where X is a matrix with few columns and D is the design matrix from categorical variables. Estimates for `β`, along with their standard errors, are obtained in two steps:

src/fit.jl

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ Estimate a linear model with high dimensional categorical variables / instrument
1010
* `contrasts::Dict = Dict()` An optional Dict of contrast codings for each categorical variable in the `formula`. Any unspecified variables will have `DummyCoding`.
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.
13-
* `method::Symbol`: A symbol for the method. Default is :cpu. Alternatively, :gpu requires `CuArrays`. In this case, use the option `double_precision = false` to use `Float32`.
14-
* `nthreads::Integer` Number of threads to use in the estimation. If `method = :cpu`, defaults to `Threads.nthreads()`. If `method = :gpu`, defaults to 256.
15-
* `double_precision::Bool`: Should the demeaning operation use Float64 rather than Float32? Default to true.
13+
* `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.
15+
* `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`.
1616
* `tol::Real` Tolerance. Default to 1e-6.
1717
* `maxiter::Integer = 10000`: Maximum number of iterations
1818
* `drop_singletons::Bool = true`: Should singletons be dropped?
@@ -54,7 +54,7 @@ function reg(df,
5454
save::Union{Bool, Symbol} = :none,
5555
method::Symbol = :cpu,
5656
nthreads::Integer = method == :cpu ? Threads.nthreads() : 256,
57-
double_precision::Bool = true,
57+
double_precision::Bool = method == :cpu,
5858
tol::Real = 1e-6,
5959
maxiter::Integer = 10000,
6060
drop_singletons::Bool = true,
@@ -86,6 +86,11 @@ function StatsAPI.fit(::Type{FixedEffectModel},
8686
df = DataFrame(df; copycols = false)
8787
N = size(df, 1)
8888

89+
if method == :gpu
90+
info("method = :gpu is deprecated. Use method = :CUDA or method = :Metal")
91+
method = :CUDA
92+
end
93+
8994
##############################################################################
9095
##
9196
## Parse formula
@@ -318,15 +323,15 @@ function StatsAPI.fit(::Type{FixedEffectModel},
318323

319324
# first pass: remove colinear variables in Xendo
320325
notcollinear_fe_endo = collinear_fe[find_cols_endo(n_exo, n_endo)] .== false
321-
basis_endo = basis(eachcol(Xendo)...) .* notcollinear_fe_endo
326+
basis_endo = basis(eachcol(Xendo)...; has_intercept = false) .* notcollinear_fe_endo
322327
Xendo = getcols(Xendo, basis_endo)
323328

324329
# second pass: remove colinear variable in Xexo, Z, and Xendo
325330
notcollinear_fe_exo = collinear_fe[find_cols_exo(n_exo)] .== false
326331
notcollinear_fe_z = collinear_fe[find_cols_z(n_exo, n_endo, n_z)] .== false
327332
notcollinear_fe_endo_small = notcollinear_fe_endo[basis_endo]
328333

329-
basis_all = basis(eachcol(Xexo)..., eachcol(Z)..., eachcol(Xendo)...)
334+
basis_all = basis(eachcol(Xexo)..., eachcol(Z)..., eachcol(Xendo)...; has_intercept = has_intercept)
330335
basis_Xexo = basis_all[1:size(Xexo, 2)] .* notcollinear_fe_exo
331336
basis_Z = basis_all[(size(Xexo, 2) +1):(size(Xexo, 2) + size(Z, 2))] .* notcollinear_fe_z
332337
basis_endo_small = basis_all[(size(Xexo, 2) + size(Z, 2) + 1):end] .* notcollinear_fe_endo_small
@@ -350,7 +355,7 @@ function StatsAPI.fit(::Type{FixedEffectModel},
350355
@info "Endogenous vars collinear with ivs. Recategorized as exogenous: $(out)"
351356

352357
# third pass
353-
basis_all = basis(eachcol(Xexo)..., eachcol(Z)..., eachcol(Xendo)...)
358+
basis_all = basis(eachcol(Xexo)..., eachcol(Z)..., eachcol(Xendo)...; has_intercept = has_intercept)
354359
basis_Xexo = basis_all[1:size(Xexo, 2)]
355360
basis_Z = basis_all[(size(Xexo, 2) +1):(size(Xexo, 2) + size(Z, 2))]
356361
end
@@ -377,7 +382,7 @@ function StatsAPI.fit(::Type{FixedEffectModel},
377382
n_exo = size(Xexo, 2)
378383
perm = 1:n_exo
379384
notcollinear_fe_exo = collinear_fe[find_cols_exo(n_exo)] .== false
380-
basis_Xexo = basis(eachcol(Xexo)...) .* notcollinear_fe_exo
385+
basis_Xexo = basis(eachcol(Xexo)...; has_intercept = has_intercept) .* notcollinear_fe_exo
381386
Xexo = getcols(Xexo, basis_Xexo)
382387
Xhat = Xexo
383388
X = Xexo

src/utils/basecol.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ end
3030
##
3131
##
3232
##############################################################################
33-
function basis(@nospecialize(xs::AbstractVector...))
34-
invXX = invsym!(crossprod(collect(xs)))
33+
function basis(@nospecialize(xs::AbstractVector...); has_intercept = false)
34+
invXX = invsym!(crossprod(collect(xs)); has_intercept = has_intercept)
3535
return diag(invXX) .> 0
3636
end
3737

@@ -51,7 +51,7 @@ function crossprod(xs::Vector{<:AbstractVector})
5151
end
5252

5353
# generalized 2inverse
54-
function invsym!(X::AbstractMatrix)
54+
function invsym!(X::AbstractMatrix; has_intercept = false)
5555
# The C value adjusts the check to the relative scale of the variable. The C value is equal to the corrected sum of squares for the variable, unless the corrected sum of squares is 0, in which case C is 1. If you specify the NOINT option but not the ABSORB statement, PROC GLM uses the uncorrected sum of squares instead. The default value of the SINGULAR= option, 107, might be too small, but this value is necessary in order to handle the high-degree polynomials used in the literature to compare regression routin
5656
tols = max.(diag(X), 1)
5757
for j in 1:size(X, 1)
@@ -69,6 +69,9 @@ function invsym!(X::AbstractMatrix)
6969
end
7070
X[j,j] = 1 / d
7171
end
72+
if has_intercept && j == 1
73+
tols = max.(diag(X), 1)
74+
end
7275
end
7376
return X
7477
end

test/Project.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
55
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
66
FixedEffects = "c8885935-8500-56a7-9867-7708b20db0eb"
77
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
8+
Metal = "dde4c033-4e86-420c-a63e-0dd931031962"
89
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
910
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1011
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
@@ -13,5 +14,6 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1314
CategoricalArrays = "0.10"
1415
CSV = "0.8, 0.9, 0.10"
1516
CUDA = "1, 2, 3, 4"
17+
Metal = "0.5"
1618
DataFrames = "0.21, 0.22, 1"
17-
FixedEffects = "2"
19+
FixedEffects = "2.2"

test/collinearity.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,17 @@ using DataFrames, CSV, FixedEffectModels, Random, Statistics, Test
3030
@test rr.coef[1] 0.0
3131
@test isnan(stderror(rr)[1])
3232

33+
34+
35+
df = DataFrame(
36+
[36.9302 44.5105;
37+
39.4935 44.5044;
38+
38.946 44.5072;
39+
37.8005 44.5098;
40+
37.2613 44.5103;
41+
35.3885 44.5109;],
42+
:auto
43+
)
44+
rr = reg(df, @formula(x1 ~ x2))
45+
@test all(!isnan, stderror(rr))
3346
end

test/fit.jl

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using CUDA, FixedEffectModels, CategoricalArrays, CSV, DataFrames, Test, LinearAlgebra
1+
using CUDA, Metal, FixedEffectModels, CategoricalArrays, CSV, DataFrames, Test, LinearAlgebra
22
using FixedEffectModels: nullloglikelihood_within
33

44

@@ -662,8 +662,11 @@ end
662662
@test x.iterations <= 50
663663

664664
methods_vec = [:cpu]
665-
if FixedEffectModels.FixedEffects.has_CUDA()
666-
push!(methods_vec, :gpu)
665+
if CUDA.functional()
666+
push!(methods_vec, :CUDA)
667+
end
668+
if Metal.functional()
669+
push!(methods_vec, :Metal)
667670
end
668671
for method in methods_vec
669672
# same thing with float32 precision

test/predict.jl

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,8 +302,11 @@ end
302302
@test fe(result)[1, :fe_Year] + fe(result)[1, :fe_State] 158.91798 atol = 1e-4
303303

304304
methods_vec = [:cpu]
305-
if FixedEffectModels.FixedEffects.has_CUDA()
306-
push!(methods_vec, :gpu)
305+
if CUDA.functional()
306+
push!(methods_vec, :CUDA)
307+
end
308+
if Metal.functional()
309+
push!(methods_vec, :Metal)
307310
end
308311
for method in methods_vec
309312
local model = @formula Sales ~ Price + fe(Year)

0 commit comments

Comments
 (0)