Skip to content

Commit 21c6d5b

Browse files
authored
[TRTLLMINF-99][infra] Add SLURM frontend failover to L0 (NVIDIA#15674)
Signed-off-by: Derek Pitman <dpitman@nvidia.com>
1 parent e158837 commit 21c6d5b

1 file changed

Lines changed: 66 additions & 35 deletions

File tree

jenkins/L0_Test.groovy

Lines changed: 66 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,12 @@ def echoRemoteLogTail(def pipeline, Map remote, String remotePath, int lines = 2
228228
// the locally-staged result XMLs when the same stageName is uploaded more than
229229
// once in a build (e.g. SLURM infra-failure retries). First attempt passes "".
230230
def uploadResults(def pipeline, SlurmCluster cluster, String clusterName, String nodeName, String stageName, Boolean stageIsInterrupted, String postTag="") {
231-
CloudManager.withSlurmSshCredentials(pipeline, clusterName, cluster) { remote ->
231+
CloudManager.withSlurmSshCredentialRemotes(pipeline, clusterName, cluster) { remotes ->
232+
// Pin one reachable frontend for the whole collect: every download targets
233+
// the same node workspace (/home/svc_tensorrt/bloom/scripts/${nodeName}),
234+
// so the find + scps must all hit the login node that holds those files.
235+
// No whole-closure failover here -- uploadArtifacts/junit must not re-run.
236+
def remote = CloudManager.selectReachableSlurmRemote(pipeline, remotes)
232237
def hasTimeoutTest = false
233238
def downloadResultSucceed = false
234239
def downloadPerfResultSucceed = false
@@ -599,7 +604,7 @@ def processShardTestList(llmSrc, testDBList, splitId, splits, perfMode=false, du
599604
}
600605

601606
def cleanUpSlurmResources(def pipeline, SlurmCluster cluster, String clusterName, String jobUID){
602-
CloudManager.withSlurmSshCredentials(pipeline, clusterName, cluster) { remote ->
607+
CloudManager.withSlurmFrontendFailover(pipeline, clusterName, cluster) { remote ->
603608
def jobWorkspace = "/home/svc_tensorrt/bloom/scripts/${jobUID}"
604609

605610
Utils.exec(pipeline, script: "echo Sleeping to allow Slurm job completion; sleep 30")
@@ -642,7 +647,7 @@ def cleanUpSlurmResources(def pipeline, SlurmCluster cluster, String clusterName
642647
pipeline,
643648
script: Utils.sshUserCmd(
644649
remote,
645-
"\"${cleanupCommands}\""
650+
Utils.bashWrappedRemoteCmd(cleanupCommands)
646651
)
647652
)
648653

@@ -658,7 +663,7 @@ def cleanUpNodeResources(def pipeline, SlurmCluster cluster, String clusterName,
658663

659664
Utils.exec(pipeline, script: "echo Sleeping to allow node destruction; sleep 30")
660665

661-
CloudManager.withSlurmSshCredentials(pipeline, clusterName, cluster) { remote ->
666+
CloudManager.withSlurmFrontendFailover(pipeline, clusterName, cluster) { remote ->
662667
Utils.exec(pipeline, script: "echo Slurm job ID: ${slurmJobID}")
663668

664669
Utils.exec(
@@ -683,7 +688,7 @@ def cleanUpNodeResources(def pipeline, SlurmCluster cluster, String clusterName,
683688
pipeline,
684689
script: Utils.sshUserCmd(
685690
remote,
686-
"\"${cleanupCommands}\""
691+
Utils.bashWrappedRemoteCmd(cleanupCommands)
687692
)
688693
)
689694

@@ -704,7 +709,7 @@ def querySlurmJobState(def pipeline, SlurmCluster cluster, String clusterName, S
704709
}
705710
String state = null
706711
try {
707-
CloudManager.withSlurmSshCredentials(pipeline, clusterName, cluster) { remote ->
712+
CloudManager.withSlurmFrontendFailover(pipeline, clusterName, cluster) { remote ->
708713
// -X: allocation row only (skip .batch/.extern steps). -Pn:
709714
// parsable, no header. First line's first token is the job state;
710715
// SLURM renders cancellations as "CANCELLED by <uid>", so we keep
@@ -748,7 +753,7 @@ def runLLMTestlistWithAgent(pipeline, platform, testList, config=VANILLA_CONFIG,
748753

749754
try {
750755
// Run ssh command to start node in desired cluster via SLURM
751-
CloudManager.withSlurmSshCredentials(pipeline, partition.clusterName, cluster) { remote ->
756+
CloudManager.withSlurmFrontendFailover(pipeline, partition.clusterName, cluster) { remote ->
752757
stage('Request Node Via Slurm') {
753758
println("Selected Cluster: ${cluster.name}")
754759

@@ -817,12 +822,14 @@ def runLLMTestlistWithAgent(pipeline, platform, testList, config=VANILLA_CONFIG,
817822
def jobRunningStartMs = null
818823

819824
stage('Check If Node Is Online') {
820-
CloudManager.withSlurmSshCredentials(pipeline, partition.clusterName, cluster) { remote ->
825+
CloudManager.withSlurmSshCredentialRemotes(pipeline, partition.clusterName, cluster) { remotes ->
821826
// Check the SLURM job once; if it is no longer active, raise a typed
822827
// InfraFailure(SLURM) so the retry layer routes it via instanceof (scope=SLURM).
823828
def checkSlurmJobActive = {
824829
try {
825-
SlurmConfig.checkJobStatus(pipeline, cluster, slurmJobID, remote)
830+
CloudManager.withSlurmFrontendFailover(pipeline, remotes) { statusRemote ->
831+
SlurmConfig.checkJobStatus(pipeline, cluster, slurmJobID, statusRemote)
832+
}
826833
} catch (InterruptedException e) {
827834
throw e
828835
} catch (Exception e) {
@@ -845,8 +852,8 @@ def runLLMTestlistWithAgent(pipeline, platform, testList, config=VANILLA_CONFIG,
845852
// which overflowed the per-stage step cap). Release the held job every 10
846853
// iterations (~30 min). 300 iterations * 3 min = 15h budget.
847854
// Exit codes: 0 = job RUNNING, 3 = job no longer active, 4 = timed out.
848-
def sacctStateCmd = Utils.sshUserCmd(remote, "\"sacct -j ${slurmJobID} --format=State -Pn --allocations\"")
849-
def releaseCmd = Utils.sshUserCmd(remote, "\"scontrol release ${slurmJobID} || true\"")
855+
def sacctStateCmd = CloudManager.sshUserCmdWithSlurmFrontendFailover(remotes, "\"sacct -j ${slurmJobID} --format=State -Pn --allocations\"")
856+
def releaseCmd = CloudManager.sshUserCmdWithSlurmFrontendFailover(remotes, "\"scontrol release ${slurmJobID} || true\"")
850857
def waitRc = pipeline.sh(returnStatus: true, script: """
851858
set +e
852859
counter=0
@@ -967,7 +974,7 @@ def runLLMTestlistWithAgent(pipeline, platform, testList, config=VANILLA_CONFIG,
967974
def setupLogPath = "/home/svc_tensorrt/slurm-logs/slurm-${slurmJobID}-${nodeName}.out"
968975
def enrootLog = Utils.exec(
969976
pipeline,
970-
script: Utils.sshUserCmd(remote, "\"grep '\\[ENROOT\\]' ${setupLogPath} 2>/dev/null || true\""),
977+
script: CloudManager.sshUserCmdWithSlurmFrontendFailover(remotes, Utils.bashWrappedRemoteCmd("grep '\\[ENROOT\\]' ${setupLogPath} 2>/dev/null || true")),
971978
returnStdout: true,
972979
numRetries: 3
973980
).trim()
@@ -1252,7 +1259,7 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
12521259
string(credentialsId: 'TRTLLM_HF_TOKEN', variable: 'HF_TOKEN'),
12531260
string(credentialsId: 'svc_tensorrt-swift-stack-key', variable: 'S3_SECRET_KEY'),
12541261
]) {
1255-
CloudManager.withSlurmSshCredentials(pipeline, partition.clusterName, cluster) { remote ->
1262+
CloudManager.withSlurmFrontendFailover(pipeline, partition.clusterName, cluster) { remote ->
12561263
def tarName = BUILD_CONFIGS[config][TARNAME]
12571264
def llmTarfile = "https://urm.nvidia.com/artifactory/${ARTIFACT_PATH}/${tarName}"
12581265
def llmPath = sh (script: "realpath .", returnStdout: true).trim()
@@ -1323,11 +1330,20 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
13231330
def makoOptsJson = transformMakoArgsToJson(["Mako options:"] + makoArgs)
13241331
String clusterNameForDurations = useClusterDurations ? partition.clusterName.replaceAll('[^a-zA-Z0-9]', '_') : null
13251332
def testListPathLocal = renderTestDB(pipeline, testList, llmSrcLocal, stageName, makoOptsJson, clusterNameForDurations)
1333+
// Copy the test list atomically. A retry that reuses a still-active job
1334+
// re-copies over ${testListPathNode} while that job may be reading it via
1335+
// --test-list; scp truncates-then-streams, so a concurrent read could see a
1336+
// partial list and silently run a subset. Stage to a temp path and mv into
1337+
// place (same-dir rename is atomic) so a reader sees the whole old or new file.
13261338
Utils.copyFileToRemoteHost(
13271339
pipeline,
13281340
remote,
13291341
testListPathLocal,
1330-
testListPathNode
1342+
"${testListPathNode}.tmp"
1343+
)
1344+
Utils.exec(
1345+
pipeline,
1346+
script: Utils.sshUserCmd(remote, "\"mv -f ${testListPathNode}.tmp ${testListPathNode}\"")
13311347
)
13321348

13331349
// Download and Merge waives.txt
@@ -1671,13 +1687,26 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
16711687
set -xEeuo pipefail
16721688
trap 'rc=\$?; echo "Error in file \${BASH_SOURCE[0]} on line \$LINENO: \$BASH_COMMAND (exit \$rc)"; exit \$rc' ERR
16731689
1674-
# Clean up previous job intermediate files so that retry can work
1690+
# Reuse an already-active job after an ambiguous frontend disconnect.
16751691
if [ -f "${jobWorkspace}/slurm_job_id.txt" ]; then
16761692
previous_job_id=\$(cat "${jobWorkspace}/slurm_job_id.txt")
16771693
echo "Found previous Slurm job ID: \${previous_job_id}"
1678-
scancel "\${previous_job_id}" || true
1679-
# Wait for 120 seconds to ensure the previous job is canceled
1680-
sleep 120
1694+
previous_state=\$(sacct -j "\${previous_job_id}" --format=State -Pn --allocations 2>/dev/null | head -1 | cut -d'|' -f1 | awk '{print \$1}' || true)
1695+
if [ -z "\${previous_state}" ]; then
1696+
previous_state=\$(scontrol show job "\${previous_job_id}" 2>/dev/null | tr ' ' '\\n' | sed -n 's/^JobState=//p' | head -1 || true)
1697+
fi
1698+
case "\${previous_state}" in
1699+
RUNNING|PENDING|CONFIGURING|COMPLETING|REQUEUED|RESIZING|SUSPENDED|SIGNALING|STOPPED)
1700+
echo "Reusing active Slurm job \${previous_job_id} in state \${previous_state}"
1701+
exit 0
1702+
;;
1703+
*)
1704+
echo "Previous Slurm job \${previous_job_id} is not active (state='\${previous_state:-UNKNOWN}'). Cleaning it up before resubmission."
1705+
scancel "\${previous_job_id}" || true
1706+
# Wait for 120 seconds to ensure the previous job is canceled
1707+
sleep 120
1708+
;;
1709+
esac
16811710
fi
16821711
16831712
# Clean up workspace: remove all files/dirs not in the keep list
@@ -1706,14 +1735,15 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
17061735
}
17071736

17081737
stage("[${stageName}] Run Pytest") {
1709-
// Submit the Slurm job
1738+
// Submit the Slurm job. Submit/metadata/track all run on the one
1739+
// frontend the enclosing withSlurmFrontendFailover pinned, so they
1740+
// share the job workspace (slurm_job_id.txt, scripts) on that login
1741+
// node; a frontend disconnect fails the whole closure over to a fresh
1742+
// frontend as a unit (the submit script reuses an active job).
17101743
Utils.exec(
17111744
pipeline,
17121745
timeout: false,
1713-
script: Utils.sshUserCmd(
1714-
remote,
1715-
scriptSubmitPathNode
1716-
),
1746+
script: Utils.sshUserCmd(remote, scriptSubmitPathNode),
17171747
numRetries: 3
17181748
)
17191749

@@ -1722,10 +1752,7 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
17221752
if (!slurmJobId) {
17231753
slurmJobId = Utils.exec(
17241754
pipeline,
1725-
script: Utils.sshUserCmd(
1726-
remote,
1727-
"\"cat ${jobWorkspace}/slurm_job_id.txt\""
1728-
),
1755+
script: Utils.sshUserCmd(remote, "\"cat ${jobWorkspace}/slurm_job_id.txt\""),
17291756
returnStdout: true,
17301757
numRetries: 3
17311758
).trim()
@@ -1816,15 +1843,12 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
18161843
true
18171844
)
18181845

1819-
// Track the Slurm job
1846+
// Track the Slurm job on the same pinned frontend.
18201847
try {
18211848
Utils.exec(
18221849
pipeline,
18231850
timeout: false,
1824-
script: Utils.sshUserCmd(
1825-
remote,
1826-
scriptTrackPathNode
1827-
),
1851+
script: Utils.sshUserCmd(remote, scriptTrackPathNode),
18281852
numRetries: 3
18291853
)
18301854
} catch (InterruptedException e) {
@@ -1853,7 +1877,7 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
18531877
}
18541878
}
18551879
echo "Finished test stage execution."
1856-
} // end CloudManager.withSlurmSshCredentials
1880+
} // end CloudManager.withSlurmFrontendFailover
18571881
} // end withCredentials
18581882
} catch (InterruptedException e) {
18591883
stageIsInterrupted = true
@@ -2245,7 +2269,7 @@ def readSlurmWorkspaceFile(def pipeline, Map remote, String path, String stageNa
22452269
pipeline,
22462270
script: Utils.sshUserCmd(
22472271
remote,
2248-
"\"cat ${path} 2>/dev/null || true\""
2272+
Utils.bashWrappedRemoteCmd("cat ${path} 2>/dev/null || true")
22492273
),
22502274
returnStdout: true,
22512275
numRetries: numRetries
@@ -2254,6 +2278,13 @@ def readSlurmWorkspaceFile(def pipeline, Map remote, String path, String stageNa
22542278
} catch (InterruptedException e) {
22552279
throw e
22562280
} catch (Exception e) {
2281+
// A dead frontend must propagate so the enclosing withSlurmFrontendFailover
2282+
// fails over to another remote; swallowing it as "" would strand the stage on
2283+
// the unreachable frontend. Any other read failure (missing file, transient)
2284+
// is non-fatal -- the metadata is best-effort, so return "" and carry on.
2285+
if (CloudManager.isSlurmFrontendConnectionFailure(e)) {
2286+
throw e
2287+
}
22572288
echo "[INFRA-RETRY] ${stageName}: unable to read SLURM metadata file ${path}: ${e.toString()}"
22582289
return ""
22592290
}
@@ -2308,7 +2339,7 @@ def captureSlurmJobNodeList(def pipeline, SlurmCluster cluster, String clusterNa
23082339
def capturedJobID = slurmJobID
23092340
def nodeList = null
23102341
try {
2311-
CloudManager.withSlurmSshCredentials(pipeline, clusterName, cluster) { remote ->
2342+
CloudManager.withSlurmFrontendFailover(pipeline, clusterName, cluster) { remote ->
23122343
def metadata = captureSlurmWorkspaceMetadata(pipeline, remote, jobWorkspace, placementContext, stageName)
23132344
capturedJobID = capturedJobID ?: metadata.slurmJobId
23142345
nodeList = metadata.nodeList

0 commit comments

Comments
 (0)