Summary
With or-tools-wasm@0.9.1, CP-SAT accepts and logs filter_subsolvers: "no_lp", but search starts the sequential [main] solver and performs LP iterations. Native OR-Tools 9.15 with the equivalent model and parameters starts [no_lp] and performs zero LP iterations.
The same source behavior also appears to prevent num_workers > 1 from activating CP-SAT's portfolio search under Emscripten.
Reproduction
Self-contained JavaScript reproduction:
import { CpModel, CpSolver, LinearExpr } from 'or-tools-wasm/cp-sat';
const P = 18, G = 6, S = 6;
const model = new CpModel();
const x = Array.from({ length: P }, () => Array.from({ length: S }, () => Array(G)));
const station = Array.from({ length: P }, () => Array(S));
for (let p = 0; p < P; p++) for (let s = 0; s < S; s++) {
station[p][s] = model.newIntVar(0, G - 1, `st_${p}_${s}`);
const literals = [];
for (let g = 0; g < G; g++) {
x[p][s][g] = model.newBoolVar(`x_${p}_${s}_${g}`);
literals.push(x[p][s][g]);
}
model.addMapDomain(station[p][s], literals);
}
for (let s = 0; s < S; s++) for (let g = 0; g < G; g++) {
model.add(LinearExpr.sum(Array.from({ length: P }, (_, p) => x[p][s][g])).eq(3));
}
for (let p = 0; p < P; p++) model.add(station[p][0].eq(Math.floor(p / 3)));
for (let s = 0; s < S; s++) model.add(station[0][s].eq(s));
for (let p = 0; p < P; p++) for (let g = 0; g < G; g++) {
model.addAtMostOne(Array.from({ length: S }, (_, s) => x[p][s][g]));
}
const penalties = [];
for (let i = 0; i < P; i++) for (let j = i + 1; j < P; j++) {
const meetings = [];
for (let s = 0; s < S; s++) {
const meet = model.newBoolVar(`meet_${i}_${j}_${s}`);
model.add(station[i][s].eq(station[j][s])).onlyEnforceIf(meet);
model.add(station[i][s].ne(station[j][s])).onlyEnforceIf(meet.not());
meetings.push(meet);
}
const count = model.newIntVar(0, S, `pair_count_${i}_${j}`);
model.add(count.eq(LinearExpr.sum(meetings)));
model.add(count.le(1));
const penalty = model.newIntVar(0, 0, `penalty_${i}_${j}`);
model.addAllowedAssignments([count, penalty], [[0, 0], [1, 0]]);
penalties.push(penalty);
}
model.minimize(LinearExpr.weightedSum(penalties, penalties.map(() => 1_000)));
const solver = new CpSolver();
const status = await solver.solve(model, {
randomSeed: 1,
maxTimeInSeconds: 10,
numSearchWorkers: 1,
filterSubsolvers: ['no_lp'],
logSearchProgress: true,
logToResponse: true,
});
console.log(solver.response()?.solveLog ?? '(no solve log)');
console.log({ status: solver.statusName(status), branches: String(solver.numBranches), conflicts: String(solver.numConflicts) });
mkdir repro && cd repro
npm init -y
npm install or-tools-wasm@0.9.1
# save the JavaScript above as repro.mjs
node repro.mjs
Environment used:
or-tools-wasm@0.9.1
- Node 24.15.0 (the same subsolver behavior was also observed in Chromium)
- one requested search worker
- seed 1
The effective parameters are logged as:
Starting CP-SAT solver v9.15.9999
Parameters: random_seed: 1 max_time_in_seconds: 10 ...
num_workers: 1 filter_subsolvers: "no_lp"
But search reports:
1 full problem subsolver: [main]
...
lp_iterations: 118992
status: UNKNOWN
Native OR-Tools 9.15.6755 on the equivalent 1,980-variable / 3,759-constraint model reports:
1 full problem subsolver: [no_lp]
lp_iterations: 0
status: OPTIMAL
In a stricter controlled comparison, native and WASM consumed the same serialized CpModelProto and SatParameters bytes. They reported the same initial fingerprint (0xcb5928685c0232d3) and presolved fingerprint (0xaa602b38d51bc271), then diverged at subsolver selection.
Likely source-level explanation
The package enables pthreads/shared memory in CMake, but vendored OR-Tools defines thread support as false for Emscripten:
https://github.com/Axelwickm/or-tools-wasm/blob/v0.9.1/ortools/port/os.h#L30-L39
#if defined(ORTOOLS_TARGET_OS_IS_ANDROID) || \
defined(ORTOOLS_TARGET_OS_IS_IOS) || \
defined(ORTOOLS_TARGET_OS_IS_EMSCRIPTEN)
#define ORTOOLS_TARGET_OS_SUPPORTS_THREADS 0
SolveCpModel() gates SolveCpModelParallel()—including the path selected by subsolvers, filter_subsolvers, interleaving, and multiple workers—behind that macro:
https://github.com/Axelwickm/or-tools-wasm/blob/v0.9.1/ortools/sat/cp_model_solver.cc#L3037-L3054
The Emscripten branch therefore falls through to:
FullProblemSolver("main", params, /*split_in_chunks=*/false, &shared)
This seems inconsistent with the package's pthread-enabled build and explains why the requested filter is visible in the log but not applied to search. It is also consistent with the published benchmark showing little change between one and eight requested CP-SAT workers.
Questions
- Is disabling OR-Tools thread support for Emscripten intentional in this package?
- Would it be safe for the pthread-enabled build to define
ORTOOLS_TARGET_OS_SUPPORTS_THREADS=1 and use the named-subsolver/parallel architecture?
- If not, should the JS API reject or explicitly warn about unsupported
subsolvers, filterSubsolvers, and multi-worker requests rather than silently running [main]?
I have not yet tested a patched WASM build; that would be the next step needed to confirm whether enabling the guarded path restores native-like no_lp behavior and remains lifecycle-safe in browsers.
Summary
With
or-tools-wasm@0.9.1, CP-SAT accepts and logsfilter_subsolvers: "no_lp", but search starts the sequential[main]solver and performs LP iterations. Native OR-Tools 9.15 with the equivalent model and parameters starts[no_lp]and performs zero LP iterations.The same source behavior also appears to prevent
num_workers > 1from activating CP-SAT's portfolio search under Emscripten.Reproduction
Self-contained JavaScript reproduction:
Environment used:
or-tools-wasm@0.9.1The effective parameters are logged as:
But search reports:
Native OR-Tools 9.15.6755 on the equivalent 1,980-variable / 3,759-constraint model reports:
In a stricter controlled comparison, native and WASM consumed the same serialized
CpModelProtoandSatParametersbytes. They reported the same initial fingerprint (0xcb5928685c0232d3) and presolved fingerprint (0xaa602b38d51bc271), then diverged at subsolver selection.Likely source-level explanation
The package enables pthreads/shared memory in CMake, but vendored OR-Tools defines thread support as false for Emscripten:
https://github.com/Axelwickm/or-tools-wasm/blob/v0.9.1/ortools/port/os.h#L30-L39
SolveCpModel()gatesSolveCpModelParallel()—including the path selected bysubsolvers,filter_subsolvers, interleaving, and multiple workers—behind that macro:https://github.com/Axelwickm/or-tools-wasm/blob/v0.9.1/ortools/sat/cp_model_solver.cc#L3037-L3054
The Emscripten branch therefore falls through to:
This seems inconsistent with the package's pthread-enabled build and explains why the requested filter is visible in the log but not applied to search. It is also consistent with the published benchmark showing little change between one and eight requested CP-SAT workers.
Questions
ORTOOLS_TARGET_OS_SUPPORTS_THREADS=1and use the named-subsolver/parallel architecture?subsolvers,filterSubsolvers, and multi-worker requests rather than silently running[main]?I have not yet tested a patched WASM build; that would be the next step needed to confirm whether enabling the guarded path restores native-like
no_lpbehavior and remains lifecycle-safe in browsers.