Skip to content

Cache common subexpressions in IndexMap - #2802

Merged
blegat merged 3 commits into
masterfrom
bl/map_idx_nl
Nov 6, 2026
Merged

blegat merged 3 commits into
masterfrom
bl/map_idx_nl

Conversation

@blegat

@blegat blegat commented Aug 6, 2025

Copy link
Copy Markdown
Member

Extracted from #2788

@odow

odow commented Oct 27, 2026

Copy link
Copy Markdown
Member

I'm going to say no to this one. It seems reasonable that common subexpressions could be supported only in direct_mode without copies.

@blegat

blegat commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Direct mode and non-direct mode shouldn't produce different results like this. For the user, direct_model should be the same as Model except the model transfer should be faster and lighter in memory. So from that perspective, it makes sense that we add this Dict in the IndexMap. We could make it so that when the variables indices map is trivial, we don't do any copy so that most of the time, this dictionary does not get filled.

@odow odow left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs tests, and a benchmark.

Comment thread src/Utilities/copy/index_map.jl Resolved
@blegat
blegat marked this pull request as ready for review November 3, 2026 09:09
@blegat

blegat commented Nov 3, 2026

Copy link
Copy Markdown
Member Author

Benchmark

import MathOptInterface as MOI
using BenchmarkTools

# f_{k+1} = sin(f_k) * f_k + f_k with f_0 = sin(x): the tree has O(3^n) nodes
# but the DAG has O(n) nodes.
function build(n)
    x = MOI.VariableIndex(1)
    f = MOI.ScalarNonlinearFunction(:sin, Any[x])
    for _ in 1:n
        sin_f = MOI.ScalarNonlinearFunction(:sin, Any[f])
        prod = MOI.ScalarNonlinearFunction(:*, Any[sin_f, f])
        f = MOI.ScalarNonlinearFunction(:+, Any[prod, f])
    end
    return f
end

# A non-identity map so that `map_indices` does not return `f` directly.
function swap_map()
    map = MOI.Utilities.IndexMap()
    map[MOI.VariableIndex(1)] = MOI.VariableIndex(2)
    return map
end

function bench(n)
    f = build(n)
    map = swap_map()
    println("n = $n, no cache:")
    @btime MOI.Utilities.map_indices(Base.Fix1(getindex, $map), $f)
    println("n = $n, cache:")
    # `setup` with `evals = 1` gives each run an empty cache.
    @btime MOI.Utilities.map_indices(m, $f) setup = (m = swap_map()) evals = 1
    return
end
n no cache (μs) cache (μs) no cache (bytes) cache (bytes)
4 4.54 1.16 25008 3728
8 383 3.0 1994928 7216
12 47200 5.4 161559936 12736

@blegat
blegat force-pushed the bl/map_idx_nl branch 2 times, most recently from bbea2d7 to 6967f28 Compare November 6, 2026 10:04
* Document that the `nl_cache` field is experimental and must only be
  accessed through `map_indices`.
* Use an `IdDict` since common subexpressions are identified by object
  identity.
* Skip the cache when the variable map is the identity (rebased on #3118).
* Also use the cache for `VectorNonlinearFunction`.
* Keep the `IndexMap(var_map, con_map)` constructor.
* Add tests.

Benchmark of `map_indices` on `f_{k+1} = sin(f_k) * f_k + f_k` with
`f_0 = sin(x)`, whose tree has O(3^n) nodes but whose DAG has O(n)
nodes, comparing `map_indices(Base.Fix1(getindex, map), f)` (no cache)
with `map_indices(map, f)` (cache):

| n  | no cache (μs) | cache (μs) | no cache (bytes) | cache (bytes) |
|----|---------------|------------|------------------|---------------|
| 1  | 0.136         | 0.322      | 896              | 1120          |
| 4  | 4.1           | 1.15       | 25008            | 3728          |
| 8  | 349           | 2.88       | 1994928          | 7216          |
| 12 | 35900         | 5.97       | 161559936        | 12736         |
Replace the Latin abbreviation rejected by Vale. Build the unary-plus expectation from an Expr so the NL reader test follows Julia's display syntax across versions.

Validation: test/FileFormats/NL/test_read.jl passes on Julia 1.13.3 and installed nightly 1.15.0-DEV.244; coverage markers confirm the new lines execute. Vale reports no errors on the changed files. Repository-wide JuliaFormatter 2.14.7 leaves no unstaged diff.
@blegat
blegat merged commit 11295de into master Nov 6, 2026
31 checks passed
@blegat
blegat deleted the bl/map_idx_nl branch November 6, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants