Skip to content

Commit 5fd0a80

Browse files
authored
perf: split Databricks E2E into 3 parallel CPU partitions, tune timeouts (#2527)
Split the monolithic DatabricksCPUTests (~43 min) into 3 partitions (DatabricksCPUTests1/2/3) that run as separate ADO matrix entries. Each partition creates its own Databricks cluster and runs ~15 notebooks, with all 3 starting simultaneously on different ADO agents. Changes: - DatabricksCPUTests.scala: Split into 3 test classes using partition indices - DatabricksUtilities.scala: Add cpuNotebookPartition() with deterministic absolute-path-based sorting, add timeoutSeconds parameter threading - DatabricksGPUTests.scala: Increase GPU workers 2->3 and concurrency 1->3 (GPU notebooks are independent; parallel execution reduces E2E wall clock) Add 30-minute GPU-specific timeout override - pipeline.yaml: Replace single databricks-cpu with cpu-1/cpu-2/cpu-3 matrix
1 parent 726b44a commit 5fd0a80

4 files changed

Lines changed: 63 additions & 29 deletions

File tree

core/src/test/scala/com/microsoft/azure/synapse/ml/nbtest/DatabricksCPUTests.scala

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,31 @@ package com.microsoft.azure.synapse.ml.nbtest
55

66
import com.microsoft.azure.synapse.ml.nbtest.DatabricksUtilities._
77

8+
import java.time.LocalDateTime
89
import scala.language.existentials
910

10-
class DatabricksCPUTests extends DatabricksTestHelper {
11+
// Split CPU E2E tests into 3 partitions that run as separate ADO matrix entries.
12+
// Each partition creates its own cluster and contains ~15 notebooks,
13+
// running up to the configured worker concurrency.
14+
// All 3 partitions start simultaneously on different ADO agents.
1115

12-
val clusterId: String = createClusterInPool(ClusterName, AdbRuntime, NumWorkers, PoolId, memory = Some("7g"))
13-
14-
databricksTestHelper(clusterId, Libraries, CPUNotebooks, 5)
16+
class DatabricksCPUTests1 extends DatabricksTestHelper {
17+
private val clusterName = s"mmlspark-build-cpu1-${LocalDateTime.now()}"
18+
val clusterId: String = createClusterInPool(clusterName, AdbRuntime, NumWorkers, PoolId, memory = Some("7g"))
19+
databricksTestHelper(clusterId, Libraries, cpuNotebookPartition(0), NumWorkers)
20+
protected override def afterAll(): Unit = { afterAllHelper(clusterId, clusterName); super.afterAll() }
21+
}
1522

16-
protected override def afterAll(): Unit = {
17-
afterAllHelper(clusterId, ClusterName)
18-
super.afterAll()
19-
}
23+
class DatabricksCPUTests2 extends DatabricksTestHelper {
24+
private val clusterName = s"mmlspark-build-cpu2-${LocalDateTime.now()}"
25+
val clusterId: String = createClusterInPool(clusterName, AdbRuntime, NumWorkers, PoolId, memory = Some("7g"))
26+
databricksTestHelper(clusterId, Libraries, cpuNotebookPartition(1), NumWorkers)
27+
protected override def afterAll(): Unit = { afterAllHelper(clusterId, clusterName); super.afterAll() }
28+
}
2029

21-
ignore("list running jobs for convenience") {
22-
val obj = databricksGet("jobs/runs/list?active_only=true&limit=1000")
23-
println(obj)
24-
}
30+
class DatabricksCPUTests3 extends DatabricksTestHelper {
31+
private val clusterName = s"mmlspark-build-cpu3-${LocalDateTime.now()}"
32+
val clusterId: String = createClusterInPool(clusterName, AdbRuntime, NumWorkers, PoolId, memory = Some("7g"))
33+
databricksTestHelper(clusterId, Libraries, cpuNotebookPartition(2), NumWorkers)
34+
protected override def afterAll(): Unit = { afterAllHelper(clusterId, clusterName); super.afterAll() }
2535
}

core/src/test/scala/com/microsoft/azure/synapse/ml/nbtest/DatabricksGPUTests.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import com.microsoft.azure.synapse.ml.nbtest.DatabricksUtilities._
77

88
class DatabricksGPUTests extends DatabricksTestHelper {
99

10-
val clusterId: String = createClusterInPool(GPUClusterName, AdbGpuRuntime, 2, GpuPoolId)
10+
// GPU fine-tuning notebooks can take up to 25 min; use 30 min timeout
11+
private val gpuTimeoutMs = 30 * 60 * 1000
1112

12-
databricksTestHelper(clusterId, GPULibraries, GPUNotebooks, 1, List())
13+
val clusterId: String = createClusterInPool(GPUClusterName, AdbGpuRuntime, 3, GpuPoolId)
14+
15+
databricksTestHelper(clusterId, GPULibraries, GPUNotebooks, 3, List(), gpuTimeoutMs)
1316

1417
protected override def afterAll(): Unit = {
1518
afterAllHelper(clusterId, GPUClusterName)

core/src/test/scala/com/microsoft/azure/synapse/ml/nbtest/DatabricksUtilities.scala

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ object DatabricksUtilities {
9797
).toJson.compactPrint
9898

9999
// Execution Params
100-
val TimeoutInMillis: Int = 50 * 60 * 1000
100+
// Per-notebook timeout: 20 min for most notebooks.
101+
// Observed max actual execution time is ~7 min for CPU notebooks.
102+
// GPU fine-tuning notebooks can take up to 25 min, so GPU tests override this.
103+
val TimeoutInMillis: Int = 20 * 60 * 1000
101104

102105
val DocsDir = FileUtilities.join(BuildInfo.baseDirectory.getParent, "docs").getCanonicalFile()
103106
val NotebookFiles: Array[File] = FileUtilities.recursiveListFiles(DocsDir)
@@ -119,6 +122,15 @@ object DatabricksUtilities {
119122
.filterNot(_.getAbsolutePath.contains("Flooding Risk")) // Azure Maps Spatial API retired 9/30/2025
120123
.filterNot(_.getAbsolutePath.contains("Geospatial Services")) // Azure Maps Spatial API retired 9/30/2025
121124

125+
// Split CPU notebooks into 3 partitions for parallel ADO matrix jobs.
126+
// Each partition creates its own cluster, so all 3 run simultaneously.
127+
// Sort by absolute path for stable, deterministic partitioning across machines.
128+
private val SortedCPUNotebooks = CPUNotebooks.sortBy(_.getAbsolutePath)
129+
val NumCPUPartitions = 3
130+
def cpuNotebookPartition(partIndex: Int): Seq[File] = {
131+
SortedCPUNotebooks.zipWithIndex.filter(_._2 % NumCPUPartitions == partIndex).map(_._1)
132+
}
133+
122134
val GPUNotebooks: Seq[File] = ParallelizableNotebooks.filter { file =>
123135
file.getAbsolutePath.contains("Fine-tune") || file.getAbsolutePath.contains("Phi Model")
124136
}
@@ -273,13 +285,14 @@ object DatabricksUtilities {
273285
()
274286
}
275287

276-
def submitRun(clusterId: String, notebookPath: String): Long = {
288+
def submitRun(clusterId: String, notebookPath: String,
289+
timeoutSeconds: Int = TimeoutInMillis / 1000): Long = {
277290
val body =
278291
s"""
279292
|{
280293
| "run_name": "test1",
281294
| "existing_cluster_id": "$clusterId",
282-
| "timeout_seconds": ${TimeoutInMillis / 1000},
295+
| "timeout_seconds": $timeoutSeconds,
283296
| "notebook_task": {
284297
| "notebook_path": "$notebookPath",
285298
| "base_parameters": []
@@ -367,15 +380,16 @@ object DatabricksUtilities {
367380
}
368381
}
369382

370-
def runNotebook(clusterId: String, notebookFile: File): Unit = {
383+
def runNotebook(clusterId: String, notebookFile: File,
384+
timeoutSeconds: Int = TimeoutInMillis / 1000): Unit = {
371385
val dirPaths = DocsDir.toURI.relativize(notebookFile.getParentFile.toURI).getPath
372386
val folderToCreate = Folder + "/" + dirPaths
373387
println(s"Creating folder $folderToCreate")
374388
workspaceMkDir(folderToCreate)
375389
val destination: String = folderToCreate + notebookFile.getName
376390
uploadNotebook(notebookFile, destination)
377-
val runId: Long = submitRun(clusterId, destination)
378-
val run: DatabricksNotebookRun = DatabricksNotebookRun(runId, notebookFile.getName)
391+
val runId: Long = submitRun(clusterId, destination, timeoutSeconds)
392+
val run: DatabricksNotebookRun = DatabricksNotebookRun(runId, notebookFile.getName, timeoutSeconds * 1000)
379393
println(s"Successfully submitted job run id ${run.runId} for notebook ${run.notebookName}")
380394
DatabricksState.JobIdsToCancel.append(run.runId)
381395
run.monitor(logLevel = 0)
@@ -439,17 +453,20 @@ abstract class DatabricksTestHelper extends TestBase {
439453
libraries: String,
440454
notebooks: Seq[File],
441455
maxConcurrency: Int,
442-
retries: List[Int] = List(1000 * 15)): Unit = {
456+
retries: List[Int] = List(1000 * 15),
457+
timeoutMs: Int = TimeoutInMillis): Unit = {
443458

444459
println("Checking if cluster is active")
445-
tryWithRetries(Seq.fill(60 * 20)(1000).toArray) { () =>
460+
// Pool-backed clusters start in ~1.5-3.5 min; allow up to 10 min
461+
tryWithRetries(Seq.fill(60 * 10)(1000).toArray) { () =>
446462
assert(isClusterActive(clusterId))
447463
}
448464

449465
Thread.sleep(1000) // Ensure cluster is not overwhelmed
450466
println("Installing libraries")
451467
installLibraries(clusterId, libraries)
452-
tryWithRetries(Seq.fill(60 * 6)(1000).toArray) { () =>
468+
// Library install typically takes ~1.5-3 min; allow up to 10 min
469+
tryWithRetries(Seq.fill(60 * 10)(1000).toArray) { () =>
453470
assert(areLibrariesInstalled(clusterId))
454471
}
455472

@@ -461,13 +478,13 @@ abstract class DatabricksTestHelper extends TestBase {
461478
val futures = notebooks.map { notebook =>
462479
Future {
463480
retry(retries, { () =>
464-
runNotebook(clusterId, notebook)
481+
runNotebook(clusterId, notebook, timeoutMs / 1000)
465482
})
466483
}
467484
}
468485
futures.zip(notebooks).foreach { case (f, nb) =>
469486
test(nb.getName) {
470-
Await.result(f, Duration(TimeoutInMillis.toLong, TimeUnit.MILLISECONDS))
487+
Await.result(f, Duration(timeoutMs.toLong, TimeUnit.MILLISECONDS))
471488
}
472489
}
473490

@@ -482,8 +499,8 @@ abstract class DatabricksTestHelper extends TestBase {
482499
}
483500
}
484501

485-
case class DatabricksNotebookRun(runId: Long, notebookName: String) {
502+
case class DatabricksNotebookRun(runId: Long, notebookName: String, timeoutMs: Int = TimeoutInMillis) {
486503
def monitor(logLevel: Int = 2): Unit = {
487-
monitorJob(runId, TimeoutInMillis, logLevel)
504+
monitorJob(runId, timeoutMs, logLevel)
488505
}
489506
}

pipeline.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,8 +174,12 @@ jobs:
174174
vmImage: $(UBUNTU_VERSION)
175175
strategy:
176176
matrix:
177-
databricks-cpu:
178-
TEST-CLASS: "com.microsoft.azure.synapse.ml.nbtest.DatabricksCPUTests"
177+
databricks-cpu-1:
178+
TEST-CLASS: "com.microsoft.azure.synapse.ml.nbtest.DatabricksCPUTests1"
179+
databricks-cpu-2:
180+
TEST-CLASS: "com.microsoft.azure.synapse.ml.nbtest.DatabricksCPUTests2"
181+
databricks-cpu-3:
182+
TEST-CLASS: "com.microsoft.azure.synapse.ml.nbtest.DatabricksCPUTests3"
179183
databricks-gpu:
180184
TEST-CLASS: "com.microsoft.azure.synapse.ml.nbtest.DatabricksGPUTests"
181185
# databricks-rapids tests have been disabled because these tests are failing.

0 commit comments

Comments
 (0)