@@ -133,6 +133,12 @@ SLURM_INFRA_RETRY_MAX = 1
133133// to avoid nesting with the inner SLURM retry.
134134K8S_INFRA_RETRY_MAX = 1
135135
136+ // Fallback discriminator for SLURM timeouts.
137+ // If we can't reach the SLURM node for an authoritative reason,
138+ // we apply a heuristic: if the job needed more than this of its budget
139+ // to fail, we treat it as a timeout.
140+ SLURM_TIMEOUT_RETRY_FRACTION = 0.9
141+
136142// Typed-exception hierarchy and FailureClassifier (PATTERN_CATALOG, classify(),
137143// flattenThrowable) live in trtllm-jenkins-shared-lib under src/trtllm/. They
138144// were originally inline here, but the Jenkins script-security sandbox
@@ -585,6 +591,39 @@ def cleanUpNodeResources(def pipeline, SlurmCluster cluster, String clusterName,
585591 }
586592}
587593
594+ // Authoritative timeout signal: ask the SLURM controller (via sacct on the
595+ // cluster login node) for a job's terminal state. Returns the uppercased
596+ // primary state token -- e.g. "TIMEOUT", "COMPLETED", "FAILED", "NODE_FAIL",
597+ // "OUT_OF_MEMORY", "CANCELLED" -- or null when it can't be determined.
598+ //
599+ // Best-effort: any SSH/sacct error returns null so callers fall back to the
600+ // duration heuristic rather than failing the stage.
601+ def querySlurmJobState (def pipeline , SlurmCluster cluster , String clusterName , String slurmJobID ) {
602+ if (! slurmJobID || ! slurmJobID. toString(). isNumber()) {
603+ return null
604+ }
605+ String state = null
606+ try {
607+ CloudManager . withSlurmSshCredentials(pipeline, clusterName, cluster) { remote ->
608+ // -X: allocation row only (skip .batch/.extern steps). -Pn:
609+ // parsable, no header. First line's first token is the job state;
610+ // SLURM renders cancellations as "CANCELLED by <uid>", so we keep
611+ // only the leading token.
612+ def out = Utils . exec(
613+ pipeline,
614+ script : Utils . sshUserCmd(remote, " \" sacct -j ${ slurmJobID} --format=State -Pn -X || true\" " ),
615+ returnStdout : true ,
616+ )?. trim()
617+ if (out) {
618+ state = out. readLines()[0 ]?. trim()?. tokenize(' ' )?. getAt(0 )?. toUpperCase(java.util.Locale . ROOT )
619+ }
620+ }
621+ } catch (Exception e) {
622+ pipeline. echo(" [INFRA-RETRY] Could not query SLURM job ${ slurmJobID} state via sacct: ${ e.message} " )
623+ }
624+ return state
625+ }
626+
588627def runLLMTestlistWithAgent (pipeline , platform , testList , config = VANILLA_CONFIG , perfMode = false , stageName = " Undefined" , splitId = 1 , splits = 1 , gpuCount = 1 , skipInstallWheel = false , cpver = " cp312" , String postTag = " " , boolean useClusterDurations = false )
589628{
590629 SlurmPartition partition = SlurmConfig . resolvePlatform(platform)
@@ -653,6 +692,12 @@ def runLLMTestlistWithAgent(pipeline, platform, testList, config=VANILLA_CONFIG,
653692 }
654693 }
655694
695+ // Wall-clock at which the SLURM job was first observed RUNNING.
696+ // Captured locally so it remains usable even when the controller
697+ // is later unreachable (the case the fallback exists for). Null until
698+ // Phase 1 confirms RUNNING; callers fall back to executeStartMs.
699+ def jobRunningStartMs = null
700+
656701 stage(' Check If Node Is Online' ) {
657702 CloudManager . withSlurmSshCredentials(pipeline, partition. clusterName, cluster) { remote ->
658703 // Check the SLURM job once; if it is no longer active, raise a typed
@@ -723,6 +768,10 @@ def runLLMTestlistWithAgent(pipeline, platform, testList, config=VANILLA_CONFIG,
723768 // Ocean, and probe job status every ~3 min (every 6th iter) to fail fast if the
724769 // job dies during bring-up. 120 * 30s = 1h.
725770 if (waitRc == 0 ) {
771+ // Job is RUNNING: stamp the walltime-budget origin for the
772+ // timeout duration fallback (within Phase 1's ~3min poll
773+ // granularity of the true RUNNING transition).
774+ jobRunningStartMs = System . currentTimeMillis()
726775 def onlineCounter = 0
727776 while (! CloudManager . isNodeOnline(nodeName) && onlineCounter < 120 ) {
728777 Thread . sleep(30L * 1000L )
@@ -804,7 +853,43 @@ def runLLMTestlistWithAgent(pipeline, platform, testList, config=VANILLA_CONFIG,
804853 } else {
805854 throw new Exception (" Unsupported container runtime: ${ cluster.containerRuntime} " )
806855 }
807- executeLLMTestOnSlurm(pipeline, platform, testList, config, perfMode, stageName, splitId, splits, skipInstallWheel, cpver, slurmRunner, postTag, useClusterDurations)
856+ long executeStartMs = System . currentTimeMillis()
857+ try {
858+ executeLLMTestOnSlurm(pipeline, platform, testList, config, perfMode, stageName, splitId, splits, skipInstallWheel, cpver, slurmRunner, postTag, useClusterDurations)
859+ } catch (InterruptedException e) {
860+ throw e
861+ } catch (Exception e) {
862+ // Decide whether this failure is a SLURM timeout (the test ran to
863+ // its partition walltime) rather than a transient infra blip.
864+ // Measure elapsed from when the job was first observed RUNNING.
865+ // Fall back to executeStartMs if the RUNNING stamp was never set.
866+ long timeoutBaselineMs = (jobRunningStartMs ?: executeStartMs) as long
867+ long elapsedMin = Math . floorDiv(System . currentTimeMillis() - timeoutBaselineMs, 60000L )
868+ Integer walltimeMin = (partition?. time ?: SlurmConfig . DEFAULT_TIMEOUT_SHORT ) as Integer
869+ def slurmState = querySlurmJobState(pipeline, cluster, partition. clusterName, slurmJobID)
870+
871+ if (slurmState == " TIMEOUT" ) {
872+ throw new UserFailure (
873+ " SLURM job ${ slurmJobID} for ${ stageName} ended in state TIMEOUT " +
874+ " (hit partition walltime ${ walltimeMin} min); treating as a test timeout, not retrying. " +
875+ " Original failure: ${ e.message} " ,
876+ e)
877+ }
878+
879+ if (slurmState == null && walltimeMin != null
880+ && elapsedMin >= (long )(SLURM_TIMEOUT_RETRY_FRACTION * walltimeMin)) {
881+ throw new UserFailure (
882+ " SLURM job ${ slurmJobID} for ${ stageName} ran ${ elapsedMin} min " +
883+ " (>= ${ (int)(SLURM_TIMEOUT_RETRY_FRACTION * 100)} % of the ${ walltimeMin} min walltime) and " +
884+ " the SLURM controller was unreachable for an authoritative state; treating as a likely " +
885+ " timeout, not retrying. Original failure: ${ e.message} " ,
886+ e)
887+ }
888+
889+ echo " [INFRA-RETRY] ${ stageName} : SLURM job ${ slurmJobID} terminal state=${ slurmState ?: 'unknown'} , " +
890+ " ran ${ elapsedMin} min of ${ walltimeMin} min walltime; deferring to failure classifier."
891+ throw e
892+ }
808893 } finally {
809894 stage(" Clean Up Slurm Resource" ) {
810895 // Workaround to handle the interruption during clean up SLURM resources
@@ -1460,15 +1545,40 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
14601545 )
14611546
14621547 // Track the Slurm job
1463- Utils . exec(
1464- pipeline,
1465- timeout : false ,
1466- script : Utils . sshUserCmd(
1467- remote,
1468- scriptTrackPathNode
1469- ),
1470- numRetries : 3
1471- )
1548+ try {
1549+ Utils . exec(
1550+ pipeline,
1551+ timeout : false ,
1552+ script : Utils . sshUserCmd(
1553+ remote,
1554+ scriptTrackPathNode
1555+ ),
1556+ numRetries : 3
1557+ )
1558+ } catch (InterruptedException e) {
1559+ throw e
1560+ } catch (Exception e) {
1561+ // The track script squashes the job's terminal SLURM state to
1562+ // exit 0/1, so a walltime kill is indistinguishable here from a
1563+ // real test failure. Re-query sacct for the allocation-level
1564+ // state (SLURM already aggregates it across nodes; TIMEOUT if
1565+ // any node hit the walltime) and, when it is TIMEOUT, raise a
1566+ // typed UserFailure so neither the SLURM retry loop nor the
1567+ // outer K8s pod retry re-runs a job that would just time out
1568+ // again. srun --kill-on-bad-exit=1 means a genuine test failure
1569+ // surfaces as FAILED, not TIMEOUT, so this stays unambiguous.
1570+ def slurmState = querySlurmJobState(pipeline, cluster, partition. clusterName, slurmJobId)
1571+ if (slurmState == " TIMEOUT" ) {
1572+ throw new UserFailure (
1573+ " SLURM job ${ slurmJobId} for ${ stageName} ended in state TIMEOUT " +
1574+ " (hit partition walltime ${ partition?.time} min); treating as a test timeout, not retrying. " +
1575+ " Original failure: ${ e.message} " ,
1576+ e)
1577+ }
1578+ echo " [INFRA-RETRY] ${ stageName} : SLURM job ${ slurmJobId} terminal state=${ slurmState ?: 'unknown'} ; " +
1579+ " deferring to failure classifier."
1580+ throw e
1581+ }
14721582 }
14731583 echo " Finished test stage execution."
14741584 } // end CloudManager.withSlurmSshCredentials
0 commit comments