@@ -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}
0 commit comments