**Build scan:**
https://gradle-enterprise.elastic.co/s/jjsb3opg5n7iy/tests/:x-pack:plugin:ml:test/org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlannerTests/testTooManyNodesAndModels_DoesNotThrowOOM_GivenNodesJustUnderLimit

**Reproduction line:**
`./gradlew ':x-pack:plugin:ml:test' --tests "org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlannerTests.testTooManyNodesAndModels_DoesNotThrowOOM_GivenNodesJustUnderLimit" -Dtests.seed=110A9DC5977D6165 -Dbuild.snapshot=false -Dtests.jvm.argline="-Dbuild.snapshot=false" -Dtests.locale=es-MX -Dtests.timezone=GB -Druntime.java=17`

**Applicable branches:**
master

**Reproduces locally?:**
Yes

**Failure history:**
https://gradle-enterprise.elastic.co/scans/tests?tests.container=org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlannerTests&tests.test=testTooManyNodesAndModels_DoesNotThrowOOM_GivenNodesJustUnderLimit

**Failure excerpt:**
```
java.lang.OutOfMemoryError: Java heap space

  at __randomizedtesting.SeedInfo.seed([110A9DC5977D6165:1B9089CC55E53785]:0)
  at org.ojalgo.array.Primitive64Array.<init>(Primitive64Array.java:112)
  at org.ojalgo.matrix.store.Primitive64Store.<init>(Primitive64Store.java:413)
  at org.ojalgo.matrix.store.Primitive64Store$1.make(Primitive64Store.java:173)
  at org.ojalgo.matrix.store.Primitive64Store$1.make(Primitive64Store.java:70)
  at org.ojalgo.structure.Factory2D.make(Factory2D.java:115)
  at org.ojalgo.optimisation.linear.SimplexTableau$DenseTableau.<init>(SimplexTableau.java:61)
  at org.ojalgo.optimisation.linear.SimplexTableau.make(SimplexTableau.java:764)
  at org.ojalgo.optimisation.linear.PrimalSimplex.build(PrimalSimplex.java:128)
  at org.ojalgo.optimisation.linear.LinearSolver$ModelIntegration.build(LinearSolver.java:183)
  at org.ojalgo.optimisation.linear.LinearSolver$ModelIntegration.build(LinearSolver.java:172)
  at org.ojalgo.optimisation.ExpressionsBasedModel$Intermediate.getSolver(ExpressionsBasedModel.java:335)
  at org.ojalgo.optimisation.ExpressionsBasedModel$Intermediate.solve(ExpressionsBasedModel.java:276)
  at org.ojalgo.optimisation.ExpressionsBasedModel.optimise(ExpressionsBasedModel.java:1665)
  at org.ojalgo.optimisation.ExpressionsBasedModel.maximise(ExpressionsBasedModel.java:1193)
  at org.elasticsearch.xpack.ml.inference.assignment.planning.LinearProgrammingPlanSolver.solveLinearProgram(LinearProgrammingPlanSolver.java:292)
  at org.elasticsearch.xpack.ml.inference.assignment.planning.LinearProgrammingPlanSolver.solvePlan(LinearProgrammingPlanSolver.java:93)
  at org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlanner.solvePreservingPreviousAssignments(AssignmentPlanner.java:71)
  at org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlanner.solveKeepingOneAllocationOnPreviousAssignments(AssignmentPlanner.java:61)
  at org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlanner.computePlan(AssignmentPlanner.java:49)
  at org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlannerTests.runTooManyNodesAndModels(AssignmentPlannerTests.java:412)
  at org.elasticsearch.xpack.ml.inference.assignment.planning.AssignmentPlannerTests.testTooManyNodesAndModels_DoesNotThrowOOM_GivenNodesJustUnderLimit(AssignmentPlannerTests.java:250)
  at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(NativeMethodAccessorImpl.java:-2)
  at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
  at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:568)
  at com.carrotsearch.randomizedtesting.RandomizedRunner.invoke(RandomizedRunner.java:1758)
  at com.carrotsearch.randomizedtesting.RandomizedRunner$8.evaluate(RandomizedRunner.java:946)
  at com.carrotsearch.randomizedtesting.RandomizedRunner$9.evaluate(RandomizedRunner.java:982)
  at com.carrotsearch.randomizedtesting.RandomizedRunner$10.evaluate(RandomizedRunner.java:996)
  at com.carrotsearch.randomizedtesting.rules.StatementAdapter.evaluate(StatementAdapter.java:36)
  at org.apache.lucene.tests.util.TestRuleSetupTeardownChained$1.evaluate(TestRuleSetupTeardownChained.java:44)
  at org.apache.lucene.tests.util.AbstractBeforeAfterRule$1.evaluate(AbstractBeforeAfterRule.java:43)

```
x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/inference/assignment/planning/LinearProgrammingPlanSolver.java
/**
     * Ojalgo creates a 2D double array with size of complexity {@link #memoryComplexity()}.
     * We set a limit of storing 10M doubles (~80MB) to avoid OOM. We fall back to the
     * bin packing solution if we're over that limit.
     */
    private static final int MEMORY_COMPLEXITY_LIMIT = 10_000_000;
    private final Random random = new Random(RANDOMIZATION_SEED);
    private final List<Node> nodes;
    private final List<Model> models;
    private final Map<Node, Double> normalizedMemoryPerNode;
    private final Map<Node, Integer> coresPerNode;
    private final Map<Model, Double> normalizedMemoryPerModel;
    private final int maxNodeCores;
    private final long maxModelMemoryBytes;
    LinearProgrammingPlanSolver(List<Node> nodes, List<Model> models) {
        this.nodes = nodes;
        maxNodeCores = this.nodes.stream().map(Node::cores).max(Integer::compareTo).orElse(0);
        long maxNodeMemory = nodes.stream().map(Node::availableMemoryBytes).max(Long::compareTo).orElse(0L);
        this.models = models.stream()
            
            .filter(m -> m.currentAllocationByNodeId().isEmpty() == false || m.memoryBytes() <= maxNodeMemory)
            
            .filter(m -> m.threadsPerAllocation() <= maxNodeCores)
            .toList();
        maxModelMemoryBytes = this.models.stream().map(Model::memoryBytes).max(Long::compareTo).orElse(1L);
        normalizedMemoryPerNode = this.nodes.stream()
            .collect(Collectors.toMap(Function.identity(), n -> n.availableMemoryBytes() / (double) maxModelMemoryBytes));
        coresPerNode = this.nodes.stream().collect(Collectors.toMap(Function.identity(), Node::cores));
        normalizedMemoryPerModel = this.models.stream()
            .collect(Collectors.toMap(Function.identity(), m -> m.memoryBytes() / (double) maxModelMemoryBytes));