fix: prioritize preferred GPU routes with fallback
This commit is contained in:
11
README.md
11
README.md
@@ -444,11 +444,18 @@ durable Git checkpoint per roughly 2,000 scanned rows. Raw rows are discarded
|
|||||||
after every aggregate merge. Transient API failure pauses at the current cursor
|
after every aggregate merge. Transient API failure pauses at the current cursor
|
||||||
for the next poll cycle; successful completion disables the scan permanently.
|
for the next poll cycle; successful completion disables the scan permanently.
|
||||||
|
|
||||||
|
Version `2026.09.18.1` keeps the first six ranked GPUs as the preferred routes
|
||||||
|
for each model and only falls back to lower-ranked eligible GPUs when all six
|
||||||
|
preferred routes are unavailable. Candidate generation retains at most six
|
||||||
|
routes per model, preserving successful-route and queue ordering without
|
||||||
|
discarding capacity on lower-ranked GPUs when the preferred tranche cannot run
|
||||||
|
the model.
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
Create a tag and submit the repository URL plus tag in "我的适配智能体".
|
Create a tag and submit the repository URL plus tag in "我的适配智能体".
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git tag -a agent-v33 -m "ModelHub agent 2026.09.04.4"
|
git tag -a agent-v34 -m "ModelHub agent 2026.09.18.1"
|
||||||
git push origin main agent-v33
|
git push origin main agent-v34
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ DEFAULT_RUNS_DIR = Path("runs")
|
|||||||
DEFAULT_LEDGER_PATH = Path("ledger/submissions.jsonl")
|
DEFAULT_LEDGER_PATH = Path("ledger/submissions.jsonl")
|
||||||
ADAPTIVE_SCAN_MAX_MODELS = 3000
|
ADAPTIVE_SCAN_MAX_MODELS = 3000
|
||||||
ADAPTIVE_SCAN_MIN_FALLBACK_MODELS = 500
|
ADAPTIVE_SCAN_MIN_FALLBACK_MODELS = 500
|
||||||
|
MAX_ROUTES_PER_MODEL = 6
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
@@ -571,54 +572,74 @@ def process_model_for_candidates(
|
|||||||
skipped: list[dict[str, Any]] = []
|
skipped: list[dict[str, Any]] = []
|
||||||
failed: list[dict[str, Any]] = []
|
failed: list[dict[str, Any]] = []
|
||||||
allowed_task_types_set = [spec.task_type for spec in specs]
|
allowed_task_types_set = [spec.task_type for spec in specs]
|
||||||
pending_task_types_by_gpu: list[tuple[str, list[str]]] = []
|
# Preserve the routing order: the first six GPUs are the preferred
|
||||||
|
# tranche. Lower-ranked GPUs are a fallback only when the preferred
|
||||||
|
# tranche produces no candidate for this model. This keeps queue/quality
|
||||||
|
# prioritization deterministic while still recovering from a model being
|
||||||
|
# already processed, too large, or unsupported on every preferred GPU.
|
||||||
|
preferred_target_gpus = target_gpus[:MAX_ROUTES_PER_MODEL]
|
||||||
|
fallback_target_gpus = target_gpus[MAX_ROUTES_PER_MODEL:]
|
||||||
|
|
||||||
route_target_gpus = target_gpus[:6] if official_registry is not None else target_gpus
|
def prepare_pending_routes(route_target_gpus: list[str]) -> list[tuple[str, list[str]]]:
|
||||||
for target_gpu in route_target_gpus:
|
pending: list[tuple[str, list[str]]] = []
|
||||||
if submission_exclusion_store is not None and submission_exclusion_store.is_blocked(model.repo_id, target_gpu):
|
for target_gpu in route_target_gpus:
|
||||||
skipped.append(
|
if submission_exclusion_store is not None and submission_exclusion_store.is_blocked(model.repo_id, target_gpu):
|
||||||
{"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "model_gpu_uniqueness_blocklist"}
|
skipped.append(
|
||||||
)
|
{"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "model_gpu_uniqueness_blocklist"}
|
||||||
continue
|
)
|
||||||
if target_gpu in processed_gpus:
|
|
||||||
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "already_processed_for_gpu"})
|
|
||||||
continue
|
|
||||||
if outcome_tracker and outcome_tracker.is_model_gpu_failed(model.repo_id, target_gpu):
|
|
||||||
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "local_failure_cooldown_24h"})
|
|
||||||
continue
|
|
||||||
|
|
||||||
exact_task_types = list(allowed_task_types_set)
|
|
||||||
if official_registry is not None:
|
|
||||||
exact_task_types = official_registry.task_types_for(
|
|
||||||
modelhub_client,
|
|
||||||
model_address=model.model_address,
|
|
||||||
model_last_modified=model.last_modified.isoformat() if model.last_modified else None,
|
|
||||||
gpu=target_gpu,
|
|
||||||
)
|
|
||||||
route_task_types = (
|
|
||||||
list(exact_task_types)
|
|
||||||
if allow_dynamic_tasks
|
|
||||||
else [task_type for task_type in allowed_task_types_set if task_type in exact_task_types]
|
|
||||||
)
|
|
||||||
compatible_task_types = []
|
|
||||||
for task_type in route_task_types:
|
|
||||||
register_dynamic_task_route(task_type, model.pipeline_tag or task_type)
|
|
||||||
if market_intelligence is not None and not market_intelligence.has_framework_route(task_type, target_gpu):
|
|
||||||
try:
|
|
||||||
market_intelligence.ensure_framework_route(modelhub_client, task_type, target_gpu)
|
|
||||||
except Exception as exc:
|
|
||||||
raise OfficialCapabilityUnavailable(
|
|
||||||
f"framework capability unavailable for {task_type}/{target_gpu}: {type(exc).__name__}: {exc}"
|
|
||||||
) from exc
|
|
||||||
if template_selector.supported_frameworks_for_auto(task_type, target_gpu):
|
|
||||||
compatible_task_types.append(task_type)
|
|
||||||
continue
|
continue
|
||||||
if market_intelligence is not None and market_intelligence.has_framework_route(task_type, target_gpu):
|
if target_gpu in processed_gpus:
|
||||||
compatible_task_types.append(task_type)
|
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "already_processed_for_gpu"})
|
||||||
if not compatible_task_types:
|
continue
|
||||||
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "no_compatible_auto_template_or_framework"})
|
if outcome_tracker and outcome_tracker.is_model_gpu_failed(model.repo_id, target_gpu):
|
||||||
continue
|
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "local_failure_cooldown_24h"})
|
||||||
pending_task_types_by_gpu.append((target_gpu, compatible_task_types))
|
continue
|
||||||
|
|
||||||
|
exact_task_types = list(allowed_task_types_set)
|
||||||
|
if official_registry is not None:
|
||||||
|
exact_task_types = official_registry.task_types_for(
|
||||||
|
modelhub_client,
|
||||||
|
model_address=model.model_address,
|
||||||
|
model_last_modified=model.last_modified.isoformat() if model.last_modified else None,
|
||||||
|
gpu=target_gpu,
|
||||||
|
)
|
||||||
|
route_task_types = (
|
||||||
|
list(exact_task_types)
|
||||||
|
if allow_dynamic_tasks
|
||||||
|
else [task_type for task_type in allowed_task_types_set if task_type in exact_task_types]
|
||||||
|
)
|
||||||
|
compatible_task_types = []
|
||||||
|
for task_type in route_task_types:
|
||||||
|
register_dynamic_task_route(task_type, model.pipeline_tag or task_type)
|
||||||
|
if market_intelligence is not None and not market_intelligence.has_framework_route(task_type, target_gpu):
|
||||||
|
try:
|
||||||
|
market_intelligence.ensure_framework_route(modelhub_client, task_type, target_gpu)
|
||||||
|
except Exception as exc:
|
||||||
|
raise OfficialCapabilityUnavailable(
|
||||||
|
f"framework capability unavailable for {task_type}/{target_gpu}: {type(exc).__name__}: {exc}"
|
||||||
|
) from exc
|
||||||
|
if template_selector.supported_frameworks_for_auto(task_type, target_gpu):
|
||||||
|
compatible_task_types.append(task_type)
|
||||||
|
continue
|
||||||
|
if market_intelligence is not None and market_intelligence.has_framework_route(task_type, target_gpu):
|
||||||
|
compatible_task_types.append(task_type)
|
||||||
|
if not compatible_task_types:
|
||||||
|
skipped.append(
|
||||||
|
{
|
||||||
|
"repoId": model.repo_id,
|
||||||
|
"targetGpu": target_gpu,
|
||||||
|
"reason": "no_compatible_auto_template_or_framework",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
pending.append((target_gpu, compatible_task_types))
|
||||||
|
return pending
|
||||||
|
|
||||||
|
pending_task_types_by_gpu = prepare_pending_routes(preferred_target_gpus)
|
||||||
|
used_fallback_routes = False
|
||||||
|
if not pending_task_types_by_gpu and fallback_target_gpus:
|
||||||
|
pending_task_types_by_gpu = prepare_pending_routes(fallback_target_gpus)
|
||||||
|
used_fallback_routes = True
|
||||||
|
|
||||||
if not pending_task_types_by_gpu:
|
if not pending_task_types_by_gpu:
|
||||||
return candidates, skipped, failed
|
return candidates, skipped, failed
|
||||||
@@ -628,41 +649,56 @@ def process_model_for_candidates(
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return [], skipped, [{"repoId": model.repo_id, "reason": str(exc)}]
|
return [], skipped, [{"repoId": model.repo_id, "reason": str(exc)}]
|
||||||
|
|
||||||
for target_gpu, task_types in pending_task_types_by_gpu:
|
def evaluate_pending_routes(pending_routes: list[tuple[str, list[str]]]) -> int:
|
||||||
best, preflight_reason = choose_candidate_for_gpu_detailed(
|
before_count = len(candidates)
|
||||||
model=model,
|
for target_gpu, task_types in pending_routes:
|
||||||
inspection=inspection,
|
if len(candidates) - before_count >= MAX_ROUTES_PER_MODEL:
|
||||||
template_selector=template_selector,
|
break
|
||||||
task_types=task_types,
|
best, preflight_reason = choose_candidate_for_gpu_detailed(
|
||||||
target_gpu=target_gpu,
|
model=model,
|
||||||
market_intelligence=market_intelligence,
|
inspection=inspection,
|
||||||
preflight_advisor=preflight_advisor,
|
template_selector=template_selector,
|
||||||
require_official_config=official_registry is not None,
|
task_types=task_types,
|
||||||
config_optimizer=config_optimizer,
|
target_gpu=target_gpu,
|
||||||
)
|
market_intelligence=market_intelligence,
|
||||||
if best is None:
|
preflight_advisor=preflight_advisor,
|
||||||
reason = (
|
require_official_config=official_registry is not None,
|
||||||
preflight_reason
|
config_optimizer=config_optimizer,
|
||||||
or (
|
)
|
||||||
"no_publicly_vetted_compatible_framework"
|
if best is None:
|
||||||
if market_intelligence is not None
|
reason = (
|
||||||
else "no_compatible_auto_template_or_framework"
|
preflight_reason
|
||||||
|
or (
|
||||||
|
"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})
|
||||||
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": reason})
|
continue
|
||||||
continue
|
record = candidate_to_record(best)
|
||||||
record = candidate_to_record(best)
|
if outcome_tracker is not None:
|
||||||
if outcome_tracker is not None:
|
record["transformersPrerequisiteSatisfied"] = (
|
||||||
record["transformersPrerequisiteSatisfied"] = (
|
outcome_tracker.has_non_transformers_failure(model.repo_id)
|
||||||
outcome_tracker.has_non_transformers_failure(model.repo_id)
|
)
|
||||||
)
|
if market_intelligence is not None:
|
||||||
if market_intelligence is not None:
|
record.update(market_intelligence.gpu_metadata(target_gpu))
|
||||||
record.update(market_intelligence.gpu_metadata(target_gpu))
|
record.update(market_intelligence.framework_metadata(best.task_type, target_gpu, best.framework))
|
||||||
record.update(market_intelligence.framework_metadata(best.task_type, target_gpu, best.framework))
|
optimization = (record.get("preflightMetadata") or {}).get("configOptimization") or {}
|
||||||
optimization = (record.get("preflightMetadata") or {}).get("configOptimization") or {}
|
if optimization.get("applied"):
|
||||||
if optimization.get("applied"):
|
record["frameworkConfigSource"] = "official_plus_learned_patch"
|
||||||
record["frameworkConfigSource"] = "official_plus_learned_patch"
|
candidates.append(record)
|
||||||
candidates.append(record)
|
return len(candidates) - before_count
|
||||||
|
|
||||||
|
evaluate_pending_routes(pending_task_types_by_gpu)
|
||||||
|
# If preferred GPUs had routes but every one failed candidate preflight,
|
||||||
|
# give the remaining eligible GPUs one chance. Once a
|
||||||
|
# preferred candidate exists, lower-ranked GPUs stay untouched for this
|
||||||
|
# model and remain available for later scans.
|
||||||
|
if not candidates and not used_fallback_routes and fallback_target_gpus:
|
||||||
|
fallback_pending = prepare_pending_routes(fallback_target_gpus)
|
||||||
|
if fallback_pending:
|
||||||
|
evaluate_pending_routes(fallback_pending)
|
||||||
|
|
||||||
return candidates, skipped, failed
|
return candidates, skipped, failed
|
||||||
|
|
||||||
@@ -968,7 +1004,7 @@ def one_candidate_per_model(candidates: list[dict[str, Any]]) -> list[dict[str,
|
|||||||
def limit_routes_per_model(
|
def limit_routes_per_model(
|
||||||
candidates: list[dict[str, Any]],
|
candidates: list[dict[str, Any]],
|
||||||
*,
|
*,
|
||||||
max_routes: int = 3,
|
max_routes: int = MAX_ROUTES_PER_MODEL,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
selected: list[dict[str, Any]] = []
|
selected: list[dict[str, Any]] = []
|
||||||
counts: Counter[str] = Counter()
|
counts: Counter[str] = Counter()
|
||||||
@@ -1453,7 +1489,7 @@ def run_submission(
|
|||||||
config_optimizer=config_optimizer,
|
config_optimizer=config_optimizer,
|
||||||
allow_dynamic_tasks=not bool(getattr(args, "task_types", None)),
|
allow_dynamic_tasks=not bool(getattr(args, "task_types", None)),
|
||||||
)
|
)
|
||||||
stage_candidates = limit_routes_per_model(stage_candidates, max_routes=3)
|
stage_candidates = limit_routes_per_model(stage_candidates, max_routes=MAX_ROUTES_PER_MODEL)
|
||||||
remaining_candidate_capacity = max(0, candidate_goal - current_candidate_models)
|
remaining_candidate_capacity = max(0, candidate_goal - current_candidate_models)
|
||||||
stage_candidates = limit_candidate_models(
|
stage_candidates = limit_candidate_models(
|
||||||
stage_candidates,
|
stage_candidates,
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
AGENT_VERSION = "2026.09.04.4"
|
AGENT_VERSION = "2026.09.18.1"
|
||||||
|
|||||||
@@ -68,6 +68,51 @@ class ExactGpuProcessedClient:
|
|||||||
return {"code": 0, "data": {"id": "should-not-submit"}}
|
return {"code": 0, "data": {"id": "should-not-submit"}}
|
||||||
|
|
||||||
|
|
||||||
|
class AllGpuOfficialRegistry:
|
||||||
|
@staticmethod
|
||||||
|
def task_types_for(_client, *, model_address: str, model_last_modified: str | None, gpu: str) -> list[str]:
|
||||||
|
del model_address, model_last_modified, gpu
|
||||||
|
return ["text-generation"]
|
||||||
|
|
||||||
|
|
||||||
|
class AllGpuVettedMarket:
|
||||||
|
@staticmethod
|
||||||
|
def has_framework_route(_task_type: str, _target_gpu: str) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def ensure_framework_route(_client, _task_type: str, _target_gpu: str) -> None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def selectable_frameworks(
|
||||||
|
*, task_type: str, target_gpu: str, incumbent_frameworks: list[str], inspection
|
||||||
|
) -> list[str]:
|
||||||
|
del task_type, target_gpu, incumbent_frameworks, inspection
|
||||||
|
return ["vllm"]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def official_config(
|
||||||
|
*, task_type: str, target_gpu: str, framework: str, gguf_filename: str | None = None
|
||||||
|
) -> str:
|
||||||
|
del task_type, target_gpu, framework, gguf_filename
|
||||||
|
return "framework: vllm"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def framework_metadata(_task_type: str, _target_gpu: str, _framework: str) -> dict:
|
||||||
|
return {"frameworkCombinedScore": 1.0, "frameworkOfficialConfigValid": True}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def gpu_metadata(_target_gpu: str) -> dict:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class SeventhGpuFallbackMarket(AllGpuVettedMarket):
|
||||||
|
@staticmethod
|
||||||
|
def has_framework_route(_task_type: str, target_gpu: str) -> bool:
|
||||||
|
return target_gpu == "gpu-7"
|
||||||
|
|
||||||
|
|
||||||
class UniquenessRejectingClient:
|
class UniquenessRejectingClient:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_task(_payload: dict) -> dict:
|
def add_task(_payload: dict) -> dict:
|
||||||
@@ -107,6 +152,50 @@ def candidate(target_gpu: str = "Vastai_va16") -> dict:
|
|||||||
|
|
||||||
|
|
||||||
class SubmissionSafetyTests(unittest.TestCase):
|
class SubmissionSafetyTests(unittest.TestCase):
|
||||||
|
def test_official_routes_prefer_first_six_gpus(self) -> None:
|
||||||
|
target_gpus = [f"gpu-{index}" for index in range(1, 8)]
|
||||||
|
model = HFModelSummary(
|
||||||
|
repo_id="owner/model",
|
||||||
|
downloads=100,
|
||||||
|
last_modified=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||||
|
pipeline_tag="text-generation",
|
||||||
|
)
|
||||||
|
candidates, skipped, failed = process_model_for_candidates(
|
||||||
|
model=model,
|
||||||
|
hf_discovery=SafeDiscovery(), # type: ignore[arg-type]
|
||||||
|
modelhub_client=OtherGpuProcessedClient(), # type: ignore[arg-type]
|
||||||
|
template_selector=TemplateSelector(),
|
||||||
|
target_gpus=target_gpus,
|
||||||
|
allowed_task_types=["text-generation"],
|
||||||
|
market_intelligence=AllGpuVettedMarket(), # type: ignore[arg-type]
|
||||||
|
official_registry=AllGpuOfficialRegistry(), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
self.assertEqual(target_gpus[:6], [item["targetGpu"] for item in candidates])
|
||||||
|
self.assertEqual([], skipped)
|
||||||
|
self.assertEqual([], failed)
|
||||||
|
|
||||||
|
def test_official_routes_fall_back_after_preferred_six_are_unusable(self) -> None:
|
||||||
|
target_gpus = [f"gpu-{index}" for index in range(1, 8)]
|
||||||
|
model = HFModelSummary(
|
||||||
|
repo_id="owner/model",
|
||||||
|
downloads=100,
|
||||||
|
last_modified=datetime(2026, 1, 1, tzinfo=timezone.utc),
|
||||||
|
pipeline_tag="text-generation",
|
||||||
|
)
|
||||||
|
candidates, skipped, failed = process_model_for_candidates(
|
||||||
|
model=model,
|
||||||
|
hf_discovery=SafeDiscovery(), # type: ignore[arg-type]
|
||||||
|
modelhub_client=OtherGpuProcessedClient(), # type: ignore[arg-type]
|
||||||
|
template_selector=TemplateSelector(),
|
||||||
|
target_gpus=target_gpus,
|
||||||
|
allowed_task_types=["text-generation"],
|
||||||
|
market_intelligence=SeventhGpuFallbackMarket(), # type: ignore[arg-type]
|
||||||
|
official_registry=AllGpuOfficialRegistry(), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
self.assertEqual(["gpu-7"], [item["targetGpu"] for item in candidates])
|
||||||
|
self.assertEqual(6, len(skipped))
|
||||||
|
self.assertEqual([], failed)
|
||||||
|
|
||||||
def test_precheck_tracks_processed_gpus_without_blocking_other_gpus(self) -> None:
|
def test_precheck_tracks_processed_gpus_without_blocking_other_gpus(self) -> None:
|
||||||
payload = {
|
payload = {
|
||||||
"code": 0,
|
"code": 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user