feat: add queue-aware adaptive scheduling

This commit is contained in:
CoolBoy
2026-08-04 20:22:08 +08:00
parent ccae7ff8f3
commit 882479e43e
12 changed files with 1586 additions and 95 deletions

View File

@@ -18,6 +18,15 @@ from history_stats import (
load_ledger,
update_history_archive,
)
from market_intelligence import (
DEFAULT_FETCH_WORKERS,
DEFAULT_FRAMEWORK_MIN_SAMPLES,
DEFAULT_FRAMEWORK_REFRESH_SECONDS,
DEFAULT_MARKET_INTELLIGENCE_PATH,
DEFAULT_QUEUE_REFRESH_SECONDS,
DEFAULT_THROUGHPUT_WINDOW_HOURS,
MarketIntelligenceManager,
)
from modelhub_client import (
DEFAULT_CAPACITY_STATE_PATH,
ModelHubAPIError,
@@ -30,7 +39,7 @@ from models import CandidateModel, HFModelSummary, ModelInspection
from outcome_tracker import DEFAULT_OUTCOMES_PATH, OutcomeTracker
from submission_claims import DEFAULT_CLAIMS_PATH, SubmissionClaimStore, candidate_key, diversify_candidates
from submission_exclusions import DEFAULT_SUBMISSION_EXCLUSIONS_PATH, SubmissionExclusionStore
from task_registry import TASK_SPEC_BY_TYPE, all_task_types, choose_framework_for_task, choose_text_generation_framework, pipeline_tags_for_task_types, task_specs_for_model
from task_registry import TASK_SPEC_BY_TYPE, all_task_types, compatible_frameworks_for_task, pipeline_tags_for_task_types, task_specs_for_model
from template_selector import TemplateSelector
@@ -61,6 +70,11 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="Disable adaptive 50/30/20 GPU scheduling and keep the legacy candidate order",
)
parser.add_argument(
"--disable-market-intelligence",
action="store_true",
help="Disable live queue/throughput and public framework statistics",
)
parser.add_argument(
"--gpu-strategy-refresh-submissions",
type=int,
@@ -107,6 +121,41 @@ def build_parser() -> argparse.ArgumentParser:
default=os.getenv("MODELHUB_SUBMISSION_EXCLUSIONS_PATH", str(DEFAULT_SUBMISSION_EXCLUSIONS_PATH)),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--market-intelligence-state-path",
default=os.getenv("MODELHUB_MARKET_INTELLIGENCE_PATH", str(DEFAULT_MARKET_INTELLIGENCE_PATH)),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--market-queue-refresh-seconds",
type=int,
default=int(os.getenv("MODELHUB_MARKET_QUEUE_REFRESH_SECONDS", str(DEFAULT_QUEUE_REFRESH_SECONDS))),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--market-framework-refresh-seconds",
type=int,
default=int(os.getenv("MODELHUB_MARKET_FRAMEWORK_REFRESH_SECONDS", str(DEFAULT_FRAMEWORK_REFRESH_SECONDS))),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--market-throughput-window-hours",
type=int,
default=int(os.getenv("MODELHUB_MARKET_THROUGHPUT_WINDOW_HOURS", str(DEFAULT_THROUGHPUT_WINDOW_HOURS))),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--market-fetch-workers",
type=int,
default=int(os.getenv("MODELHUB_MARKET_FETCH_WORKERS", str(DEFAULT_FETCH_WORKERS))),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--market-framework-min-samples",
type=int,
default=int(os.getenv("MODELHUB_MARKET_FRAMEWORK_MIN_SAMPLES", str(DEFAULT_FRAMEWORK_MIN_SAMPLES))),
help=argparse.SUPPRESS,
)
parser.add_argument(
"--gpu-strategy-state-path",
default=os.getenv("MODELHUB_GPU_STRATEGY_STATE_PATH", str(DEFAULT_GPU_STRATEGY_PATH)),
@@ -209,37 +258,90 @@ def choose_candidate_for_gpu(
template_selector: TemplateSelector,
task_types: list[str],
target_gpu: str,
market_intelligence: MarketIntelligenceManager | None = None,
) -> CandidateModel | None:
for task_type in task_types:
supported_frameworks = template_selector.supported_frameworks_for_auto(task_type, target_gpu)
if not supported_frameworks:
continue
compatible_frameworks: list[str] = []
try:
framework = choose_framework_for_task(task_type, target_gpu, supported_frameworks, inspection)
template = template_selector.select_template(task_type, framework, target_gpu)
config_params = template_selector.render_config(
template,
gguf_filename=inspection.selected_gguf if framework == "llamacpp" else None,
)
if supported_frameworks:
compatible_frameworks = compatible_frameworks_for_task(
task_type,
target_gpu,
supported_frameworks,
inspection,
)
except ValueError:
pass
if market_intelligence is not None:
discovered = market_intelligence.compatible_discovered_frameworks(
task_type=task_type,
target_gpu=target_gpu,
inspection=inspection,
)
compatible_frameworks.extend(
framework for framework in discovered if framework not in compatible_frameworks
)
if not compatible_frameworks:
continue
spec = TASK_SPEC_BY_TYPE[task_type]
return CandidateModel(
repo_id=model.repo_id,
model_address=model.model_address,
pipeline_tag=model.pipeline_tag,
modality=spec.modality,
task_type=task_type,
target_gpu=target_gpu,
framework=framework,
template_id=template.template_id,
config_params=config_params,
downloads=model.downloads,
last_modified=model.last_modified,
gguf_filename=inspection.selected_gguf,
score=0.0,
warnings=[],
)
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:
config_params = (
market_intelligence.official_config(
task_type=task_type,
target_gpu=target_gpu,
framework=framework,
gguf_filename=inspection.selected_gguf,
)
if market_intelligence is not None
else None
)
if config_params is not None:
template_id = f"modelhub-live-{task_type}-{framework}-{target_gpu}".lower().replace("_", "-")
else:
try:
template = template_selector.select_template(task_type, framework, target_gpu)
config_params = template_selector.render_config(
template,
gguf_filename=inspection.selected_gguf if framework == "llamacpp" else None,
)
template_id = template.template_id
except (KeyError, ValueError):
continue
framework_metadata = (
market_intelligence.framework_metadata(task_type, target_gpu, framework)
if market_intelligence is not None
else {}
)
score = float(framework_metadata.get("frameworkCombinedScore") or 0.0)
warnings: list[str] = []
if bool(framework_metadata.get("frameworkEvidenceQualified", False)):
warnings.append("framework_selected_from_success_evidence")
if config_params is not None and bool(framework_metadata.get("frameworkOfficialConfigValid", False)):
warnings.append("official_build_config_synced")
spec = TASK_SPEC_BY_TYPE[task_type]
return CandidateModel(
repo_id=model.repo_id,
model_address=model.model_address,
pipeline_tag=model.pipeline_tag,
modality=spec.modality,
task_type=task_type,
target_gpu=target_gpu,
framework=framework,
template_id=template_id,
config_params=config_params,
downloads=model.downloads,
last_modified=model.last_modified,
gguf_filename=inspection.selected_gguf,
score=score,
warnings=warnings,
)
return None
@@ -284,6 +386,7 @@ def process_model_for_candidates(
allowed_task_types: list[str],
outcome_tracker: OutcomeTracker | None = None,
submission_exclusion_store: SubmissionExclusionStore | None = None,
market_intelligence: MarketIntelligenceManager | None = None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]]]:
specs = [spec for spec in task_specs_for_model(model) if spec.task_type in allowed_task_types]
if not specs:
@@ -345,11 +448,16 @@ def process_model_for_candidates(
template_selector=template_selector,
task_types=task_types,
target_gpu=target_gpu,
market_intelligence=market_intelligence,
)
if best is None:
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "no_compatible_auto_template_or_framework"})
continue
candidates.append(candidate_to_record(best))
record = candidate_to_record(best)
if market_intelligence is not None:
record.update(market_intelligence.gpu_metadata(target_gpu))
record.update(market_intelligence.framework_metadata(best.task_type, target_gpu, best.framework))
candidates.append(record)
return candidates, skipped, failed
@@ -423,6 +531,7 @@ def collect_candidates_from_models(
outcome_tracker: OutcomeTracker | None,
submission_exclusion_store: SubmissionExclusionStore | None,
read_concurrency: int,
market_intelligence: MarketIntelligenceManager | None = None,
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[dict[str, Any]], int]:
candidates: list[dict[str, Any]] = []
skipped: list[dict[str, Any]] = []
@@ -451,6 +560,7 @@ def collect_candidates_from_models(
allowed_task_types=selected_task_types,
outcome_tracker=outcome_tracker,
submission_exclusion_store=submission_exclusion_store,
market_intelligence=market_intelligence,
): index
for index, model in enumerate(chunk)
}
@@ -624,6 +734,8 @@ def run_submission(
)
strategy_manager: GPUStrategyManager | None = None
strategy_summary: dict[str, Any] = {"enabled": False}
market_intelligence: MarketIntelligenceManager | None = None
market_summary: dict[str, Any] = {"enabled": False}
strategy_enabled = (
not bool(getattr(args, "disable_gpu_strategy", False))
and not bool(getattr(args, "gpu", None) or getattr(args, "gpus", None))
@@ -707,15 +819,19 @@ def run_submission(
"historyArchivePath": str(history_archive_path),
"historyArchiveRecordCount": len(archived_history),
"gpuStrategy": strategy_summary,
"marketIntelligence": market_summary,
"scanLimit": 0,
"candidateGoal": 0,
"scanStages": [],
"scannedModels": 0,
"candidateCount": 0,
"candidateFrameworkCounts": {},
"targetSubmitCount": 0,
"maxSubmitAttempts": 0,
"plannedSubmitCount": 0,
"submittedCount": 0,
"submittedGpuCounts": {},
"submittedFrameworkCounts": {},
"duplicateCount": 0,
"modelGpuUniquenessRejectedCount": 0,
"skippedCount": 0,
@@ -728,12 +844,63 @@ def run_submission(
"outcomeSyncCount": synced_count,
}
market_capable = (
hasattr(modelhub_client, "list_machine_info")
and hasattr(modelhub_client, "list_framework_stats")
and hasattr(modelhub_client, "list_tasks_page")
)
if not bool(getattr(args, "disable_market_intelligence", False)) and market_capable:
market_intelligence = MarketIntelligenceManager(
Path(getattr(args, "market_intelligence_state_path", DEFAULT_MARKET_INTELLIGENCE_PATH)),
queue_refresh_seconds=max(
60,
int(getattr(args, "market_queue_refresh_seconds", DEFAULT_QUEUE_REFRESH_SECONDS) or DEFAULT_QUEUE_REFRESH_SECONDS),
),
framework_refresh_seconds=max(
300,
int(
getattr(args, "market_framework_refresh_seconds", DEFAULT_FRAMEWORK_REFRESH_SECONDS)
or DEFAULT_FRAMEWORK_REFRESH_SECONDS
),
),
throughput_window_hours=max(
1,
int(
getattr(args, "market_throughput_window_hours", DEFAULT_THROUGHPUT_WINDOW_HOURS)
or DEFAULT_THROUGHPUT_WINDOW_HOURS
),
),
fetch_workers=max(
1,
int(getattr(args, "market_fetch_workers", DEFAULT_FETCH_WORKERS) or DEFAULT_FETCH_WORKERS),
),
framework_min_samples=max(
1,
int(
getattr(args, "market_framework_min_samples", DEFAULT_FRAMEWORK_MIN_SAMPLES)
or DEFAULT_FRAMEWORK_MIN_SAMPLES
),
),
)
market_intelligence.prepare(
modelhub_client,
supported_gpus=target_gpus,
task_types=selected_task_types,
now=now,
)
try:
market_intelligence.set_local_outcome_stats(outcome_tracker.get_stats_report())
except Exception:
market_intelligence.set_local_outcome_stats(None)
market_summary = market_intelligence.summary()
if strategy_enabled:
strategy_manager = GPUStrategyManager(
Path(getattr(args, "gpu_strategy_state_path", DEFAULT_GPU_STRATEGY_PATH)),
refresh_submissions=max(1, int(getattr(args, "gpu_strategy_refresh_submissions", 200) or 200)),
recent_terminal_window=max(1, int(getattr(args, "gpu_strategy_recent_window", 1000) or 1000)),
long_term_min_samples=max(1, int(getattr(args, "gpu_strategy_min_long_samples", 100) or 100)),
market_intelligence=market_intelligence,
)
strategy_manager.prepare(modelhub_client, supported_gpus=target_gpus, now=now)
strategy_summary = strategy_manager.summary()
@@ -824,6 +991,7 @@ def run_submission(
outcome_tracker=outcome_tracker,
submission_exclusion_store=submission_exclusion_store,
read_concurrency=max(1, args.read_concurrency),
market_intelligence=market_intelligence,
)
candidates.extend(stage_candidates)
skipped.extend(stage_skipped)
@@ -1027,6 +1195,9 @@ def run_submission(
outcome_tracker.save()
skip_reason_counts = Counter(str(item.get("reason") or "unknown") for item in skipped)
failure_reason_counts = Counter(str(item.get("reason") or "unknown") for item in failed)
candidate_framework_counts = Counter(str(item.get("framework") or "unknown") for item in candidates)
submitted_gpu_counts = Counter(str(item.get("targetGpu") or "unknown") for item in submitted)
submitted_framework_counts = Counter(str(item.get("framework") or "unknown") for item in submitted)
summary = {
"generatedAt": now.isoformat(),
@@ -1044,11 +1215,13 @@ def run_submission(
"historyArchivePath": str(history_archive_path),
"historyArchiveRecordCount": len(archived_history),
"gpuStrategy": strategy_summary,
"marketIntelligence": market_summary,
"scanLimit": scan_limit,
"candidateGoal": candidate_goal,
"scanStages": scan_stages,
"scannedModels": scanned_model_count,
"candidateCount": len(candidates),
"candidateFrameworkCounts": dict(candidate_framework_counts.most_common()),
"targetSubmitCount": target_submit_count,
"maxSubmitAttempts": 0 if args.dry_run else min(
len(diversified_candidates),
@@ -1058,6 +1231,8 @@ def run_submission(
"duplicateCount": len(duplicate_candidates),
"modelGpuUniquenessRejectedCount": len(uniqueness_rejected_candidates),
"submittedCount": len(submitted),
"submittedGpuCounts": dict(submitted_gpu_counts.most_common()),
"submittedFrameworkCounts": dict(submitted_framework_counts.most_common()),
"skippedCount": len(skipped),
"skipReasonCounts": dict(skip_reason_counts.most_common()),
"failedCount": len(failed),