Skip to content

Commit df57ee9

Browse files
LoopedBard3Parker BibusCopilot
authored
Fix mlnet performance benchmark timeouts by pre-provisioning the SSWE model into the Helix payload (#5250)
* Pre-provision ML.NET SSWE model into Helix payload to fix mlnet timeouts The mlnet performance benchmarks (StochasticDualCoordinateAscentClassifierBench.TrainSentiment) apply a pretrained SSWE word embedding that ML.NET downloads (~70 MB) from aka.ms/mlnet-resources at benchmark runtime. That download stalls on the Helix machines, hanging the entire mlnet work item until it times out and is killed, discarding all mlnet results so every mlnet benchmark appears to fail. Download the model on the build agent (reliable connectivity) into the correlation payload and point MICROSOFTML_RESOURCE_PATH at it via the Helix pre-commands, removing the runtime network dependency. Best-effort and strictly gated on run_kind == mlnet, so non-mlnet runs are unaffected and a download failure falls back to prior behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tighten SSWE download validation and trim docstring Download to a temp file, validate the size against Content-Length (when present) and a minimum-size floor, then atomically replace the destination so a truncated or early-closed response can't leave a corrupt sentiment.emd in the payload. Also make the function docstring more concise. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Only enforce SSWE minimum-size floor when Content-Length is absent When the server sends a Content-Length, an exact match fully validates the download, so trust it regardless of size (the asset could legitimately shrink without becoming invalid). Only fall back to the minimum-size floor when no Content-Length is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reduce SSWE download retries to 3 and timeout to 60s Address review feedback: 5 retries and a 5-minute timeout are more than needed for a ~70 MB download, so cap retries at 3 and the per-operation socket timeout at 60s to avoid wasting build-agent time on a stuck download. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Validate SSWE model by SHA256 instead of size heuristics Per review feedback, replace the Content-Length/minimum-size checks with an exact SHA256 comparison against the known-good model hash. The SSWE model is a fixed pretrained asset that shouldn't change, so a hash check definitively rejects any truncated or corrupted download regardless of what headers the server returns. If the asset is ever intentionally updated, the constant must be recomputed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Quote MICROSOFTML_RESOURCE_PATH export and skip trailing retry sleep Address review feedback: quote the non-Windows export value so a payload path containing spaces or shell metacharacters isn't word-split (which would make ML.NET miss the pre-provisioned model), and only sleep between download attempts rather than after the final failed one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Parker Bibus <parker.bibus@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8b1d923 commit df57ee9

1 file changed

Lines changed: 92 additions & 0 deletions

File tree

scripts/run_performance_job.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
from dataclasses import dataclass, field
44
from datetime import timedelta
55
from glob import glob
6+
import hashlib
67
import json
78
import os
89
import shutil
910
from subprocess import CalledProcessError
1011
import sys
1112
import tempfile
13+
import time
1214
from traceback import format_exc
1315
import urllib.request
1416
import xml.etree.ElementTree as ET
@@ -120,6 +122,79 @@ class RunPerformanceJobArgs:
120122
live_libraries_build_config: Optional[str] = None
121123
cross_build: bool = False
122124

125+
# Subdirectory (inside the Helix correlation payload) that holds the pre-downloaded ML.NET resources.
126+
# On the Helix machine this is referenced as <HELIX_CORRELATION_PAYLOAD>/mlnet-resources.
127+
MLNET_RESOURCES_PAYLOAD_SUBDIR = "mlnet-resources"
128+
129+
# Known-good SHA256 of the SSWE word-embedding model (sentiment.emd, 73,674,434 bytes). The asset is
130+
# a fixed pretrained model and is not expected to change; validating the hash guarantees we shipped a
131+
# complete, uncorrupted file (a truncated or proxy-mangled response won't match). If the upstream
132+
# asset is ever intentionally updated, recompute and update this value.
133+
MLNET_SSWE_MODEL_SHA256 = "a8062ef5d3a1ffc079a2b3c439a5533279f4ac6d85882ca8488fb8ff239fefdd"
134+
135+
def try_provision_mlnet_resources(payload_dir: str) -> bool:
136+
"""
137+
Pre-download the ML.NET SSWE word-embedding model into the correlation payload.
138+
139+
StochasticDualCoordinateAscentClassifierBench.TrainSentiment applies a pretrained word embedding
140+
('sentiment.emd', ~70 MB) that ML.NET otherwise downloads from https://aka.ms/mlnet-resources at
141+
benchmark runtime. That download stalls on the Helix machines and hangs the whole mlnet work item
142+
until it times out. Downloading it here on the build agent (reliable connectivity) and pointing
143+
MICROSOFTML_RESOURCE_PATH at <payload>/mlnet-resources lets ML.NET load it from disk instead.
144+
ML.NET resolves the model at <MICROSOFTML_RESOURCE_PATH>/Text/Sswe/sentiment.emd.
145+
146+
Best-effort: returns False on failure so the caller skips the env var and the previous
147+
(runtime-download) behavior is left unchanged.
148+
"""
149+
resource_root = os.path.join(payload_dir, MLNET_RESOURCES_PAYLOAD_SUBDIR)
150+
dest = os.path.join(resource_root, "Text", "Sswe", "sentiment.emd")
151+
os.makedirs(os.path.dirname(dest), exist_ok=True)
152+
153+
# The direct blob URL is the redirect target of the aka.ms link; prefer it to avoid the redirect,
154+
# and fall back to the aka.ms link in case the blob path ever changes.
155+
urls = [
156+
"https://mlpublicassets.blob.core.windows.net/assets/Text/Sswe/sentiment.emd",
157+
"https://aka.ms/mlnet-resources/Text/Sswe/sentiment.emd",
158+
]
159+
160+
last_error: Optional[Exception] = None
161+
max_attempts = 3
162+
for attempt in range(1, max_attempts + 1):
163+
for url in urls:
164+
tmp_dest = dest + ".tmp"
165+
try:
166+
getLogger().info(f"Downloading ML.NET SSWE model from {url} (attempt {attempt})")
167+
with urllib.request.urlopen(url, timeout=60) as response:
168+
with open(tmp_dest, "wb") as f:
169+
shutil.copyfileobj(response, f)
170+
171+
# The model is a fixed asset, so verify the exact hash to reject any truncated or
172+
# corrupted download before it can be shipped in the payload.
173+
sha256 = hashlib.sha256()
174+
with open(tmp_dest, "rb") as f:
175+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
176+
sha256.update(chunk)
177+
actual_hash = sha256.hexdigest()
178+
if actual_hash != MLNET_SSWE_MODEL_SHA256:
179+
raise Exception(f"sha256 {actual_hash} does not match expected {MLNET_SSWE_MODEL_SHA256}")
180+
181+
os.replace(tmp_dest, dest)
182+
getLogger().info(f"Downloaded and verified ML.NET SSWE model to {dest}")
183+
return True
184+
except Exception as e:
185+
last_error = e
186+
getLogger().warning(f"Failed to download ML.NET SSWE model from {url}: {e}")
187+
if os.path.exists(tmp_dest):
188+
os.remove(tmp_dest)
189+
# Only wait between attempts, not after the final one.
190+
if attempt < max_attempts:
191+
time.sleep(10)
192+
193+
getLogger().warning(
194+
"Could not pre-provision the ML.NET SSWE model into the payload after retries "
195+
f"(last error: {last_error}); ML.NET will attempt to download it at benchmark runtime.")
196+
return False
197+
123198
def get_pre_commands(
124199
os_group: str,
125200
os_distro: Optional[str],
@@ -715,6 +790,14 @@ def run_performance_job(args: RunPerformanceJobArgs):
715790
getLogger().info("Copying performance repository to payload directory")
716791
shutil.copytree(args.performance_repo_dir, performance_payload_dir, ignore=shutil.ignore_patterns("CorrelationStaging", ".git", "artifacts", ".dotnet", ".venv", ".vs"))
717792

793+
# For ML.NET runs, pre-download the SSWE word-embedding model into the payload so the benchmarks
794+
# don't have to fetch it from the network on the (flaky) Helix machines. See
795+
# try_provision_mlnet_resources for details. The matching MICROSOFTML_RESOURCE_PATH env var is
796+
# set in the Helix pre-commands below when this succeeds.
797+
mlnet_resources_provisioned = False
798+
if args.run_kind == "mlnet":
799+
mlnet_resources_provisioned = try_provision_mlnet_resources(payload_dir)
800+
718801
if args.internal:
719802
creator = ""
720803
scenario_arguments = ["--upload-to-perflab-container"]
@@ -1037,6 +1120,15 @@ def run_performance_job(args: RunPerformanceJobArgs):
10371120
helix_pre_commands = get_pre_commands(args.os_group, args.os_distro, args.internal, args.runtime_type, args.codegen_type, args.build_config, v8_version)
10381121
helix_post_commands = get_post_commands(args.os_group, args.internal, args.runtime_type)
10391122

1123+
# Point ML.NET at the SSWE model that was pre-downloaded into the correlation payload above, so it
1124+
# loads the word embedding from disk instead of downloading it at benchmark runtime (which hangs
1125+
# the work item on Helix). %HELIX_CORRELATION_PAYLOAD% is expanded by the shell at run time.
1126+
if mlnet_resources_provisioned:
1127+
if args.os_group == "windows":
1128+
helix_pre_commands += [f"set \"MICROSOFTML_RESOURCE_PATH=%HELIX_CORRELATION_PAYLOAD%\\{MLNET_RESOURCES_PAYLOAD_SUBDIR}\""]
1129+
else:
1130+
helix_pre_commands += [f"export MICROSOFTML_RESOURCE_PATH=\"$HELIX_CORRELATION_PAYLOAD/{MLNET_RESOURCES_PAYLOAD_SUBDIR}\""]
1131+
10401132
ci_setup_arguments.local_build = args.local_build
10411133

10421134
if args.affinity != "0":

0 commit comments

Comments
 (0)