perf: run InternalCompat Scala and Python validation in parallel lanes - #2658
Conversation
InternalCompat took 67.6 min on build 231549771, making it the slowest signal on a SynapseML PR. Its Scala and Python halves are independent -- neither reads the other's output -- and only ran back to back because they happened to share one checkout. Split the job into two matrix legs, mirroring how SynapseML-Internal's own pipeline shards work across templates/scala_test_job.yml and templates/python_test_job.yml. Measured breakdown of the 67.6 min (build 231549771): shared prefix (checkout, publish OSS to M2, retarget, compile) 12.5m Create Internal conda env 7.1m Scala: spark.aifunc 22.8m + ebm 2.5m + predict 0.4m 25.9m Python: package 1.0m + testPythonExcludeAIFunc 19.2m 20.2m The Scala leg additionally skips conda env creation. SynapseML-Internal runs ScalaAIFuncTests*, ScalaEBMTests and ScalaPredictTests with useConda unset, reserving that env for its PowerBI and nbtest jobs -- and those are exactly the three packages this leg runs. The step already exported CREATE_SEMPY_WRITER=false, which is Internal's non-conda path. Activation is now guarded on the env actually existing, so the step keeps working if the env is ever reintroduced. Result: wall clock becomes max(~39, ~41) rather than the sum, so InternalCompat reports roughly 27 min sooner (-39%). Two legs and no more: every leg re-pays the 12.5 min shared prefix, so a third leg (splitting spark.aifunc away from ebm+predict) would only reach ~38 min while consuming another agent. Internal can shard 6-and-8 ways because its legs resolve a published OSS artifact instead of building one from the change under test, so they have no comparable prefix to amortise. Validation: - YAML parses; all 12 inline scripts pass bash -n - Lane simulation confirms the 7 shared steps run in both legs and the 9 lane-specific steps run in exactly one - The conda-presence predicate was exercised against 5 `conda env list` shapes (present, present+active, absent, conda missing, similar-but- different name) and for set -e safety when conda is absent InternalCompat remains continueOnError: true and is not a required status check, so the new per-leg check names do not affect branch protection. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
/azp run |
|
Hey Rana Singh (@ranadeepsingh) 👋! We use semantic commit messages to streamline the release process. Examples of commit messages with semantic prefixes:
To test your commit locally, please follow our guild on building from source. |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
There was a problem hiding this comment.
Pull request overview
This PR optimizes the SynapseML-Internal Compatibility Check Azure Pipelines job by converting it into a 2-leg matrix (scala and python) so the Scala and Python validation paths can run in parallel, reducing overall PR feedback latency.
Changes:
- Introduces a
strategy.matrixwithCOMPAT_LANEset toscalaorpythonto run two parallel legs. - Gates lane-specific steps (conda env creation, Scala tests, Python packaging/tests, and per-lane test result publishing) using
condition: eq(variables['COMPAT_LANE'], ...). - Makes Scala test execution resilient to the conda env being absent by guarding conda activation on env presence.
Show a summary per file
| File | Description |
|---|---|
pipeline.yaml |
Splits InternalCompat into parallel Scala/Python matrix lanes and adds lane-conditional execution for conda, test, and publish steps. |
Review details
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Lite
Measured results — build 231589470 ✅Build succeeded, 68/68 jobs, 0 failures. Both matrix legs were created and ran on separate agents. Wall clock for the
|
| Leg | Projected | Measured |
|---|---|---|
scala |
~39m | 38.2m |
python |
~41m | 38.7m |
| Job wall clock (max of legs) | ~41m | 38.7m |
| Serial baseline (231549771) | — | 67.6m |
−28.9 min (−43%).
Test parity — no tests were lost
This is the part that actually needed proving, since the scala leg now runs without the conda env.
| Suite group | Baseline (serial, conda present) | scala leg (no conda) |
|---|---|---|
spark.aifunc |
129 tests / 12 suites | 129 / 12 |
ebm |
71 / 9 | 71 / 9 |
predict |
11 / 2 | 11 / 2 |
| Scala total | 211 / 23 | 211 / 23 |
Python (ExcludeAIFunc) |
26 passed, 2 skipped | 26 passed, 2 skipped |
Identical counts on both sides. The scala leg's step list contains no Accept Anaconda TOS, no Create Internal conda env, and no conda-feed auth — confirming on a real agent what SynapseML-Internal/templates/scala_test_job.yml implies via useConda: false for the aifunc / ebm / predict suites.
Per-leg step breakdown
=== InternalCompat scala : succeeded TOTAL 38.2m ===
Publish OSS to local Maven 8.5m
Compile Internal against OSS 1.6m
Free disk space 0.9m
Run Internal Scala tests 25.1m <- 211 tests
(no conda steps)
=== InternalCompat python : succeeded TOTAL 38.7m ===
Publish OSS to local Maven 7.9m
Compile Internal against OSS 1.4m
Create Internal conda env 7.0m
Package Internal Python against OSS 0.9m
Run Internal Python tests (ExcludeAIFunc) 18.1m <- 26 passed / 2 skipped
Honest scope of the win
This shortens the InternalCompat signal, not necessarily total PR turnaround:
baseline 231549771 |
this build 231589470 |
|
|---|---|---|
| InternalCompat finishes at | 72.1m — was the critical path | 47.7m |
Databricks GPU E2E finishes at |
61.9m | 79.4m — now the critical path |
| Build total | 72.4m | 79.6m |
InternalCompat is now off the critical path — its answer arrives ~24 min earlier. The build total rose in this particular run because Databricks GPU E2E independently went 58.7m → 71.5m and every job started ~5-10 min later (heavier agent-queue contention that run). Neither is related to this diff, which touches only the InternalCompat job. Databricks GPU E2E is the next bottleneck worth attacking.
Cost
Two agents re-pay the ~12 min shared prefix (checkout + Publish OSS to local Maven + Compile Internal against OSS), so agent-minutes go 67.6 → 76.9 (+9.3, +14%). That is the deliberate trade: +14% agent-minutes for −43% latency on the slowest signal in the build. It is also why this stops at 2 legs — a 3rd would buy ~3 min for another full prefix.
Addresses the Copilot review comment on #2658: after splitting InternalCompat into scala/python matrix legs, the Key Vault steps still ran in both lanes. Only the Scala lane consumes them. "Run Internal Scala tests" maps the four sempy-integration-* values into its env block; nothing in the Python lane reads them. ADO exposes Key Vault values as secret variables, which are never auto-exported into a script environment, and the Python steps declare no env block. sbt's project/Secrets.scala is inert here as well: every accessor is gated behind SYNAPSEML_ENABLE_PUBLISH (unset in this job) and it shells out to az directly rather than reading pipeline variables. Fetching them in the Python lane therefore materialised a downloaded certificate and the mmlspark-keys secrets on an agent that never used them -- an exposure the serial job did not have, since it had only one agent. templates/fabric_kv.yml gains a "condition" parameter defaulting to succeeded(), which is the implicit step condition, so the Fabric E2E job that includes it without parameters is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
What
Splits the
SynapseML-Internal Compatibility Check(InternalCompat) job into two parallel matrix legs —scalaandpython.Measured: 67.6 min → 39.2 min (−28.4 min, −42%), with identical test counts on both sides.
Rebased onto
masterafter #2655 merged. Two commits, two files, +74 / −9.Note
Replaces #2657, which was opened from a fork.
InternalCompatis guarded byne(variables['System.PullRequest.IsFork'], 'True')— because SynapseML-Internal is private and fork PRs cannot authenticate to it — so on that PR the job was skipped entirely and the change went unexercised. This branch lives onmicrosoft/SynapseMLso the job actually runs.Results
Two full runs of this branch, against the serial baseline:
231549771
231589470
231600597
scalalegpythonlegTest counts are unchanged — this buys latency, it does not skip work:
spark.aifuncebmpredictExcludeAIFuncMeasured step breakdown (run 2)
Scope of the win — this is not 28 min off every PR
InternalCompatwas the critical path in the baseline; it no longer is:231549771231589470InternalCompatfinishes atDatabricks GPU E2Efinishes atThe
InternalCompatanswer now arrives ~24 min earlier and the job is off the critical path. Total build time in run 1 was higher, becauseDatabricks GPU E2Eindependently went 58.7 → 71.5 min and every job in that run started 5–10 min later under heavier agent-queue contention. Neither is related to this diff, which touches onlyInternalCompat.Databricks GPU E2Eis the next bottleneck worth attacking.Why
Its Scala and Python halves are independent — neither reads the other's output — and only ran back to back because they happened to share one checkout.
Create Internal conda envspark.aifunc22.8m +ebm2.5m +predict0.4mtestPythonExcludeAIFunc19.2mHow
This mirrors what SynapseML-Internal's own pipeline already does — it shards into 6 Scala jobs and 8 Python jobs via
templates/scala_test_job.ymlandtemplates/python_test_job.yml.Shared steps stay ungated; lane-specific steps get
eq(variables['COMPAT_LANE'], ...).The Scala leg skips conda env creation (−6.3 min, measured). This was not a guess: SynapseML-Internal runs
ScalaAIFuncTests*,ScalaEBMTestsandScalaPredictTestswithuseCondaunset, reserving that env for its PowerBI and nbtest jobs — exactly the three packages this leg runs. Both runs confirm it: thescalaleg has no conda step and still executes all 211 tests. Activation is guarded on the env actually existing, so the step still works unchanged if the env is ever reintroduced.Key Vault secrets are fetched only in the Scala leg (second commit, in response to review).
Run Internal Scala testsis the sole consumer — it maps the foursempy-integration-*values into itsenv:block. Nothing in the Python lane reads them: ADO exposes Key Vault values as secret variables, which are never auto-exported into a script environment, and the Python steps declare noenv:block; sbt'sproject/Secrets.scalais inert here too (every accessor is behindSYNAPSEML_ENABLE_PUBLISH, unset in this job) and shells out toazdirectly. Left as-is, the split would have materialised a downloaded certificate and themmlspark-keyssecrets on an agent that never used them — an exposure the serial job did not have, since it had only one agent.templates/fabric_kv.ymlgains aconditionparameter defaulting tosucceeded()— the implicit step condition — because a- template:reference cannot carry acondition:, and gating only themmlspark-keystask would have broken the Python leg, which resolvesKeyVaultName: '$(fabric-test-kv-name)'from it. The Fabric E2E job includes the template without parameters and is therefore unchanged.Why two legs and not more
Every leg re-pays the ~12.5 min shared prefix, so sharding has sharply diminishing returns:
spark.aifuncfromebm+predict)The trade is +9 agent-minutes (+13%) — the duplicated prefix — for −28 min of latency. A third leg would buy ~1 min for another full prefix. Internal can shard 6-and-8 ways because its legs resolve a published OSS artifact rather than building one from the change under test, so they have no comparable prefix to amortise.
Validation
Executed, not just reviewed:
bash -n.lane=scalaruns 14 / skips 7,lane=pythonruns 18 / skips 3.fabric_kv.ymlhas exactly two includes — one gated toscala(InternalCompat), one withparameters=None(Fabric E2E, defaultsucceeded()), proving Fabric E2E is unaffected.conda env listshapes — env present, present-and-active (*), absent, conda missing entirely, and a similar-but-different name (synapseml-internal-old) — plus aset -esafety check. All 5 correct.Risk
InternalCompatkeepscontinueOnError: trueand is not a required status check, so the new per-leg check names (... Compatibility Check scala/... python) do not affect branch protection.succeededOrFailed()back tosucceeded(). That condition existed only to keep Python running when the Scala tests failed in the same job; the split now guarantees that structurally.Not done here
The ~12.5 min shared prefix is now the floor, and
Databricks GPU E2Eis the build's new critical path. Caching the Internal conda env, or publishing the OSS M2 artifacts once and fanning out, would attack the prefix — but both add serialisation and belong in their own change with their own measurements.