Skip to content

Commit

Permalink
feat: Automatic node sharing for ConstraintProvider (Enterprise) (#685)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukáš Petrovický <[email protected]>
  • Loading branch information
Christopher-Chianelli and triceo authored Mar 8, 2024
1 parent 207e122 commit 53f83bd
Show file tree
Hide file tree
Showing 18 changed files with 552 additions and 166 deletions.
3 changes: 3 additions & 0 deletions benchmark/src/main/resources/benchmark.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,9 @@
<xs:element minOccurs="0" name="constraintStreamImplType" type="tns:constraintStreamImplType"/>


<xs:element minOccurs="0" name="constraintStreamAutomaticNodeSharing" type="xs:boolean"/>


<xs:element minOccurs="0" name="incrementalScoreCalculatorClass" type="xs:string"/>


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig;
import ai.timefold.solver.core.config.solver.EnvironmentMode;
import ai.timefold.solver.core.config.util.ConfigUtils;
import ai.timefold.solver.core.enterprise.TimefoldSolverEnterpriseService;
import ai.timefold.solver.core.impl.domain.solution.descriptor.SolutionDescriptor;
import ai.timefold.solver.core.impl.score.director.AbstractScoreDirectorFactory;
import ai.timefold.solver.core.impl.score.director.ScoreDirectorType;
Expand Down Expand Up @@ -41,9 +42,19 @@ public Supplier<AbstractScoreDirectorFactory<Solution_, Score_>> buildScoreDirec
"The constraintProviderClass (" + config.getConstraintProviderClass()
+ ") does not implement " + ConstraintProvider.class.getSimpleName() + ".");
}
final Class<? extends ConstraintProvider> constraintProviderClass;
if (Boolean.TRUE.equals(config.getConstraintStreamAutomaticNodeSharing())) {
TimefoldSolverEnterpriseService enterpriseService =
TimefoldSolverEnterpriseService
.loadOrFail(TimefoldSolverEnterpriseService.Feature.AUTOMATIC_NODE_SHARING);
constraintProviderClass =
enterpriseService.buildLambdaSharedConstraintProvider(config.getConstraintProviderClass());
} else {
constraintProviderClass = config.getConstraintProviderClass();
}
return () -> {
ConstraintProvider constraintProvider = ConfigUtils.newInstance(config,
"constraintProviderClass", config.getConstraintProviderClass());
"constraintProviderClass", constraintProviderClass);
ConfigUtils.applyCustomProperties(constraintProvider, "constraintProviderClass",
config.getConstraintProviderCustomProperties(), "constraintProviderCustomProperties");
return buildScoreDirectorFactory(solutionDescriptor, constraintProvider, environmentMode);
Expand Down
13 changes: 12 additions & 1 deletion core/core-impl/src/build/revapi-differences.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,19 @@
"new": "class ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig",
"superClass": "ai.timefold.solver.core.config.heuristic.selector.move.NearbyAutoConfigurationMoveSelectorConfig<ai.timefold.solver.core.config.heuristic.selector.move.generic.list.kopt.KOptListMoveSelectorConfig>",
"justification": "Adding support for Nearby Selection autoconfiguration"
},
{
"ignore": true,
"code": "java.annotation.attributeValueChanged",
"old": "class ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig",
"new": "class ai.timefold.solver.core.config.score.director.ScoreDirectorFactoryConfig",
"annotationType": "jakarta.xml.bind.annotation.XmlType",
"attribute": "propOrder",
"oldValue": "{\"easyScoreCalculatorClass\", \"easyScoreCalculatorCustomProperties\", \"constraintProviderClass\", \"constraintProviderCustomProperties\", \"constraintStreamImplType\", \"incrementalScoreCalculatorClass\", \"incrementalScoreCalculatorCustomProperties\", \"scoreDrlList\", \"initializingScoreTrend\", \"assertionScoreDirectorFactory\"}",
"newValue": "{\"easyScoreCalculatorClass\", \"easyScoreCalculatorCustomProperties\", \"constraintProviderClass\", \"constraintProviderCustomProperties\", \"constraintStreamImplType\", \"constraintStreamAutomaticNodeSharing\", \"incrementalScoreCalculatorClass\", \"incrementalScoreCalculatorCustomProperties\", \"scoreDrlList\", \"initializingScoreTrend\", \"assertionScoreDirectorFactory\"}",
"justification": "Add support for automatic constraint stream node sharing outside of Quarkus for Timefold Solver Enterprise edition users."
}
]
}
}
]
]
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"constraintProviderClass",
"constraintProviderCustomProperties",
"constraintStreamImplType",
"constraintStreamAutomaticNodeSharing",
"incrementalScoreCalculatorClass",
"incrementalScoreCalculatorCustomProperties",
"scoreDrlList",
Expand All @@ -41,6 +42,7 @@ public class ScoreDirectorFactoryConfig extends AbstractConfig<ScoreDirectorFact
@XmlJavaTypeAdapter(JaxbCustomPropertiesAdapter.class)
protected Map<String, String> constraintProviderCustomProperties = null;
protected ConstraintStreamImplType constraintStreamImplType;
protected Boolean constraintStreamAutomaticNodeSharing;

protected Class<? extends IncrementalScoreCalculator> incrementalScoreCalculatorClass = null;

Expand Down Expand Up @@ -101,6 +103,14 @@ public void setConstraintStreamImplType(ConstraintStreamImplType constraintStrea
this.constraintStreamImplType = constraintStreamImplType;
}

public Boolean getConstraintStreamAutomaticNodeSharing() {
return constraintStreamAutomaticNodeSharing;
}

public void setConstraintStreamAutomaticNodeSharing(Boolean constraintStreamAutomaticNodeSharing) {
this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing;
}

public Class<? extends IncrementalScoreCalculator> getIncrementalScoreCalculatorClass() {
return incrementalScoreCalculatorClass;
}
Expand Down Expand Up @@ -187,6 +197,11 @@ public ScoreDirectorFactoryConfig withConstraintStreamImplType(ConstraintStreamI
return this;
}

public ScoreDirectorFactoryConfig withConstraintStreamAutomaticNodeSharing(Boolean constraintStreamAutomaticNodeSharing) {
this.constraintStreamAutomaticNodeSharing = constraintStreamAutomaticNodeSharing;
return this;
}

public ScoreDirectorFactoryConfig
withIncrementalScoreCalculatorClass(Class<? extends IncrementalScoreCalculator> incrementalScoreCalculatorClass) {
this.incrementalScoreCalculatorClass = incrementalScoreCalculatorClass;
Expand Down Expand Up @@ -246,6 +261,8 @@ public ScoreDirectorFactoryConfig inherit(ScoreDirectorFactoryConfig inheritedCo
constraintProviderCustomProperties, inheritedConfig.getConstraintProviderCustomProperties());
constraintStreamImplType = ConfigUtils.inheritOverwritableProperty(
constraintStreamImplType, inheritedConfig.getConstraintStreamImplType());
constraintStreamAutomaticNodeSharing = ConfigUtils.inheritOverwritableProperty(constraintStreamAutomaticNodeSharing,
inheritedConfig.getConstraintStreamAutomaticNodeSharing());
incrementalScoreCalculatorClass = ConfigUtils.inheritOverwritableProperty(
incrementalScoreCalculatorClass, inheritedConfig.getIncrementalScoreCalculatorClass());
incrementalScoreCalculatorCustomProperties = ConfigUtils.inheritMergeableMapProperty(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.ServiceLoader;
import java.util.function.BiFunction;

import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
import ai.timefold.solver.core.api.solver.SolverFactory;
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionCacheType;
import ai.timefold.solver.core.config.heuristic.selector.common.SelectionOrder;
Expand Down Expand Up @@ -57,6 +58,9 @@ static TimefoldSolverEnterpriseService loadOrFail(Feature feature) {
return service;
}

Class<? extends ConstraintProvider>
buildLambdaSharedConstraintProvider(Class<? extends ConstraintProvider> originalConstraintProvider);

<Solution_> ConstructionHeuristicDecider<Solution_> buildConstructionHeuristic(int moveThreadCount,
Termination<Solution_> termination, ConstructionHeuristicForager<Solution_> forager,
EnvironmentMode environmentMode, HeuristicConfigPolicy<Solution_> configPolicy);
Expand Down Expand Up @@ -90,7 +94,9 @@ <Solution_> DestinationSelector<Solution_> applyNearbySelection(DestinationSelec
enum Feature {
MULTITHREADED_SOLVING("Multi-threaded solving", "remove moveThreadCount from solver configuration"),
PARTITIONED_SEARCH("Partitioned search", "remove partitioned search phase from solver configuration"),
NEARBY_SELECTION("Nearby selection", "remove nearby selection from solver configuration");
NEARBY_SELECTION("Nearby selection", "remove nearby selection from solver configuration"),
AUTOMATIC_NODE_SHARING("Automatic node sharing",
"remove automatic node sharing from solver configuration");

private final String name;
private final String workaround;
Expand Down
2 changes: 2 additions & 0 deletions core/core-impl/src/main/resources/solver.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@

<xs:element minOccurs="0" name="constraintStreamImplType" type="tns:constraintStreamImplType"/>

<xs:element minOccurs="0" name="constraintStreamAutomaticNodeSharing" type="xs:boolean"/>

<xs:element minOccurs="0" name="incrementalScoreCalculatorClass" type="xs:string"/>

<xs:element minOccurs="0" name="incrementalScoreCalculatorCustomProperties" type="tns:jaxbAdaptedMap"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ The following features are only available in Timefold Solver Enterprise Edition:

* <<nearbySelection,Nearby selection>>,
* <<multithreadedSolving,multi-threaded solving>>,
* and <<partitionedSearch,partitioned Search>>.
* <<partitionedSearch,partitioned Search>>,
* and <<automaticNodeSharing, automatic node sharing>>.
[#nearbySelection]
Expand Down Expand Up @@ -759,5 +760,194 @@ the host is likely to hang or freeze,
unless there is an OS specific policy in place to avoid Timefold Solver from hogging all the CPU processors.
====

[#automaticNodeSharing]
=== Automatic node sharing

[NOTE]
====
This feature is a commercial feature of Timefold Solver Enterprise Edition.
It is not available in the Community Edition.
====

When a `ConstraintProvider` does an operation for multiple constraints (such as finding all shifts corresponding to an employee), that work can be shared.
This can significantly improve score calculation speed if the repeated operation is computationally expensive.

==== Configuration

[tabs]
======
Plain Java::
* Add `<constraintStreamAutomaticNodeSharing>true</constraintStreamAutomaticNodeSharing>` in your `solverConfig.xml`:
+
[source,xml,options="nowrap"]
----
<!-- ... -->
<scoreDirectorFactory>
<constraintProviderClass>org.acme.MyConstraintProvider</constraintProviderClass>
<constraintStreamAutomaticNodeSharing>true</constraintStreamAutomaticNodeSharing>
</scoreDirectorFactory>
<!-- ... -->
----
Spring Boot::
Set the property `timefold.solver.constraint-stream-automatic-node-sharing` to `true` in `application.properties`:
+
[source,properties,options="nowrap"]
----
timefold.solver.constraint-stream-automatic-node=true
----
Quarkus::
Set the property `quarkus.timefold.solver.constraint-stream-automatic-node-sharing` to `true` in `application.properties`:
+
[source,properties,options="nowrap"]
----
quarkus.timefold.solver.constraint-stream-automatic-node-sharing=true
----
======

[IMPORTANT]
====
Debugging breakpoints put inside your constraints will not be respected, because the `ConstraintProvider` class will be transformed when this feature is enabled.
====

==== What is node sharing?

When using xref:constraints-and-score/score-calculation.adoc#constraintStreams[constraint streams], each xref:constraints-and-score/score-calculation.adoc#constraintStreamsBuildingBlocks[building block] forms a node in the score calculation network.
When two building blocks are functionally equivalent, they can share the same node in the network.
Sharing nodes allows the operation to be performed only once instead of multiple times, improving the performance of the solver.
To be functionally equivalent, the following must be true:

* The building blocks must represent the same operation.

* The building blocks must have functionally equivalent parent building blocks.

* The building blocks must have functionally equivalent inputs.

For example, the building blocks below are functionally equivalent:

[source,java,options="nowrap"]
----
Predicate<Shift> predicate = shift -> shift.getEmployee().getName().equals("Ann");
var a = factory.forEach(Shift.class)
.filter(predicate);
var b = factory.forEach(Shift.class)
.filter(predicate);
----

Whereas these building blocks are not functionally equivalent:

[source,java,options="nowrap"]
----
Predicate<Shift> predicate1 = shift -> shift.getEmployee().getName().equals("Ann");
Predicate<Shift> predicate2 = shift -> shift.getEmployee().getName().equals("Bob");
// Different parents
var a = factory.forEach(Shift.class)
.filter(predicate2);
var b = factory.forEach(Shift.class)
.filter(predicate1)
.filter(predicate2);
// Different operations
var a = factory.forEach(Shift.class)
.ifExists(Employee.class);
var b = factory.forEach(Shift.class)
.ifNotExists(Employee.class);
// Different inputs
var a = factory.forEach(Shift.class)
.filter(predicate1);
var b = factory.forEach(Shift.class)
.filter(predicate2);
----

Counterintuitively, the building blocks produced by these (seemly) identical methods are not necessarily functionally equivalent:

[source,java,options="nowrap"]
----
UniConstraintStream<Shift> a(ConstraintFactory constraintFactory) {
return factory.forEach(Shift.class)
.filter(shift -> shift.getEmployee().getName().equals("Ann"));
}
UniConstraintStream<Shift> b(ConstraintFactory constraintFactory) {
return factory.forEach(Shift.class)
.filter(shift -> shift.getEmployee().getName().equals("Ann"));
}
----

The Java Virtual Machine is free to (and often does) create different instances of functionally equivalent lambdas.
This severely limits the effectiveness of node sharing, since the only way to know two lambdas are equal is to compare their references.

When automatic node sharing is used, the `ConstraintProvider` class is transformed so all lambdas are accessed via a static final field.
Consider the following input class:

[source,java,options="nowrap"]
----
public class MyConstraintProvider implements ConstraintProvider {
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
a(constraintFactory),
b(constraintFactory)
};
}
Constraint a(ConstraintFactory constraintFactory) {
return factory.forEach(Shift.class)
.filter(shift -> shift.getEmployee().getName().equals("Ann"))
.penalize(SimpleScore.ONE)
.asConstraint("a");
}
Constraint b(ConstraintFactory constraintFactory) {
return factory.forEach(Shift.class)
.filter(shift -> shift.getEmployee().getName().equals("Ann"))
.penalize(SimpleScore.ONE)
.asConstraint("b");
}
}
----

When automatic node sharing is enabled, the class will be transformed to look like this:

[source,java,options="nowrap"]
----
public class MyConstraintProvider implements ConstraintProvider {
private static final Predicate<Shift> $predicate1 = shift -> shift.getEmployee().getName().equals("Ann");
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
a(constraintFactory),
b(constraintFactory)
};
}
Constraint a(ConstraintFactory constraintFactory) {
return factory.forEach(Shift.class)
.filter($predicate1)
.penalize(SimpleScore.ONE)
.asConstraint("a");
}
Constraint b(ConstraintFactory constraintFactory) {
return factory.forEach(Shift.class)
.filter($predicate1)
.penalize(SimpleScore.ONE)
.asConstraint("b");
}
}
----

This transformation means that debugging breakpoints placed inside the original `ConstraintProvider` will not be honored in the transformed `ConstraintProvider`.

From the above, you can see how this feature allows building blocks to share functionally equivalent parents, without needing the `ConstraintProvider` to be written in an awkward way.
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ BenchmarkConfigBuildItem registerAdditionalBeans(BuildProducer<AdditionalBeanBui
BuildProducer<UnremovableBeanBuildItem> unremovableBeans,
SolverConfigBuildItem solverConfigBuildItem) {
// We don't support benchmarking for multiple solvers
if (solverConfigBuildItem.getSolvetConfigMap().size() > 1) {
if (solverConfigBuildItem.getSolverConfigMap().size() > 1) {
throw new ConfigurationException("""
When defining multiple solvers, the benchmark feature is not enabled.
Consider using separate <solverBenchmark> instances for evaluating different solver configurations.""");
Expand Down
Loading

0 comments on commit 53f83bd

Please sign in to comment.