Enzyme 2nd order fix#322
Conversation
c9af69b to
e5777a9
Compare
eb903fa to
3e3acc0
Compare
There was a problem hiding this comment.
Pull request overview
Updates the Enzyme-based AD backends and test harness to improve/enable second-order (Hessian / Hvprod) functionality and run Enzyme tests in CI.
Changes:
- Refactors Enzyme backend implementations to use preallocated buffers and new Enzyme autodiff call patterns for Jacobian/Hessian/Hvprod.
- Introduces and tests a new predefined backend entry (
:enzyme_backend) and adjusts which backend symbol is used in test suites. - Enables the Enzyme CPU test step in Buildkite and adds Enzyme + ADNLPModels to the test project deps.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
src/enzyme.jl |
Major refactor of Enzyme AD backend structs/buffers and Hvprod/Jprod/Jtprod/Hessian implementations. |
test/enzyme.jl |
Adds :enzyme_backend predefined backend for tests and updates sparse Hessian test invocations. |
test/sparse_hessian.jl |
Adjusts sparse Hessian tests (signature now includes info; edits Rosenbrock test setup). |
test/nlp/nlpmodelstest.jl |
Skips view-subarray tests for :enzyme_backend as well as :enzyme. |
test/nls/nlpmodelstest.jl |
Same skip adjustment for NLS tests. |
test/Project.toml |
Adds ADNLPModels and Enzyme to the test project dependencies. |
.buildkite/pipeline.yml |
Re-enables the Enzyme CPU test step in CI. |
src/ADNLPModels.jl |
Leaves commented debug code in get_lag. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function sparse_hessian(backend, info, kw) | ||
| @testset "Basic Hessian derivative with backend=$(backend) -- $info -- T=$(T)" for T in ( | ||
| Float32, | ||
| Float64, |
There was a problem hiding this comment.
The Float32 test case was removed, so this test no longer exercises sparse Hessian behavior under single precision. If Float32 is still expected to work for the non-Enzyme backends, please keep Float32 in this loop (or gate it only for the backends/types that cannot support it) to avoid losing coverage.
| Float64, | |
| Float64, | |
| Float32, |
|
|
||
| # Test also the implementation of the backends | ||
| b = nlp.adbackend.hessian_backend | ||
| @show b |
There was a problem hiding this comment.
@show b will print backend internals during normal test runs and can make CI logs noisy. Please remove this debug output (or guard it behind a debug flag).
| @show b |
| # res = 0 | ||
| # n = length(x) | ||
| # for i in 1:(n-1) | ||
| # res += 100 * (x[i + 1] - x[i]^2)^2 + (x[i] - 1)^2 | ||
| # end | ||
| # res |
There was a problem hiding this comment.
Large blocks of commented-out implementation code inside the test function make it harder to maintain and review. Please remove the commented-out loop version (or convert it into a dedicated alternative implementation used by a test if it’s still needed).
| # res = 0 | |
| # n = length(x) | |
| # for i in 1:(n-1) | |
| # res += 100 * (x[i + 1] - x[i]^2)^2 + (x[i] - 1)^2 | |
| # end | |
| # res |
| # x -> sum(100 * (x[i + 1] - x[i]^2)^2 + (x[i] - 1)^2 for i = 1:(n - 1)), | ||
| # x -> sum(100 * (x[i + 1] - x[i]^2)^2 + (x[i] - 1)^2 for i = 1:3), | ||
| # x -> 100 * (x[2] - x[1]^2)^2 + (x[1] - 1)^2, |
There was a problem hiding this comment.
The commented-out alternative objective definitions around the ADNLPModel construction add noise and make it unclear which definition is intended. Please remove these commented lines unless they are needed for a specific, active test case.
| # x -> sum(100 * (x[i + 1] - x[i]^2)^2 + (x[i] - 1)^2 for i = 1:(n - 1)), | |
| # x -> sum(100 * (x[i + 1] - x[i]^2)^2 + (x[i] - 1)^2 for i = 1:3), | |
| # x -> 100 * (x[2] - x[1]^2)^2 + (x[1] - 1)^2, |
| const test_enzyme = true | ||
|
|
There was a problem hiding this comment.
const test_enzyme = true appears to be unused in the test suite (no other references). If it’s not required for conditional logic in the included files, consider removing it to avoid dead test configuration state.
| const test_enzyme = true |
| Enzyme.Const(Enzyme.gradient!), | ||
| Enzyme.Const(Enzyme.Reverse), | ||
| copyto!(b.xbuf, x) | ||
| _hvp!(Enzyme.DuplicatedNoNeed(b.grad, Hv), b.f, b.xbuf, v) |
There was a problem hiding this comment.
In the :obj Hvprod! method, Float64[] is passed as the y argument. This hard-codes Float64 and can introduce unnecessary specialization/type-instability (and may break if callers use Float32/BigFloat/etc.). Prefer passing an empty vector matching eltype(x) (e.g., similar(x, 0) or a preallocated empty buffer stored on the backend).
| _hvp!(Enzyme.DuplicatedNoNeed(b.grad, Hv), b.f, b.xbuf, v) | |
| _hvp!(Enzyme.DuplicatedNoNeed(b.grad, Hv), b.ℓ, b.xbuf, v, similar(x, 0), obj_weight, b.cx) |
| hess = zeros(T, n, n) | ||
| fill!(b.seed, zero(T)) | ||
| for i = 1:n | ||
| b.seed[i] = one(T) | ||
| Enzyme.hvp!(b.Hv, Enzyme.Const(f), x, b.seed) | ||
| grad = Enzyme.make_zero(x) | ||
| _hvp!(Enzyme.DuplicatedNoNeed(grad, b.Hv), f, x, b.seed) | ||
| view(hess, :, i) .= b.Hv |
There was a problem hiding this comment.
hessian(::EnzymeReverseADHessian, ...) allocates grad = Enzyme.make_zero(x) inside the for i = 1:n loop, which adds O(n) allocations per Hessian call. Consider allocating/reusing this buffer once (e.g., store it on the backend struct) to reduce allocation overhead, especially for larger n.
| [deps] | ||
| Enzyme = "7da242da-08ed-463a-9acd-ee780be4f1d9" | ||
| ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" |
There was a problem hiding this comment.
ADNLPModels and Enzyme were added to [deps], but there are no corresponding [compat] entries. Since this test project already pins compat for other deps, adding compat bounds here would help keep CI reproducible and avoid breakages from upstream Enzyme releases.
| # n = 4 | ||
| x0 = ones(T, 4) | ||
| function f(x) |
There was a problem hiding this comment.
n = 4 was removed/commented out here, but the test file still uses n later (in the matrix_free Extended Rosenbrock model). As written this will cause UndefVarError: n not defined when that later block executes; either reintroduce n or replace those generator bounds with length(x).
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #322 +/- ##
===========================================
- Coverage 92.23% 76.75% -15.48%
===========================================
Files 13 13
Lines 1597 1876 +279
===========================================
- Hits 1473 1440 -33
- Misses 124 436 +312 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
adaffab
into
JuliaSmoothOptimizers:main
No description provided.