feat: prioritize proven GPU framework combinations

This commit is contained in:
CoolBoy
2026-08-05 18:21:59 +08:00
parent 882479e43e
commit 5e47d9e695
11 changed files with 431 additions and 86 deletions

View File

@@ -48,26 +48,26 @@ Optional tuning:
- `MODELHUB_MARKET_QUEUE_REFRESH_SECONDS` default `600` - `MODELHUB_MARKET_QUEUE_REFRESH_SECONDS` default `600`
- `MODELHUB_MARKET_FRAMEWORK_REFRESH_SECONDS` default `21600` - `MODELHUB_MARKET_FRAMEWORK_REFRESH_SECONDS` default `21600`
- `MODELHUB_MARKET_THROUGHPUT_WINDOW_HOURS` default `6` - `MODELHUB_MARKET_THROUGHPUT_WINDOW_HOURS` default `6`
- `MODELHUB_MARKET_FRAMEWORK_MIN_SAMPLES` default `100` - `MODELHUB_MARKET_FRAMEWORK_MIN_SAMPLES` default `300`
- `MODELSCOPE_PAGE_INTERVAL_SECONDS` default `0.25` - `MODELSCOPE_PAGE_INTERVAL_SECONDS` default `0.25`
- `MODELSCOPE_PAGE_CACHE_TTL_SECONDS` default `900` - `MODELSCOPE_PAGE_CACHE_TTL_SECONDS` default `900`
- `MODELHUB_AGENT_VERIFY_CACHE_TTL_SECONDS` default `900` - `MODELHUB_AGENT_VERIFY_CACHE_TTL_SECONDS` default `900`
## Adaptive GPU Strategy ## Adaptive GPU Strategy
When no explicit GPU override is supplied, the worker uses a queue-aware 50/30/20 When no explicit GPU override is supplied, the worker uses a success-first 70/30
strategy generation: strategy generation with no self-funded exploration:
- 50%: the three long-term GPUs with the best Wilson lower confidence score and at least 100 terminal samples - 70%: the three long-term GPUs with the best Wilson lower confidence score and at least 100 terminal samples
- 30%: exploration across every currently supported GPU - 30%: the top recent GPUs among the latest 1,000 terminal tasks
- 20%: the top recent GPUs among the latest 1,000 terminal tasks - 0%: unvetted/all-GPU exploration; community-wide results provide the exploration signal
The 50/30/20 category ratio remains exact across accepted tasks. Inside each The 70/30 category ratio remains exact across accepted tasks. Inside each
category, weighted fair scheduling combines the category's historical rank with category, weighted fair scheduling combines the category's historical rank with
live public market data: live public market data:
- estimated backlog hours (`waiting / recent completions per hour`) instead of raw queue length - recent public success quality, scored with a strongly weighted Wilson lower confidence bound
- recent public success quality, scored with a Wilson lower confidence bound - estimated backlog hours (`waiting / recent completions per hour`) as a bounded tie-breaker
- machine availability, running workers, and advertised concurrency - machine availability, running workers, and advertised concurrency
- a circuit breaker for unavailable or apparently stalled GPU pools - a circuit breaker for unavailable or apparently stalled GPU pools
@@ -79,11 +79,12 @@ snapshot, uses a retry backoff, and never blocks normal submissions.
For each compatible model/GPU pair, the worker also ranks the GPU's supported For each compatible model/GPU pair, the worker also ranks the GPU's supported
frameworks using ModelHub's public aggregate `modelCount` and `successCount` data, frameworks using ModelHub's public aggregate `modelCount` and `successCount` data,
then blends in the worker's own GPU+framework outcomes with a capped weight. then blends in the worker's own GPU+framework outcomes with a capped weight.
Only frameworks with at least 100 samples receive statistical priority; the Only frameworks with at least 300 public samples and a safe Wilson lower bound
legacy safe framework order remains the fallback. Framework statistics refresh are eligible. Framework statistics refresh
every 6 hours, so they do not add per-model API traffic. Newly published every 6 hours, so they do not add per-model API traffic. Newly published
frameworks are discovered automatically, but receive no novelty bonus: they can frameworks are discovered automatically, but receive no novelty bonus: they can
win only when their confidence-adjusted success score is better. A new framework win only when their confidence-adjusted success score beats the best incumbent by
at least 10%. A new framework
is eligible only after the authenticated official build-config endpoint returns is eligible only after the authenticated official build-config endpoint returns
a complete config that passes local structure, placeholder, framework-name, and a complete config that passes local structure, placeholder, framework-name, and
GPU-parallelism validation. Valid official configs are cached and refreshed with GPU-parallelism validation. Valid official configs are cached and refreshed with
@@ -91,9 +92,14 @@ the framework snapshot; local templates remain the fail-safe fallback.
Only platform-accepted tasks count. After exactly 200 accepted tasks, the next Only platform-accepted tasks count. After exactly 200 accepted tasks, the next
poll cycle reloads all account history, generates a new immutable strategy snapshot, poll cycle reloads all account history, generates a new immutable strategy snapshot,
and resets the generation counters to 100/60/40 targets. The active snapshot and and resets the generation counters to 140/60 targets. The active snapshot and
progress are stored in `.modelhub_state/gpu_strategy.json`. progress are stored in `.modelhub_state/gpu_strategy.json`.
Five consecutive local failures open a 12-hour GPU/framework circuit breaker.
A sub-20% success rate over the latest 20 terminal tasks opens a 6-hour breaker.
Candidate shortages expand the model search window; they never unlock an
unvetted GPU or framework.
ModelScope HTTP 429 responses use exponential backoff and `Retry-After`. Successful ModelScope HTTP 429 responses use exponential backoff and `Retry-After`. Successful
pages remain cached, so a later cycle retries the failed page instead of restarting pages remain cached, so a later cycle retries the failed page instead of restarting
the whole pagination scan. the whole pagination scan.
@@ -139,6 +145,10 @@ adds fail-closed model/GPU prechecks and persistent uniqueness exclusions.
Version `2026.08.04.1` adds queue/throughput intelligence, GPU health circuit Version `2026.08.04.1` adds queue/throughput intelligence, GPU health circuit
breaking, weighted-fair scheduling, live framework/config discovery, and breaking, weighted-fair scheduling, live framework/config discovery, and
confidence-ranked public-plus-local framework selection. confidence-ranked public-plus-local framework selection.
Version `2026.08.05.1` removes self-funded GPU exploration, switches accepted
traffic to 70/30 long-term/recent exploitation, raises the public framework gate
to 300 samples, makes success dominate queue pressure, and adds recent local
GPU/framework circuit breakers.
## Deploy ## Deploy

View File

@@ -8,7 +8,7 @@ It currently supports:
- continuous queue refill via `run_poll.sh` - continuous queue refill via `run_poll.sh`
- multiple ModelHub tokens read from `KEY.md` and `KEYS.md` - multiple ModelHub tokens read from `KEY.md` and `KEYS.md`
- automatic task/framework/template selection across the supported GPU catalog - automatic task/framework/template selection across the supported GPU catalog
- adaptive long-term/exploration/recent GPU scheduling with a persistent local snapshot - adaptive long-term/recent GPU exploitation with a persistent local snapshot
- live queue/throughput-aware GPU weighting and confidence-ranked framework selection - live queue/throughput-aware GPU weighting and confidence-ranked framework selection
## Layout ## Layout
@@ -74,19 +74,21 @@ bash run_poll.sh --dry-run
## Behavior ## Behavior
- The runner auto-discovers all safe GPU/template combinations from the public submit catalog. - The runner auto-discovers all safe GPU/template combinations from the public submit catalog.
- Automatic GPU selection uses exact 50/30/20 accepted-task scheduling: long-term - Automatic GPU selection uses exact 70/30 accepted-task scheduling: long-term
Wilson-ranked top 3 GPUs, all supported GPUs, and the top GPUs from the latest Wilson-ranked top 3 GPUs and the top GPUs from the latest 1,000 terminal tasks.
1,000 terminal tasks. There is no all-GPU exploration category.
- Within each category, weighted-fair scheduling uses estimated queue backlog hours, - Within each category, weighted-fair scheduling uses estimated queue backlog hours,
recent public throughput/success, machine availability, and worker concurrency. recent public throughput/success, machine availability, and worker concurrency.
Unavailable or stalled GPU pools are circuit-broken instead of continuing to absorb work. Unavailable or stalled GPU pools are circuit-broken instead of continuing to absorb work.
- Compatible frameworks are ranked by ModelHub public aggregate success statistics - Compatible frameworks are ranked by ModelHub public aggregate success statistics
plus capped local GPU+framework evidence, with a 100-sample public minimum and plus capped local GPU+framework evidence, with a 300-sample public minimum and
Wilson confidence bounds. The legacy safe order is retained whenever evidence Wilson confidence bounds. Missing or undersized public evidence receives zero
is missing or too small. traffic rather than falling back to exploration.
- New frameworks are discovered from the live catalog but get no novelty bonus. - New frameworks are discovered from the live catalog but get no novelty bonus.
They are eligible only with a complete official build config that passes local They are eligible only with a complete official build config that passes local
validation; cached local templates remain the fallback if live config sync fails. validation and a confidence score at least 10% above the best incumbent.
- Five consecutive local failures pause a GPU/framework pair for 12 hours; a
sub-20% rate over the latest 20 terminal tasks pauses it for 6 hours.
- A strategy generation lasts exactly 200 platform-accepted submissions. Rejected API calls and - A strategy generation lasts exactly 200 platform-accepted submissions. Rejected API calls and
duplicates do not advance it. The next cycle refreshes platform history before submitting again. duplicates do not advance it. The next cycle refreshes platform history before submitting again.
- Strategy state is stored in `.modelhub_state/gpu_strategy.json`; a generation never recalculates - Strategy state is stored in `.modelhub_state/gpu_strategy.json`; a generation never recalculates
@@ -163,7 +165,7 @@ Each run typically includes:
Persistent local scheduler state is written under `.modelhub_state/`: Persistent local scheduler state is written under `.modelhub_state/`:
- `gpu_strategy.json`: GPU ranks, generation progress, and 50/30/20 accepted counters - `gpu_strategy.json`: GPU ranks, generation progress, and 70/30 accepted counters
- `market_intelligence.json`: cached public queue, throughput, health, and framework statistics - `market_intelligence.json`: cached public queue, throughput, health, and framework statistics
- `account_capacity.json`: learned per-account active-task limits - `account_capacity.json`: learned per-account active-task limits
- `submission_exclusions.jsonl`: non-retryable model/GPU uniqueness rejections - `submission_exclusions.jsonl`: non-retryable model/GPU uniqueness rejections

View File

@@ -88,7 +88,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning") parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning")
parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run") parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run")
parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks") parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks")
parser.add_argument("--disable-gpu-strategy", action="store_true", help="Disable adaptive 50/30/20 GPU scheduling") parser.add_argument("--disable-gpu-strategy", action="store_true", help="Disable adaptive 70/30 proven-GPU scheduling")
parser.add_argument( parser.add_argument(
"--disable-market-intelligence", "--disable-market-intelligence",
action="store_true", action="store_true",

View File

@@ -17,13 +17,12 @@ DEFAULT_GPU_STRATEGY_PATH = Path(".modelhub_state/gpu_strategy.json")
DEFAULT_REFRESH_SUBMISSIONS = 200 DEFAULT_REFRESH_SUBMISSIONS = 200
DEFAULT_RECENT_TERMINAL_WINDOW = 1000 DEFAULT_RECENT_TERMINAL_WINDOW = 1000
DEFAULT_LONG_TERM_MIN_SAMPLES = 100 DEFAULT_LONG_TERM_MIN_SAMPLES = 100
STRATEGY_STATE_VERSION = 2 STRATEGY_STATE_VERSION = 3
LONG_TERM = "long_term" LONG_TERM = "long_term"
ALL_SUPPORTED = "all_supported"
RECENT = "recent" RECENT = "recent"
CATEGORIES = (LONG_TERM, ALL_SUPPORTED, RECENT) CATEGORIES = (LONG_TERM, RECENT)
CATEGORY_WEIGHTS = {LONG_TERM: 5, ALL_SUPPORTED: 3, RECENT: 2} CATEGORY_WEIGHTS = {LONG_TERM: 7, RECENT: 3}
def _empty_category_counts() -> dict[str, int]: def _empty_category_counts() -> dict[str, int]:
return {category: 0 for category in CATEGORIES} return {category: 0 for category in CATEGORIES}
@@ -354,7 +353,7 @@ class GPUStrategyManager:
self.log( self.log(
f"[strategy] {action} generation={self.state.get('generation', 0)} " f"[strategy] {action} generation={self.state.get('generation', 0)} "
f"accepted={self.state.get('acceptedSinceRefresh', 0)}/{self.state.get('refreshSubmissions', self.refresh_submissions)} " f"accepted={self.state.get('acceptedSinceRefresh', 0)}/{self.state.get('refreshSubmissions', self.refresh_submissions)} "
f"categories={counts.get(LONG_TERM, 0)},{counts.get(ALL_SUPPORTED, 0)},{counts.get(RECENT, 0)} " f"categories={counts.get(LONG_TERM, 0)},{counts.get(RECENT, 0)} "
f"long={','.join(self.state.get('longTermGpus') or [])} recent={self.state.get('recentGpu') or 'n/a'}" f"long={','.join(self.state.get('longTermGpus') or [])} recent={self.state.get('recentGpu') or 'n/a'}"
) )
@@ -377,20 +376,25 @@ class GPUStrategyManager:
recent = [recent_gpu] recent = [recent_gpu]
if category == LONG_TERM: if category == LONG_TERM:
eligible = long_term eligible = list(long_term)
base_pattern = (5.0, 3.0, 2.0) base_pattern = (5.0, 3.0, 2.0)
elif category == RECENT:
eligible = recent
base_pattern = (6.0, 3.0, 1.0)
else: else:
eligible = supported eligible = list(recent)
base_pattern = tuple(1.0 for _ in eligible) base_pattern = (6.0, 3.0, 1.0)
if self.market_intelligence is not None:
try:
vetted = self.market_intelligence.eligible_gpus(supported)
except Exception:
vetted = supported
eligible = [gpu for gpu in eligible if gpu in vetted]
eligible.extend(gpu for gpu in vetted if gpu not in eligible)
if not eligible: if not eligible:
return [] return []
weights: dict[str, float] = {} weights: dict[str, float] = {}
for index, gpu in enumerate(eligible): for index, gpu in enumerate(eligible):
base = base_pattern[min(index, len(base_pattern) - 1)] base = base_pattern[index] if index < len(base_pattern) else 0.5
market_weight = 1.0 market_weight = 1.0
if self.market_intelligence is not None: if self.market_intelligence is not None:
try: try:
@@ -460,9 +464,13 @@ class GPUStrategyManager:
break break
if selected is None: if selected is None:
# With market intelligence enabled, an empty vetted pool means
# stop instead of silently turning fallback into exploration.
if self.market_intelligence is not None:
break
# Unknown/custom GPUs can only appear when callers bypass the normal resolver. # Unknown/custom GPUs can only appear when callers bypass the normal resolver.
selected = next((candidate for candidate in candidates if candidate_key(candidate) not in used), None) selected = next((candidate for candidate in candidates if candidate_key(candidate) not in used), None)
actual_category = ALL_SUPPORTED actual_category = planned_category
if selected is None: if selected is None:
break break
@@ -508,9 +516,9 @@ class GPUStrategyManager:
for category in CATEGORIES for category in CATEGORIES
} }
for candidate in candidates: for candidate in candidates:
category = str(candidate.get("strategyCategory") or ALL_SUPPORTED) category = str(candidate.get("strategyCategory") or LONG_TERM)
if category not in category_counts: if category not in category_counts:
category = ALL_SUPPORTED category = LONG_TERM
category_counts[category] += 1 category_counts[category] += 1
gpu = str(candidate.get("targetGpu") or "") gpu = str(candidate.get("targetGpu") or "")
if gpu in gpu_category_counts[category]: if gpu in gpu_category_counts[category]:

View File

@@ -68,7 +68,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument( parser.add_argument(
"--disable-gpu-strategy", "--disable-gpu-strategy",
action="store_true", action="store_true",
help="Disable adaptive 50/30/20 GPU scheduling and keep the legacy candidate order", help="Disable adaptive 70/30 proven-GPU scheduling and keep the legacy candidate order",
) )
parser.add_argument( parser.add_argument(
"--disable-market-intelligence", "--disable-market-intelligence",
@@ -274,22 +274,14 @@ def choose_candidate_for_gpu(
except ValueError: except ValueError:
pass pass
if market_intelligence is not None: if market_intelligence is not None:
discovered = market_intelligence.compatible_discovered_frameworks( compatible_frameworks = market_intelligence.selectable_frameworks(
task_type=task_type, task_type=task_type,
target_gpu=target_gpu, target_gpu=target_gpu,
incumbent_frameworks=compatible_frameworks,
inspection=inspection, inspection=inspection,
) )
compatible_frameworks.extend(
framework for framework in discovered if framework not in compatible_frameworks
)
if not compatible_frameworks: if not compatible_frameworks:
continue continue
if market_intelligence is not None:
compatible_frameworks = market_intelligence.rank_frameworks(
task_type=task_type,
target_gpu=target_gpu,
compatible_frameworks=compatible_frameworks,
)
for framework in compatible_frameworks: for framework in compatible_frameworks:
config_params = ( config_params = (
@@ -451,7 +443,12 @@ def process_model_for_candidates(
market_intelligence=market_intelligence, market_intelligence=market_intelligence,
) )
if best is None: if best is None:
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "no_compatible_auto_template_or_framework"}) reason = (
"no_publicly_vetted_compatible_framework"
if market_intelligence is not None
else "no_compatible_auto_template_or_framework"
)
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": reason})
continue continue
record = candidate_to_record(best) record = candidate_to_record(best)
if market_intelligence is not None: if market_intelligence is not None:
@@ -706,6 +703,7 @@ def run_submission(
target_gpus = resolve_target_gpus(args, template_selector, selected_task_types) target_gpus = resolve_target_gpus(args, template_selector, selected_task_types)
if not target_gpus: if not target_gpus:
raise RuntimeError("No auto-submittable GPUs are available for the selected task types") raise RuntimeError("No auto-submittable GPUs are available for the selected task types")
submission_target_gpus = list(target_gpus)
hf_discovery = hf_discovery or HuggingFaceDiscovery(base_url=args.hf_base_url) hf_discovery = hf_discovery or HuggingFaceDiscovery(base_url=args.hf_base_url)
if modelhub_client is None: if modelhub_client is None:
@@ -809,6 +807,7 @@ def run_submission(
"dryRun": bool(args.dry_run), "dryRun": bool(args.dry_run),
"selectedTaskTypes": selected_task_types, "selectedTaskTypes": selected_task_types,
"targetGpus": target_gpus, "targetGpus": target_gpus,
"submissionEligibleGpus": submission_target_gpus,
"dailyTarget": args.daily_target, "dailyTarget": args.daily_target,
"unlimitedDailyTarget": unlimited_daily_target, "unlimitedDailyTarget": unlimited_daily_target,
"submittedTodayBeforeRun": daily_snapshot["totalCount"], "submittedTodayBeforeRun": daily_snapshot["totalCount"],
@@ -893,6 +892,14 @@ def run_submission(
except Exception: except Exception:
market_intelligence.set_local_outcome_stats(None) market_intelligence.set_local_outcome_stats(None)
market_summary = market_intelligence.summary() market_summary = market_intelligence.summary()
submission_target_gpus = market_intelligence.eligible_gpus(target_gpus)
market_summary["eligibleGpus"] = submission_target_gpus
market_summary["shadowOnlyGpus"] = [gpu for gpu in target_gpus if gpu not in submission_target_gpus]
print(
f"[market] eligible_gpus={','.join(submission_target_gpus) or 'none'} "
f"shadow_only={','.join(market_summary['shadowOnlyGpus']) or 'none'}",
flush=True,
)
if strategy_enabled: if strategy_enabled:
strategy_manager = GPUStrategyManager( strategy_manager = GPUStrategyManager(
@@ -930,7 +937,7 @@ def run_submission(
scan_limit = resolve_scan_limit( scan_limit = resolve_scan_limit(
args, args,
target_gpu_count=len(target_gpus), target_gpu_count=len(submission_target_gpus),
remaining_daily_quota=remaining_daily_quota, remaining_daily_quota=remaining_daily_quota,
platform_available_slots=platform_available_slots, platform_available_slots=platform_available_slots,
) )
@@ -986,7 +993,7 @@ def run_submission(
hf_discovery=hf_discovery, hf_discovery=hf_discovery,
modelhub_client=modelhub_client, modelhub_client=modelhub_client,
template_selector=template_selector, template_selector=template_selector,
target_gpus=target_gpus, target_gpus=submission_target_gpus,
selected_task_types=selected_task_types, selected_task_types=selected_task_types,
outcome_tracker=outcome_tracker, outcome_tracker=outcome_tracker,
submission_exclusion_store=submission_exclusion_store, submission_exclusion_store=submission_exclusion_store,
@@ -1204,6 +1211,7 @@ def run_submission(
"dryRun": bool(args.dry_run), "dryRun": bool(args.dry_run),
"selectedTaskTypes": selected_task_types, "selectedTaskTypes": selected_task_types,
"targetGpus": target_gpus, "targetGpus": target_gpus,
"submissionEligibleGpus": submission_target_gpus,
"maxSubmitsPerRun": int(getattr(args, "max_submits_per_run", 0) or 0), "maxSubmitsPerRun": int(getattr(args, "max_submits_per_run", 0) or 0),
"dailyTarget": args.daily_target, "dailyTarget": args.daily_target,
"unlimitedDailyTarget": unlimited_daily_target, "unlimitedDailyTarget": unlimited_daily_target,

View File

@@ -12,12 +12,15 @@ from common import parse_datetime, read_json, utc_now, write_json
DEFAULT_MARKET_INTELLIGENCE_PATH = Path(".modelhub_state/market_intelligence.json") DEFAULT_MARKET_INTELLIGENCE_PATH = Path(".modelhub_state/market_intelligence.json")
MARKET_STATE_VERSION = 2 MARKET_STATE_VERSION = 3
DEFAULT_QUEUE_REFRESH_SECONDS = 600 DEFAULT_QUEUE_REFRESH_SECONDS = 600
DEFAULT_FRAMEWORK_REFRESH_SECONDS = 21_600 DEFAULT_FRAMEWORK_REFRESH_SECONDS = 21_600
DEFAULT_THROUGHPUT_WINDOW_HOURS = 6 DEFAULT_THROUGHPUT_WINDOW_HOURS = 6
DEFAULT_FETCH_WORKERS = 4 DEFAULT_FETCH_WORKERS = 4
DEFAULT_FRAMEWORK_MIN_SAMPLES = 100 DEFAULT_FRAMEWORK_MIN_SAMPLES = 300
DEFAULT_GPU_MIN_RECENT_TERMINALS = 20
DEFAULT_FRAMEWORK_MIN_WILSON = 0.05
NEW_FRAMEWORK_PROMOTION_MARGIN = 1.10
ERROR_RETRY_SECONDS = 300 ERROR_RETRY_SECONDS = 300
@@ -73,6 +76,7 @@ def _neutral_gpu_stats(gpus: list[str]) -> dict[str, dict[str, Any]]:
"healthFactor": 1.0, "healthFactor": 1.0,
"queueWeight": 1.0, "queueWeight": 1.0,
"selectionWeight": 1.0, "selectionWeight": 1.0,
"submissionEligible": None,
"error": "market_data_unavailable", "error": "market_data_unavailable",
} }
for gpu in gpus for gpu in gpus
@@ -156,6 +160,45 @@ class MarketIntelligenceManager:
return False return False
return list(state.get("supportedGpus") or []) == gpus and list(state.get("taskTypes") or []) == task_types return list(state.get("supportedGpus") or []) == gpus and list(state.get("taskTypes") or []) == task_types
@staticmethod
def _catalog_compatible(state: dict[str, Any] | None, gpus: list[str], task_types: list[str]) -> bool:
if not state:
return False
return list(state.get("supportedGpus") or []) == gpus and list(state.get("taskTypes") or []) == task_types
@staticmethod
def _rescore_cached_gpu_stats(gpu_stats: dict[str, dict[str, Any]]) -> None:
usable = [
item
for item in gpu_stats.values()
if item.get("backlogHours") is not None and item.get("recentWilsonLowerBound") is not None
]
finite_backlogs = [
float(item["backlogHours"])
for item in usable
if float(item["backlogHours"]) < 9_999.0
]
median_backlog = statistics.median(finite_backlogs) if finite_backlogs else 24.0
max_wilson = max((float(item.get("recentWilsonLowerBound") or 0.0) for item in usable), default=0.0)
for item in usable:
backlog = max(0.25, float(item.get("backlogHours") or 9_999.0))
wilson = float(item.get("recentWilsonLowerBound") or 0.0)
health_factor = float(item.get("healthFactor") or 1.0)
queue_factor = _clamp((max(0.25, median_backlog) / backlog) ** 0.15, 0.70, 1.30)
quality_ratio = wilson / max_wilson if max_wilson > 0 else 0.0
quality_factor = _clamp(2.5 * (quality_ratio**2.2), 0.05, 2.5)
item["queueFactor"] = queue_factor
item["qualityFactor"] = quality_factor
item["queueWeight"] = _clamp(queue_factor * health_factor, 0.05, 2.0)
item["selectionWeight"] = _clamp(queue_factor * quality_factor * health_factor, 0.02, 3.0)
item["submissionEligible"] = bool(
item.get("canVerify") is not False
and health_factor >= 0.5
and int(item.get("recentTerminal") or 0) >= DEFAULT_GPU_MIN_RECENT_TERMINALS
and int(item.get("recentSuccess") or 0) > 0
and wilson >= DEFAULT_FRAMEWORK_MIN_WILSON
)
def _base_state(self, gpus: list[str], task_types: list[str], now: datetime) -> dict[str, Any]: def _base_state(self, gpus: list[str], task_types: list[str], now: datetime) -> dict[str, Any]:
return { return {
"version": MARKET_STATE_VERSION, "version": MARKET_STATE_VERSION,
@@ -185,7 +228,14 @@ class MarketIntelligenceManager:
gpus = list(dict.fromkeys(gpu for gpu in supported_gpus if gpu)) gpus = list(dict.fromkeys(gpu for gpu in supported_gpus if gpu))
tasks = list(dict.fromkeys(task for task in task_types if task)) tasks = list(dict.fromkeys(task for task in task_types if task))
loaded = self._load() loaded = self._load()
state = loaded if self._compatible(loaded, gpus, tasks) else self._base_state(gpus, tasks, now) if self._compatible(loaded, gpus, tasks):
state = loaded
elif int((loaded or {}).get("version") or 0) == 2 and self._catalog_compatible(loaded, gpus, tasks):
state = dict(loaded or {})
state["version"] = MARKET_STATE_VERSION
self._rescore_cached_gpu_stats(state.get("gpuStats") or {})
else:
state = self._base_state(gpus, tasks, now)
queue_due = not _fresh( queue_due = not _fresh(
state.get("queueUpdatedAt"), state.get("queueUpdatedAt"),
@@ -355,9 +405,16 @@ class MarketIntelligenceManager:
result = _neutral_gpu_stats(gpus) result = _neutral_gpu_stats(gpus)
for gpu, stats in fetched.items(): for gpu, stats in fetched.items():
backlog = max(0.25, float(stats["backlogHours"])) backlog = max(0.25, float(stats["backlogHours"]))
queue_factor = _clamp(math.sqrt(max(0.25, median_backlog) / backlog), 0.35, 1.75) # Queue pressure is now a tie-breaker between proven GPUs, not a way
# for a fast but unreliable pool to outrank a successful one.
queue_factor = _clamp(
(max(0.25, median_backlog) / backlog) ** 0.15,
0.70,
1.30,
)
wilson = float(stats["recentWilsonLowerBound"]) wilson = float(stats["recentWilsonLowerBound"])
quality_factor = 1.0 if max_wilson <= 0 else 0.6 + 1.4 * (wilson / max_wilson) quality_ratio = wilson / max_wilson if max_wilson > 0 else 0.0
quality_factor = _clamp(2.5 * (quality_ratio**2.2), 0.05, 2.5)
health_factor = 1.0 health_factor = 1.0
if stats.get("canVerify") is False: if stats.get("canVerify") is False:
health_factor = 0.05 health_factor = 0.05
@@ -379,7 +436,14 @@ class MarketIntelligenceManager:
stats["qualityFactor"] = quality_factor stats["qualityFactor"] = quality_factor
stats["healthFactor"] = health_factor stats["healthFactor"] = health_factor
stats["queueWeight"] = _clamp(queue_factor * health_factor, 0.05, 2.0) stats["queueWeight"] = _clamp(queue_factor * health_factor, 0.05, 2.0)
stats["selectionWeight"] = _clamp(queue_factor * quality_factor * health_factor, 0.05, 3.0) stats["selectionWeight"] = _clamp(queue_factor * quality_factor * health_factor, 0.02, 3.0)
stats["submissionEligible"] = bool(
stats.get("canVerify") is not False
and health_factor >= 0.5
and int(stats.get("recentTerminal") or 0) >= DEFAULT_GPU_MIN_RECENT_TERMINALS
and int(stats.get("recentSuccess") or 0) > 0
and wilson >= DEFAULT_FRAMEWORK_MIN_WILSON
)
result[gpu] = stats result[gpu] = stats
for gpu, error in errors.items(): for gpu, error in errors.items():
@@ -472,10 +536,22 @@ class MarketIntelligenceManager:
return result return result
def gpu_weight(self, gpu: str, *, category: str) -> float: def gpu_weight(self, gpu: str, *, category: str) -> float:
del category
stats = ((self.state or {}).get("gpuStats") or {}).get(gpu) or {} stats = ((self.state or {}).get("gpuStats") or {}).get(gpu) or {}
if category == "all_supported": return max(0.02, float(stats.get("selectionWeight") or 1.0))
return max(0.05, float(stats.get("queueWeight") or 1.0))
return max(0.05, float(stats.get("selectionWeight") or 1.0)) def eligible_gpus(self, supported_gpus: list[str]) -> list[str]:
stats = (self.state or {}).get("gpuStats") or {}
ranked: list[tuple[float, int, str]] = []
for index, gpu in enumerate(supported_gpus):
item = stats.get(gpu) or {}
eligibility = item.get("submissionEligible")
# Missing live data falls back to the scheduler's already-proven
# long/recent pools. Explicitly failed public gates never do.
if eligibility is False:
continue
ranked.append((-float(item.get("selectionWeight") or 1.0), index, gpu))
return [gpu for _weight, _index, gpu in sorted(ranked)]
def gpu_metadata(self, gpu: str) -> dict[str, Any]: def gpu_metadata(self, gpu: str) -> dict[str, Any]:
stats = ((self.state or {}).get("gpuStats") or {}).get(gpu) or {} stats = ((self.state or {}).get("gpuStats") or {}).get(gpu) or {}
@@ -487,6 +563,7 @@ class MarketIntelligenceManager:
"queueBacklogHours": stats.get("backlogHours"), "queueBacklogHours": stats.get("backlogHours"),
"publicRecentSuccessRate": stats.get("recentSuccessRate"), "publicRecentSuccessRate": stats.get("recentSuccessRate"),
"publicThroughputPerHour": stats.get("throughputPerHour"), "publicThroughputPerHour": stats.get("throughputPerHour"),
"marketSubmissionEligible": stats.get("submissionEligible"),
"marketDataStale": bool(stats.get("stale", False)), "marketDataStale": bool(stats.get("stale", False)),
} }
@@ -529,17 +606,41 @@ class MarketIntelligenceManager:
local_samples = local_success + local_failure local_samples = local_success + local_failure
local_score = _wilson_lower_bound(local_success, local_samples) local_score = _wilson_lower_bound(local_success, local_samples)
public_qualified = public_samples >= self.framework_min_samples recent_item = (self.local_outcome_stats.get("recentCombinationStats") or {}).get(local_key) or {}
recent_success = max(0, int(recent_item.get("successCount") or 0))
recent_failure = max(0, int(recent_item.get("failureCount") or 0))
recent_samples = recent_success + recent_failure
recent_rate = recent_success / recent_samples if recent_samples else None
consecutive_failures = max(0, int(recent_item.get("consecutiveFailures") or 0))
last_terminal_at = parse_datetime(recent_item.get("lastTerminalAt"))
circuit_reason = None
circuit_until = None
if last_terminal_at is not None and consecutive_failures >= 5:
circuit_reason = "five_consecutive_local_failures"
circuit_until = last_terminal_at + timedelta(hours=12)
elif last_terminal_at is not None and recent_samples >= 20 and recent_rate is not None and recent_rate < 0.20:
circuit_reason = "recent_local_success_below_20_percent"
circuit_until = last_terminal_at + timedelta(hours=6)
circuit_open = bool(circuit_until is not None and circuit_until > utc_now())
if not circuit_open:
circuit_reason = None
circuit_until = None
public_qualified = (
public_samples >= self.framework_min_samples
and public_score >= DEFAULT_FRAMEWORK_MIN_WILSON
)
local_qualified = local_samples >= 20 local_qualified = local_samples >= 20
if public_qualified: if public_qualified:
local_weight = min(0.35, local_samples / (local_samples + 50.0)) if local_samples >= 5 else 0.0 evidence_success = recent_success if recent_samples >= 5 else local_success
combined = public_score * (1.0 - local_weight) + local_score * local_weight evidence_samples = recent_samples if recent_samples >= 5 else local_samples
elif local_qualified: evidence_score = _wilson_lower_bound(evidence_success, evidence_samples)
combined = local_score local_weight = min(0.60, evidence_samples / (evidence_samples + 100.0)) if evidence_samples >= 5 else 0.0
combined = public_score * (1.0 - local_weight) + evidence_score * local_weight
else: else:
combined = 0.0 combined = 0.0
return { return {
"qualified": public_qualified or local_qualified, "qualified": public_qualified and not circuit_open,
"publicQualified": public_qualified, "publicQualified": public_qualified,
"localQualified": local_qualified, "localQualified": local_qualified,
"combinedScore": combined, "combinedScore": combined,
@@ -548,8 +649,55 @@ class MarketIntelligenceManager:
"localSamples": local_samples, "localSamples": local_samples,
"localSuccessRate": local_success / local_samples if local_samples else None, "localSuccessRate": local_success / local_samples if local_samples else None,
"localScore": local_score if local_samples else None, "localScore": local_score if local_samples else None,
"recentLocalSamples": recent_samples,
"recentLocalSuccessRate": recent_rate,
"consecutiveLocalFailures": consecutive_failures,
"circuitOpen": circuit_open,
"circuitReason": circuit_reason,
"circuitUntil": circuit_until.isoformat() if circuit_until else None,
} }
def selectable_frameworks(
self,
*,
task_type: str,
target_gpu: str,
incumbent_frameworks: list[str],
inspection: Any,
) -> list[str]:
discovered = self.compatible_discovered_frameworks(
task_type=task_type,
target_gpu=target_gpu,
inspection=inspection,
)
candidates = list(dict.fromkeys([*incumbent_frameworks, *discovered]))
vetted = [
framework
for framework in candidates
if self._framework_evidence(task_type, target_gpu, framework)["qualified"]
]
incumbent_scores = [
float(self._framework_evidence(task_type, target_gpu, framework)["combinedScore"])
for framework in vetted
if framework in incumbent_frameworks
]
incumbent_best = max(incumbent_scores, default=0.0)
promoted: list[str] = []
for framework in vetted:
if framework in incumbent_frameworks:
promoted.append(framework)
continue
evidence = self._framework_evidence(task_type, target_gpu, framework)
if incumbent_best > 0 and float(evidence["combinedScore"]) < incumbent_best * NEW_FRAMEWORK_PROMOTION_MARGIN:
continue
promoted.append(framework)
return self.rank_frameworks(
task_type=task_type,
target_gpu=target_gpu,
compatible_frameworks=promoted,
)
def compatible_discovered_frameworks( def compatible_discovered_frameworks(
self, self,
*, *,
@@ -609,10 +757,16 @@ class MarketIntelligenceManager:
"frameworkMarketWilsonLowerBound": item.get("wilsonLowerBound"), "frameworkMarketWilsonLowerBound": item.get("wilsonLowerBound"),
"frameworkLocalSamples": evidence["localSamples"], "frameworkLocalSamples": evidence["localSamples"],
"frameworkLocalSuccessRate": evidence["localSuccessRate"], "frameworkLocalSuccessRate": evidence["localSuccessRate"],
"frameworkRecentLocalSamples": evidence["recentLocalSamples"],
"frameworkRecentLocalSuccessRate": evidence["recentLocalSuccessRate"],
"frameworkConsecutiveLocalFailures": evidence["consecutiveLocalFailures"],
"frameworkCombinedScore": evidence["combinedScore"], "frameworkCombinedScore": evidence["combinedScore"],
"frameworkMarketQualified": evidence["publicQualified"], "frameworkMarketQualified": evidence["publicQualified"],
"frameworkLocalQualified": evidence["localQualified"], "frameworkLocalQualified": evidence["localQualified"],
"frameworkEvidenceQualified": evidence["qualified"], "frameworkEvidenceQualified": evidence["qualified"],
"frameworkCircuitOpen": evidence["circuitOpen"],
"frameworkCircuitReason": evidence["circuitReason"],
"frameworkCircuitUntil": evidence["circuitUntil"],
"frameworkOfficialConfigValid": bool(item.get("officialConfigValid", False)), "frameworkOfficialConfigValid": bool(item.get("officialConfigValid", False)),
"frameworkOfficialConfigStale": bool(item.get("officialConfigStale", False)), "frameworkOfficialConfigStale": bool(item.get("officialConfigStale", False)),
"frameworkConfigSource": "modelhub_live" if item.get("officialConfigValid") else "local_template", "frameworkConfigSource": "modelhub_live" if item.get("officialConfigValid") else "local_template",

View File

@@ -153,6 +153,28 @@ class OutcomeTracker:
f"{gpu}|{fw}|{tt}": {"targetGpu": gpu, "framework": fw, "taskType": tt, **_summarize(records)} f"{gpu}|{fw}|{tt}": {"targetGpu": gpu, "framework": fw, "taskType": tt, **_summarize(records)}
for (gpu, fw, tt), records in combo_groups.items() for (gpu, fw, tt), records in combo_groups.items()
} }
recent_combination_stats: dict[str, dict[str, Any]] = {}
for (gpu, fw, tt), records in combo_groups.items():
recent = sorted(records, key=_outcome_record_timestamp, reverse=True)[:20]
consecutive_failures = 0
for record in recent:
if record.get("outcome") != "failed":
break
consecutive_failures += 1
last_terminal_at = None
if recent:
last_terminal_at = (
parse_datetime(recent[0].get("lastSyncTime"))
or parse_datetime(recent[0].get("submitTime"))
)
recent_combination_stats[f"{gpu}|{fw}|{tt}"] = {
"targetGpu": gpu,
"framework": fw,
"taskType": tt,
**_summarize(recent),
"consecutiveFailures": consecutive_failures,
"lastTerminalAt": last_terminal_at.isoformat() if last_terminal_at else None,
}
warnings: list[str] = [] warnings: list[str] = []
for gpu, summary in gpu_summaries.items(): for gpu, summary in gpu_summaries.items():
@@ -172,6 +194,7 @@ class OutcomeTracker:
"gpuSummaries": gpu_summaries, "gpuSummaries": gpu_summaries,
"frameworkSummaries": framework_summaries, "frameworkSummaries": framework_summaries,
"combinationStats": combination_stats, "combinationStats": combination_stats,
"recentCombinationStats": recent_combination_stats,
"totals": _summarize(terminal), "totals": _summarize(terminal),
"warnings": warnings, "warnings": warnings,
} }
@@ -275,6 +298,11 @@ def _summarize(records: list[dict[str, Any]]) -> dict[str, Any]:
} }
def _outcome_record_timestamp(record: dict[str, Any]) -> float:
timestamp = parse_datetime(record.get("lastSyncTime")) or parse_datetime(record.get("submitTime"))
return timestamp.timestamp() if timestamp else 0.0
def _outcome_record_key(record: dict[str, Any]) -> str: def _outcome_record_key(record: dict[str, Any]) -> str:
task_id = record.get("taskId") task_id = record.get("taskId")
if task_id is not None: if task_id is not None:

View File

@@ -67,7 +67,7 @@ def build_parser() -> argparse.ArgumentParser:
parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning") parser.add_argument("--skip-outcome-sync", action="store_true", help="Skip outcome sync from ModelHub before scanning")
parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run") parser.add_argument("--skip-history-archive", action="store_true", help="Skip historical task archive download for this run")
parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks") parser.add_argument("--dry-run", action="store_true", help="Plan the day without creating tasks")
parser.add_argument("--disable-gpu-strategy", action="store_true", help="Disable adaptive 50/30/20 GPU scheduling") parser.add_argument("--disable-gpu-strategy", action="store_true", help="Disable adaptive 70/30 proven-GPU scheduling")
parser.add_argument( parser.add_argument(
"--disable-market-intelligence", "--disable-market-intelligence",
action="store_true", action="store_true",

View File

@@ -1 +1 @@
AGENT_VERSION = "2026.08.04.1" AGENT_VERSION = "2026.08.05.1"

View File

@@ -14,7 +14,6 @@ if str(PACKAGE_DIR) in sys.path:
sys.path.insert(0, str(PACKAGE_DIR)) sys.path.insert(0, str(PACKAGE_DIR))
from gpu_strategy import ( # noqa: E402 from gpu_strategy import ( # noqa: E402
ALL_SUPPORTED,
LONG_TERM, LONG_TERM,
RECENT, RECENT,
GPUStrategyManager, GPUStrategyManager,
@@ -55,7 +54,15 @@ class WeightedMarket:
return {"gpu-a": 3.0, "gpu-b": 0.1, "gpu-c": 0.1, "gpu-d": 0.1}[gpu] return {"gpu-a": 3.0, "gpu-b": 0.1, "gpu-c": 0.1, "gpu-d": 0.1}[gpu]
def gpu_metadata(self, gpu: str) -> dict: def gpu_metadata(self, gpu: str) -> dict:
return {"marketWeight": self.gpu_weight(gpu, category=ALL_SUPPORTED)} return {"marketWeight": self.gpu_weight(gpu, category=LONG_TERM)}
def eligible_gpus(self, supported_gpus: list[str]) -> list[str]:
return list(supported_gpus)
class VettedMarket(WeightedMarket):
def eligible_gpus(self, supported_gpus: list[str]) -> list[str]:
return [gpu for gpu in supported_gpus if gpu in {"gpu-b", "gpu-c"}]
class GPUStrategyTests(unittest.TestCase): class GPUStrategyTests(unittest.TestCase):
@@ -80,14 +87,14 @@ class GPUStrategyTests(unittest.TestCase):
self.assertNotIn("tiny", snapshot["longTermGpus"]) self.assertNotIn("tiny", snapshot["longTermGpus"])
def test_weighted_category_planner_is_exact_over_200_accepts(self) -> None: def test_weighted_category_planner_is_exact_over_200_accepts(self) -> None:
counts = {LONG_TERM: 0, ALL_SUPPORTED: 0, RECENT: 0} counts = {LONG_TERM: 0, RECENT: 0}
for _ in range(200): for _ in range(200):
category = choose_next_category(counts) category = choose_next_category(counts)
counts[category] += 1 counts[category] += 1
self.assertEqual({LONG_TERM: 100, ALL_SUPPORTED: 60, RECENT: 40}, counts) self.assertEqual({LONG_TERM: 140, RECENT: 60}, counts)
def test_candidate_order_uses_50_30_20_for_first_strategy_generation(self) -> None: def test_candidate_order_uses_70_30_without_exploration(self) -> None:
supported = ["gpu-a", "gpu-b", "gpu-c", "gpu-d"] supported = ["gpu-a", "gpu-b", "gpu-c", "gpu-d"]
snapshot = build_strategy_snapshot([], supported_gpus=supported) snapshot = build_strategy_snapshot([], supported_gpus=supported)
manager = GPUStrategyManager("unused.json") manager = GPUStrategyManager("unused.json")
@@ -101,9 +108,9 @@ class GPUStrategyTests(unittest.TestCase):
ordered = manager.order_candidates(candidates) ordered = manager.order_candidates(candidates)
categories = Counter(candidate["strategyCategory"] for candidate in ordered[:200]) categories = Counter(candidate["strategyCategory"] for candidate in ordered[:200])
self.assertEqual(100, categories[LONG_TERM]) self.assertEqual(140, categories[LONG_TERM])
self.assertEqual(60, categories[ALL_SUPPORTED]) self.assertEqual(60, categories[RECENT])
self.assertEqual(40, categories[RECENT]) self.assertNotIn("all_supported", categories)
def test_refresh_happens_only_after_200_accepted_submissions(self) -> None: def test_refresh_happens_only_after_200_accepted_submissions(self) -> None:
with tempfile.TemporaryDirectory() as temporary_dir: with tempfile.TemporaryDirectory() as temporary_dir:
@@ -126,7 +133,7 @@ class GPUStrategyTests(unittest.TestCase):
self.assertEqual(200, manager.state["acceptedTotal"]) self.assertEqual(200, manager.state["acceptedTotal"])
self.assertEqual(0, manager.state["acceptedSinceRefresh"]) self.assertEqual(0, manager.state["acceptedSinceRefresh"])
def test_market_weights_change_gpu_mix_without_changing_50_30_20_split(self) -> None: def test_market_weights_change_gpu_mix_without_changing_70_30_split(self) -> None:
supported = ["gpu-a", "gpu-b", "gpu-c", "gpu-d"] supported = ["gpu-a", "gpu-b", "gpu-c", "gpu-d"]
manager = GPUStrategyManager("unused.json", market_intelligence=WeightedMarket()) manager = GPUStrategyManager("unused.json", market_intelligence=WeightedMarket())
manager.state = build_strategy_snapshot([], supported_gpus=supported) manager.state = build_strategy_snapshot([], supported_gpus=supported)
@@ -138,15 +145,26 @@ class GPUStrategyTests(unittest.TestCase):
ordered = manager.order_candidates(candidates)[:200] ordered = manager.order_candidates(candidates)[:200]
category_counts = Counter(candidate["strategyCategory"] for candidate in ordered) category_counts = Counter(candidate["strategyCategory"] for candidate in ordered)
exploration_counts = Counter( gpu_counts = Counter(candidate["targetGpu"] for candidate in ordered)
candidate["targetGpu"]
for candidate in ordered
if candidate["strategyCategory"] == ALL_SUPPORTED
)
self.assertEqual({LONG_TERM: 100, ALL_SUPPORTED: 60, RECENT: 40}, category_counts) self.assertEqual({LONG_TERM: 140, RECENT: 60}, category_counts)
self.assertGreater(exploration_counts["gpu-a"], exploration_counts["gpu-b"]) self.assertGreater(gpu_counts["gpu-a"], gpu_counts["gpu-b"])
self.assertTrue(all(exploration_counts[gpu] > 0 for gpu in supported)) self.assertNotIn("all_supported", category_counts)
def test_unvetted_gpus_receive_zero_submissions(self) -> None:
supported = ["gpu-a", "gpu-b", "gpu-c", "gpu-d"]
manager = GPUStrategyManager("unused.json", market_intelligence=VettedMarket())
manager.state = build_strategy_snapshot([], supported_gpus=supported)
candidates = [
{"repoId": f"owner/model-{model}", "targetGpu": gpu}
for model in range(50)
for gpu in supported
]
ordered = manager.order_candidates(candidates)
self.assertTrue(ordered)
self.assertEqual({"gpu-b", "gpu-c"}, {candidate["targetGpu"] for candidate in ordered})
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -13,6 +13,7 @@ if str(PACKAGE_DIR) in sys.path:
sys.path.insert(0, str(PACKAGE_DIR)) sys.path.insert(0, str(PACKAGE_DIR))
from main import choose_candidate_for_gpu # noqa: E402 from main import choose_candidate_for_gpu # noqa: E402
from common import write_json # noqa: E402
from market_intelligence import MarketIntelligenceManager # noqa: E402 from market_intelligence import MarketIntelligenceManager # noqa: E402
from models import HFModelSummary, ModelInspection # noqa: E402 from models import HFModelSummary, ModelInspection # noqa: E402
from template_selector import TemplateSelector # noqa: E402 from template_selector import TemplateSelector # noqa: E402
@@ -158,6 +159,52 @@ class MarketIntelligenceTests(unittest.TestCase):
) )
self.assertEqual(first_call_count, client.calls) self.assertEqual(first_call_count, client.calls)
def test_version_two_snapshot_is_migrated_without_losing_public_evidence(self) -> None:
with tempfile.TemporaryDirectory() as temporary_dir:
path = Path(temporary_dir) / "market.json"
now = datetime.now(timezone.utc)
write_json(
path,
{
"version": 2,
"supportedGpus": ["fast"],
"taskTypes": ["text-generation"],
"queueUpdatedAt": now.isoformat(),
"frameworkUpdatedAt": now.isoformat(),
"queueError": None,
"frameworkError": None,
"gpuStats": {
"fast": {
"gpu": "fast",
"canVerify": True,
"recentTerminal": 100,
"recentSuccess": 80,
"recentWilsonLowerBound": 0.70,
"backlogHours": 10.0,
"healthFactor": 1.0,
}
},
"frameworkStats": {
"text-generation": {
"fast": {
"vllm": {"modelCount": 1000, "wilsonLowerBound": 0.20}
}
}
},
},
)
client = FailingMarketClient()
manager = MarketIntelligenceManager(path, log_fn=lambda _message: None)
state = manager.prepare(
client,
supported_gpus=["fast"],
task_types=["text-generation"],
now=now + timedelta(seconds=30),
)
self.assertEqual(3, state["version"])
self.assertTrue(state["gpuStats"]["fast"]["submissionEligible"])
self.assertEqual(0, client.calls)
def test_framework_ranking_uses_confidence_bound_and_ignores_tiny_samples(self) -> None: def test_framework_ranking_uses_confidence_bound_and_ignores_tiny_samples(self) -> None:
manager = MarketIntelligenceManager("unused.json", framework_min_samples=100) manager = MarketIntelligenceManager("unused.json", framework_min_samples=100)
manager.state = { manager.state = {
@@ -332,6 +379,76 @@ class MarketIntelligenceTests(unittest.TestCase):
assert candidate is not None assert candidate is not None
self.assertEqual("vllm", candidate.framework) self.assertEqual("vllm", candidate.framework)
def test_unproven_public_framework_receives_no_submission(self) -> None:
manager = MarketIntelligenceManager("unused.json")
manager.state = {
"frameworkStats": {
"text-generation": {
"Cambricon_mlu-370-x4": {
"vllm": {"modelCount": 299, "wilsonLowerBound": 0.50},
}
}
}
}
candidate = choose_candidate_for_gpu(
model=HFModelSummary(
repo_id="owner/model",
downloads=100,
last_modified=None,
pipeline_tag="text-generation",
),
inspection=ModelInspection(repo_id="owner/model", weight_files=["model.safetensors"]),
template_selector=TemplateSelector(),
task_types=["text-generation"],
target_gpu="Cambricon_mlu-370-x4",
market_intelligence=manager,
)
self.assertIsNone(candidate)
def test_five_consecutive_local_failures_circuit_break_framework(self) -> None:
manager = MarketIntelligenceManager("unused.json", framework_min_samples=100)
manager.state = {
"frameworkStats": {
"text-generation": {
"Iluvatar_bi-100": {
"vllm": {"modelCount": 1000, "wilsonLowerBound": 0.40},
"transformers": {"modelCount": 1000, "wilsonLowerBound": 0.20},
}
}
}
}
manager.set_local_outcome_stats(
{
"combinationStats": {},
"recentCombinationStats": {
"Iluvatar_bi-100|vllm|text-generation": {
"successCount": 0,
"failureCount": 5,
"consecutiveFailures": 5,
"lastTerminalAt": datetime.now(timezone.utc).isoformat(),
}
},
}
)
candidate = choose_candidate_for_gpu(
model=HFModelSummary(
repo_id="owner/model",
downloads=100,
last_modified=None,
pipeline_tag="text-generation",
),
inspection=ModelInspection(repo_id="owner/model", weight_files=["model.safetensors"]),
template_selector=TemplateSelector(),
task_types=["text-generation"],
target_gpu="Iluvatar_bi-100",
market_intelligence=manager,
)
self.assertIsNotNone(candidate)
assert candidate is not None
self.assertEqual("transformers", candidate.framework)
metadata = manager.framework_metadata("text-generation", "Iluvatar_bi-100", "vllm")
self.assertTrue(metadata["frameworkCircuitOpen"])
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()