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,9 +572,16 @@ 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]]]:
|
||||||
|
pending: list[tuple[str, list[str]]] = []
|
||||||
for target_gpu in route_target_gpus:
|
for target_gpu in route_target_gpus:
|
||||||
if submission_exclusion_store is not None and submission_exclusion_store.is_blocked(model.repo_id, target_gpu):
|
if submission_exclusion_store is not None and submission_exclusion_store.is_blocked(model.repo_id, target_gpu):
|
||||||
skipped.append(
|
skipped.append(
|
||||||
@@ -616,9 +624,22 @@ def process_model_for_candidates(
|
|||||||
if market_intelligence is not None and market_intelligence.has_framework_route(task_type, target_gpu):
|
if market_intelligence is not None and market_intelligence.has_framework_route(task_type, target_gpu):
|
||||||
compatible_task_types.append(task_type)
|
compatible_task_types.append(task_type)
|
||||||
if not compatible_task_types:
|
if not compatible_task_types:
|
||||||
skipped.append({"repoId": model.repo_id, "targetGpu": target_gpu, "reason": "no_compatible_auto_template_or_framework"})
|
skipped.append(
|
||||||
|
{
|
||||||
|
"repoId": model.repo_id,
|
||||||
|
"targetGpu": target_gpu,
|
||||||
|
"reason": "no_compatible_auto_template_or_framework",
|
||||||
|
}
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
pending_task_types_by_gpu.append((target_gpu, compatible_task_types))
|
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,7 +649,11 @@ 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:
|
||||||
|
before_count = len(candidates)
|
||||||
|
for target_gpu, task_types in pending_routes:
|
||||||
|
if len(candidates) - before_count >= MAX_ROUTES_PER_MODEL:
|
||||||
|
break
|
||||||
best, preflight_reason = choose_candidate_for_gpu_detailed(
|
best, preflight_reason = choose_candidate_for_gpu_detailed(
|
||||||
model=model,
|
model=model,
|
||||||
inspection=inspection,
|
inspection=inspection,
|
||||||
@@ -663,6 +688,17 @@ def process_model_for_candidates(
|
|||||||
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