NODEDC_MISSION_CORE/experiments/perception/worker/Invoke-E8RealtimeTracking.ps1

412 lines
16 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$JobRoot,
[Parameter(Mandatory = $true)]
[string]$RunnerPath,
[Parameter(Mandatory = $true)]
[string]$ProfilePath,
[Parameter(Mandatory = $true)]
[string]$ValidFovRoot,
[ValidateRange(0, 1000000)]
[int]$StartFrame = 1000,
[ValidateRange(0, 1000000)]
[int]$EndFrame = 1600,
[ValidateRange(1, 1000)]
[int]$FreeGiBFloor = 360,
[string]$RuntimeRoot = "D:\NDC_MISSIONCORE\runtime",
[string]$ModelRoot = "D:\NDC_MISSIONCORE\runtime\models\yolox_s",
[string]$TritonContainer = "mission-core-triton",
[string]$ContainerImage = "nvcr.io/nvidia/tritonserver:26.06-py3"
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
function Assert-LastExitCode {
param([string]$Operation)
if ($LASTEXITCODE -ne 0) {
throw "$Operation failed with exit code $LASTEXITCODE"
}
}
function Assert-RegularFile {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if (-not $item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a regular file"
}
function Assert-Directory {
param([string]$Path, [string]$Label)
$item = Get-Item -LiteralPath $Path -Force
if ($item.PSIsContainer -and -not ($item.Attributes -band [IO.FileAttributes]::ReparsePoint)) {
return $item.FullName
}
throw "$Label is not a real directory"
}
function Assert-DDrivePath {
param([string]$Path, [string]$Label)
$fullPath = [IO.Path]::GetFullPath($Path)
if ([IO.Path]::GetPathRoot($fullPath).TrimEnd("\") -ine "D:") {
throw "$Label must be stored on D:"
}
return $fullPath
}
function Convert-ToDockerPath {
param([string]$Path)
return $Path.Replace("\", "/")
}
function Get-DFreeBytes {
return [int64](Get-PSDrive -Name D).Free
}
function Assert-FreeSpace {
param([string]$Phase, [int64]$RequiredAdditionalBytes = 0)
$freeBytes = Get-DFreeBytes
$floorBytes = [int64]$FreeGiBFloor * 1GB
Write-Host (
"DISK_GUARD PHASE={0} DRIVE=D FREE_BYTES={1} FREE_GIB={2} FLOOR_GIB={3} REQUIRED_ADDITIONAL_GIB={4}" -f
$Phase,
$freeBytes,
[math]::Round($freeBytes / 1GB, 3),
$FreeGiBFloor,
[math]::Round($RequiredAdditionalBytes / 1GB, 3)
)
if ($freeBytes -lt ($floorBytes + $RequiredAdditionalBytes)) {
throw "D: does not have the guarded LAB E8 working-set reserve during $Phase"
}
return $freeBytes
}
function Test-TritonModelReady {
try {
$response = Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/models/yolox_s/ready" `
-Method Get -UseBasicParsing -TimeoutSec 10
return $response.StatusCode -eq 200
}
catch {
return $false
}
}
$jobDirectory = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $JobRoot).Path "Job root"
) "Job root"
$runner = Assert-DDrivePath (
Assert-RegularFile (Resolve-Path -LiteralPath $RunnerPath).Path "LAB E8 runner"
) "LAB E8 runner"
$profile = Assert-DDrivePath (
Assert-RegularFile (Resolve-Path -LiteralPath $ProfilePath).Path "LAB E8 profile"
) "LAB E8 profile"
$validFov = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $ValidFovRoot).Path "Valid-FOV root"
) "Valid-FOV root"
$runtime = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $RuntimeRoot).Path "Runtime root"
) "Runtime root"
$model = Assert-DDrivePath (
Assert-Directory (Resolve-Path -LiteralPath $ModelRoot).Path "YOLOX model root"
) "YOLOX model root"
$runnerRoot = Split-Path $runner -Parent
if ((Split-Path $profile -Parent) -ne $runnerRoot) {
throw "LAB E8 runner and profile must share one read-only mount"
}
foreach ($dependency in @(
"run_e5_instance_tracking.py",
"run_recorded_perception_epoch.py",
"run_e3_rectified_segmentation.py",
"run_evaluation_prelabels.py"
)) {
$null = Assert-RegularFile (Join-Path $runnerRoot $dependency) "LAB E8 runner dependency"
}
$job = Get-Content -LiteralPath (Join-Path $jobDirectory "job.json") -Raw | ConvertFrom-Json
if ($job.schema_version -ne "missioncore.compute-job/v1" -or $job.job_id -ne (Split-Path $jobDirectory -Leaf)) {
throw "Compute job manifest is incompatible"
}
$sourceId = [string]$job.input.source_id
$epoch = [int]$job.input.codec_epoch
$fullFrameCount = [int]$job.input.segment_count
$clipFrameCount = $EndFrame - $StartFrame + 1
$timelineStart = [double]$job.input.timeline.start_seconds
if (
$sourceId -ne "sensor.camera.right" -or
$StartFrame -lt 0 -or
$EndFrame -lt $StartFrame -or
$EndFrame -ge $fullFrameCount -or
$clipFrameCount -lt 2
) {
throw "LAB E8 clip escapes the camera job"
}
Write-Output (
"PHASE=job-manifest-validated JOB={0} CLIP={1}-{2} FRAMES={3}" -f
$job.job_id, $StartFrame, $EndFrame, $clipFrameCount
)
$epochRoot = Assert-DDrivePath (
Assert-Directory (
Join-Path $jobDirectory ("input\camera\{0}\epoch-{1}" -f $sourceId, $epoch)
) "Camera epoch"
) "Camera epoch"
$initPath = Assert-RegularFile (Join-Path $epochRoot "init.mp4") "Camera init"
$segmentsRoot = Assert-Directory (Join-Path $epochRoot "segments") "Camera segments"
$derivedRoot = Join-Path $runtime "derived"
$tmpRoot = Join-Path $runtime "tmp"
$pythonEnvironment = Assert-DDrivePath (
Assert-Directory (Join-Path $derivedRoot "perception-p0-env-v1") "Python environment"
) "Python environment"
$null = New-Item -ItemType Directory -Path $derivedRoot -Force
$null = New-Item -ItemType Directory -Path $tmpRoot -Force
# LAB E8 has temporary decoded input but no per-frame output images. Reserve
# decoded RGB payload, partial stream and two GiB for filesystem/codec overhead.
$pixelWorkingSet = [int64]$clipFrameCount * 800 * 600 * 3
$partialStreamReserve = [int64][math]::Ceiling(
([int64]$job.input.byte_length * ($EndFrame + 1) / $fullFrameCount) * 1.2
)
$workingSetReserve = [int64][math]::Ceiling($pixelWorkingSet * 1.4) + $partialStreamReserve + 2GB
$freeBytesBefore = Assert-FreeSpace "preflight" $workingSetReserve
& docker image inspect $ContainerImage *> $null
Assert-LastExitCode "Existing container image inspection"
$tritonState = docker inspect --format "{{.State.Running}}" $TritonContainer
Assert-LastExitCode "Triton container inspection"
if ($tritonState.Trim().ToLowerInvariant() -ne "true") {
throw "LAB E8 requires the existing Triton container"
}
$runnerName = Split-Path $runner -Leaf
$profileName = Split-Path $profile -Leaf
$orchestratorSha256 = (Get-FileHash -LiteralPath $PSCommandPath -Algorithm SHA256).Hash.ToLowerInvariant()
$preflightArgs = @(
"run", "--rm", "--network", "none", "--read-only",
"--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "128", "--tmpfs", "/tmp:rw,noexec,nosuid,size=256m",
"-e", "PYTHONPATH=/runner:/opt/env",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $pythonEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "preflight",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model"
)
Write-Output "PHASE=e8-preflight-start"
& docker @preflightArgs
Assert-LastExitCode "LAB E8 preflight"
Write-Output "PHASE=e8-preflight-complete"
$modelWasReady = Test-TritonModelReady
$loadedByRun = $false
if (-not $modelWasReady) {
Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/load" `
-Method Post -ContentType "application/json" -Body "{}" `
-UseBasicParsing -TimeoutSec 60 *> $null
$loadedByRun = $true
$deadline = [DateTime]::UtcNow.AddSeconds(60)
while (-not (Test-TritonModelReady)) {
if ([DateTime]::UtcNow -ge $deadline) {
throw "YOLOX-S did not become ready in Triton"
}
Start-Sleep -Milliseconds 500
}
}
Write-Output ("PHASE=e8-model-ready LOADED_BY_RUN={0}" -f $loadedByRun)
$runToken = [Guid]::NewGuid().ToString("N")
$workRoot = Join-Path $tmpRoot ("{0}-e8-{1}" -f $job.job_id, $runToken)
$framesRoot = Join-Path $workRoot "frames"
$streamPath = Join-Path $workRoot "camera.mp4"
$ptsPath = Join-Path $workRoot "pts.json"
$timelinePath = Join-Path $workRoot "timeline.jsonl"
$publishRoot = Join-Path $derivedRoot (".{0}-e8-{1}.publish" -f $job.job_id, $runToken)
$stagingRoot = Join-Path $publishRoot "output"
$null = New-Item -ItemType Directory -Path $framesRoot
$null = New-Item -ItemType Directory -Path $publishRoot
$totalWatch = [Diagnostics.Stopwatch]::StartNew()
$completed = $false
Write-Output "PHASE=e8-private-staging-created"
try {
$stream = [IO.File]::Open(
$streamPath, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None
)
try {
$input = [IO.File]::OpenRead($initPath)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
for ($sequence = 1; $sequence -le ($EndFrame + 1); $sequence++) {
$path = Assert-RegularFile (
Join-Path $segmentsRoot ("{0}.m4s" -f $sequence)
) "Camera segment"
$input = [IO.File]::OpenRead($path)
try { $input.CopyTo($stream) } finally { $input.Dispose() }
}
$stream.Flush($true)
}
finally {
$stream.Dispose()
}
$null = Assert-FreeSpace "post-stream-reconstruction"
$extractWatch = [Diagnostics.Stopwatch]::StartNew()
Write-Output "PHASE=e8-frame-extraction-start"
$selectFilter = "select='between(n,{0},{1})'" -f $StartFrame, $EndFrame
& ffmpeg -hide_banner -loglevel fatal -i $streamPath -map 0:v:0 `
-vf $selectFilter -fps_mode passthrough (Join-Path $framesRoot "frame-%06d.png")
Assert-LastExitCode "LAB E8 camera clip extraction"
& ffprobe -v error -select_streams v:0 `
-show_entries frame=best_effort_timestamp_time -of json $streamPath |
Set-Content -LiteralPath $ptsPath -Encoding utf8
Assert-LastExitCode "LAB E8 camera timestamp probe"
$decodedFrames = @(Get-ChildItem -LiteralPath $framesRoot -File -Filter "frame-*.png")
$ptsDocument = Get-Content -LiteralPath $ptsPath -Raw | ConvertFrom-Json
$pts = @($ptsDocument.frames)
if ($decodedFrames.Count -ne $clipFrameCount -or $pts.Count -ne ($EndFrame + 1)) {
throw "Decoded LAB E8 frame/timestamp count differs from the selected clip"
}
$firstEpochSeconds = [double]::Parse(
([string]$pts[0].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
)
$previousSessionSeconds = -1.0
$timelineWriter = [IO.StreamWriter]::new(
$timelinePath, $false, [Text.UTF8Encoding]::new($false)
)
try {
for ($localIndex = 0; $localIndex -lt $clipFrameCount; $localIndex++) {
$sourceIndex = $StartFrame + $localIndex
$epochSeconds = [double]::Parse(
([string]$pts[$sourceIndex].best_effort_timestamp_time).Trim(),
[Globalization.CultureInfo]::InvariantCulture
) - $firstEpochSeconds
$sessionSeconds = $timelineStart + $epochSeconds
if ($sessionSeconds -le $previousSessionSeconds) {
throw "Decoded LAB E8 clip timestamps are not strictly monotonic"
}
$row = [ordered]@{
frame_index = $localIndex
sequence = $localIndex + 1
source_frame_index = $sourceIndex
source_sequence = $sourceIndex + 1
epoch_seconds = $epochSeconds
session_seconds = $sessionSeconds
}
$timelineWriter.WriteLine(($row | ConvertTo-Json -Compress))
$previousSessionSeconds = $sessionSeconds
}
$timelineWriter.Flush()
}
finally {
$timelineWriter.Dispose()
}
$extractWatch.Stop()
$freeBytesPostExtract = Assert-FreeSpace "post-frame-extraction"
Write-Output ("PHASE=e8-frame-extraction-complete FRAMES={0}" -f $clipFrameCount)
$runArgs = @(
"run", "--rm", "--gpus", "all",
"--network", ("container:{0}" -f $TritonContainer),
"--read-only", "--security-opt", "no-new-privileges:true", "--cap-drop", "ALL",
"--pids-limit", "256", "--shm-size", "1g",
"--tmpfs", "/tmp:rw,noexec,nosuid,size=512m",
"-e", "PYTHONPATH=/runner:/opt/env",
"-v", ((Convert-ToDockerPath $jobDirectory) + ":/job:ro"),
"-v", ((Convert-ToDockerPath $validFov) + ":/valid-fov:ro"),
"-v", ((Convert-ToDockerPath $model) + ":/model:ro"),
"-v", ((Convert-ToDockerPath $pythonEnvironment) + ":/opt/env:ro"),
"-v", ((Convert-ToDockerPath $framesRoot) + ":/frames:ro"),
"-v", ((Convert-ToDockerPath $workRoot) + ":/work:ro"),
"-v", ((Convert-ToDockerPath $publishRoot) + ":/publish:rw"),
"-v", ((Convert-ToDockerPath $runnerRoot) + ":/runner:ro"),
"--entrypoint", "python3", $ContainerImage,
("/runner/{0}" -f $runnerName), "run",
"--job", "/job",
"--profile", ("/runner/{0}" -f $profileName),
"--valid-fov-root", "/valid-fov",
"--model-root", "/model",
"--frames", "/frames",
"--timeline", "/work/timeline.jsonl",
"--output", "/publish/output",
"--triton-url", "http://127.0.0.1:8000",
"--free-bytes-floor", [string]([int64]$FreeGiBFloor * 1GB),
"--orchestrator-sha256", $orchestratorSha256,
"--container-image", $ContainerImage,
"--telemetry-interval-seconds", "1"
)
Write-Output ("PHASE=e8-source-paced-replay-start FRAMES={0}" -f $clipFrameCount)
& docker @runArgs
Assert-LastExitCode "LAB E8 source-paced detector/tracker"
$freeBytesPostReplay = Assert-FreeSpace "post-replay"
Write-Output "PHASE=e8-source-paced-replay-complete"
$result = Get-Content -LiteralPath (Join-Path $stagingRoot "result.json") -Raw | ConvertFrom-Json
if (
$result.schema_version -ne "missioncore.e8-realtime-tracking-result/v1" -or
$result.result_id -notmatch "^e8-realtime-tracking-[a-f0-9]{64}$" -or
$result.acceptance_state -ne "accepted" -or
$result.ground_truth -ne $false
) {
throw "Final LAB E8 result manifest is incompatible or rejected"
}
$finalRoot = Join-Path $derivedRoot ([string]$result.result_id)
if (Test-Path -LiteralPath $finalRoot) {
throw "An immutable result with the same LAB E8 identity already exists: $finalRoot"
}
Move-Item -LiteralPath $stagingRoot -Destination $finalRoot
Remove-Item -LiteralPath $publishRoot -Force
$totalWatch.Stop()
$completed = $true
$freeBytesFinal = Assert-FreeSpace "post-publication"
Write-Output ("RESULT_ROOT={0}" -f $finalRoot)
Write-Output ("RESULT_ID={0}" -f $result.result_id)
Write-Output ("PREPARATION_SECONDS={0}" -f $extractWatch.Elapsed.TotalSeconds)
Write-Output ("ORCHESTRATOR_WALL_SECONDS={0}" -f $totalWatch.Elapsed.TotalSeconds)
Write-Output ("DISK_FREE_BYTES_BEFORE={0}" -f $freeBytesBefore)
Write-Output ("DISK_FREE_BYTES_POST_EXTRACT={0}" -f $freeBytesPostExtract)
Write-Output ("DISK_FREE_BYTES_POST_REPLAY={0}" -f $freeBytesPostReplay)
Write-Output ("DISK_FREE_BYTES_FINAL={0}" -f $freeBytesFinal)
}
finally {
if (Test-Path -LiteralPath $workRoot) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}
if (-not $completed -and (Test-Path -LiteralPath $publishRoot)) {
Remove-Item -LiteralPath $publishRoot -Recurse -Force
}
if ($loadedByRun) {
try {
Invoke-WebRequest `
-Uri "http://127.0.0.1:8000/v2/repository/models/yolox_s/unload" `
-Method Post -ContentType "application/json" -Body "{}" `
-UseBasicParsing -TimeoutSec 60 *> $null
Write-Output "PHASE=e8-model-state-restored"
}
catch {
Write-Warning "LAB E8 could not restore the prior unloaded YOLOX-S state"
}
}
}