Safetensors
GGUF
Turkish
llama
Llama-3
instruct
finetune
chatml
gpt4
synthetic data
distillation
function calling
json mode
axolotl
roleplaying
chat
Instructions to use tda45/TdAI with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use tda45/TdAI with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="tda45/TdAI", filename="llama.cpp/models/ggml-vocab-aquila.gguf", )
output = llm( "Once upon a time,", max_tokens=512, echo=True ) print(output)
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use tda45/TdAI with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf tda45/TdAI # Run inference directly in the terminal: llama cli -hf tda45/TdAI
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./llama-cli -hf tda45/TdAI
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf tda45/TdAI # Run inference directly in the terminal: ./build/bin/llama-cli -hf tda45/TdAI
Use Docker
docker model run hf.co/tda45/TdAI
- LM Studio
- Jan
- Ollama
How to use tda45/TdAI with Ollama:
ollama run hf.co/tda45/TdAI
- Unsloth Studio
How to use tda45/TdAI with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for tda45/TdAI to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for tda45/TdAI to start chatting
- Atomic Chat new
- Docker Model Runner
How to use tda45/TdAI with Docker Model Runner:
docker model run hf.co/tda45/TdAI
- Lemonade
How to use tda45/TdAI with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull tda45/TdAI
Run and chat with the model
lemonade run user.TdAI-{{QUANT_TAG}}List all available models
lemonade list
| """Run llama.cpp Hexagon tests in a single QDC job. | |
| Bundles test scripts into one artifact and submits a single QDC job: | |
| 1. run_bench_tests_posix.py — llama-cli and llama-bench on CPU / GPU / NPU | |
| (from scripts/snapdragon/qdc/) | |
| Results are written to $GITHUB_STEP_SUMMARY when set (GitHub Actions). | |
| Prerequisites: | |
| pip install /path/to/qualcomm_device_cloud_sdk*.whl | |
| Platform is inferred from --device: | |
| android Appium + pytest (Android phones: SM8750 / SM8650 / SM8850) | |
| linux BASH (Linux IoT: QCS9075M) | |
| Required environment variables: | |
| QDC_API_KEY API key from QDC UI -> Users -> Settings -> API Keys | |
| Usage: | |
| python run_qdc_jobs.py \\ | |
| --pkg-dir pkg-snapdragon/llama.cpp \\ | |
| --model-url https://.../Llama-3.2-1B-Instruct-Q4_0.gguf \\ | |
| --device SM8750 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import enum | |
| import logging | |
| import os | |
| import re | |
| import shutil | |
| import sys | |
| import tempfile | |
| import time | |
| import urllib.request | |
| import xml.etree.ElementTree as ET | |
| from dataclasses import dataclass, field | |
| from pathlib import Path | |
| from typing import Callable | |
| from qualcomm_device_cloud_sdk.api import qdc_api | |
| from qualcomm_device_cloud_sdk.logging import configure_logging | |
| from qualcomm_device_cloud_sdk.models import ( | |
| ArtifactType, | |
| JobMode, | |
| JobState, | |
| JobSubmissionParameter, | |
| JobType, | |
| TestFramework, | |
| ) | |
| # configure_logging only sets up the SDK logger; basicConfig is needed for | |
| # our own log.info to reach stdout. | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s %(name)s %(levelname)s - %(message)s", | |
| handlers=[logging.StreamHandler()], | |
| ) | |
| configure_logging(level=logging.INFO, handlers=[logging.StreamHandler()]) | |
| # Silence per-poll GET/status spam from the SDK and its HTTP client. | |
| logging.getLogger("qualcomm_device_cloud").setLevel(logging.WARNING) | |
| logging.getLogger("httpx").setLevel(logging.WARNING) | |
| logging.getLogger("httpcore").setLevel(logging.WARNING) | |
| log = logging.getLogger(__name__) | |
| POLL_INTERVAL = 30 | |
| JOB_TIMEOUT = 3600 | |
| LOG_UPLOAD_TIMEOUT = 600 | |
| CAPACITY_TIMEOUT = 1800 | |
| CAPACITY_POLL = 60 | |
| MAX_CONCURRENT_JOBS = 5 | |
| DEFAULT_RETRIES = 0 | |
| RETRY_DELAY = 300 | |
| TERMINAL_STATES = {JobState.COMPLETED, JobState.CANCELED} | |
| NON_TERMINAL_STATES = {JobState.DISPATCHED, JobState.RUNNING, JobState.SETUP, JobState.SUBMITTED} | |
| class DeviceUnavailableError(Exception): | |
| """Raised when the QDC device resource is not available (retryable).""" | |
| _SCRIPTS_DIR = Path(__file__).parent | |
| _TESTS_DIR = _SCRIPTS_DIR / "tests" | |
| # --- Shared test assets ------------------------------------------------------- | |
| _UTILS = _TESTS_DIR / "utils.py" | |
| _CONFTEST = _TESTS_DIR / "conftest.py" | |
| _PYTEST_LINE_RE = re.compile( | |
| r"(?:[\w/]+\.py::)?(?:\w+::)?([\w\[\].-]+)\s+(PASSED|FAILED|ERROR|SKIPPED)" | |
| ) | |
| _EXCLUDED_LOGS = { | |
| "qdc_android_whole_host-000.log", | |
| "qdc_kernel_host-000.log", | |
| "qdc_LE_whole_host-000.log", | |
| "qdc_LE_kernel_host-000.log", | |
| "script.log", | |
| } | |
| _NON_TERMINAL_STATE_VALUES = {s.value for s in NON_TERMINAL_STATES} | |
| # --- Android (Appium + pytest) assets ---------------------------------------- | |
| _RUN_BENCH = _TESTS_DIR / "run_bench_tests_posix.py" | |
| _RUN_BACKEND_OPS = _TESTS_DIR / "run_backend_ops_posix.py" | |
| _REQUIREMENTS = _SCRIPTS_DIR / "requirements.txt" | |
| _UPSTREAM_ADB_SCRIPTS = ( | |
| "https://raw.githubusercontent.com/ggml-org/llama.cpp/master/scripts/snapdragon/adb" | |
| ) | |
| _ADB_SCRIPT_NAMES = [ | |
| "run-bench.sh", | |
| "run-cli.sh", | |
| "run-completion.sh", | |
| "run-tool.sh", | |
| ] | |
| # --- Linux (BASH) assets ------------------------------------------------------ | |
| _RUN_LINUX_TEMPLATE = _TESTS_DIR / "linux" / "run_linux.sh" | |
| _LINUX_ENTRY_SCRIPT = "/bin/bash /data/local/tmp/TestContent/run_linux.sh" | |
| # ============================================================================= | |
| # Artifact builders (per platform) | |
| # ============================================================================= | |
| class JobResult: | |
| passed: bool | |
| tests: dict[str, bool] = field(default_factory=dict) | |
| raw_logs: dict[str, str] = field(default_factory=dict) | |
| failure_details: dict[str, str] = field(default_factory=dict) | |
| def _write_lf(path: Path, content: str) -> None: | |
| """Write text with LF line endings (required by /bin/bash on Linux).""" | |
| with open(path, "w", encoding="utf-8", newline="\n") as f: | |
| f.write(content) | |
| def _build_android_artifact( | |
| pkg_dir: Path, | |
| stage_dir: Path, | |
| test_mode: str, | |
| model_url: str | None, | |
| ) -> Path: | |
| """Android zip (Appium/pytest). Extracted by QDC under /qdc/appium/. | |
| Zip structure: | |
| llama_cpp_bundle/ installed package (adb pushed to /data/local/tmp/) | |
| run-{bench,cli,completion,tool}.sh upstream adb wrappers (patched) | |
| tests/ | |
| utils.py shared adb helpers | |
| conftest.py Appium pytest fixtures | |
| test_bench_posix.py bench + cli tests (for --test bench or all) | |
| test_backend_ops_posix.py test-backend-ops on HTP0 | |
| requirements.txt | |
| pytest.ini addopts = --junitxml=results.xml | |
| """ | |
| bundle_dir = stage_dir / "llama_cpp_bundle" | |
| shutil.copytree(pkg_dir, bundle_dir) | |
| # Download upstream adb scripts so they land at /qdc/appium/ on the QDC | |
| # runner. They wrap `adb shell` internally. Patch in `chmod +x bin/* lib/*` | |
| # right after `cd $basedir` so device binaries are executable. | |
| for name in _ADB_SCRIPT_NAMES: | |
| url = f"{_UPSTREAM_ADB_SCRIPTS}/{name}" | |
| dest = stage_dir / name | |
| log.info("Downloading %s", url) | |
| urllib.request.urlretrieve(url, str(dest)) | |
| content = dest.read_text() | |
| content = content.replace( | |
| "cd $basedir;", | |
| "cd $basedir; chmod +x bin/* lib/* 2>/dev/null;", | |
| ) | |
| dest.write_text(content) | |
| dest.chmod(0o755) | |
| tests_dir = stage_dir / "tests" | |
| tests_dir.mkdir() | |
| shutil.copy(_UTILS, tests_dir / "utils.py") | |
| shutil.copy(_CONFTEST, tests_dir / "conftest.py") | |
| if test_mode in ("bench", "all"): | |
| assert model_url is not None | |
| (tests_dir / "test_bench_posix.py").write_text( | |
| _RUN_BENCH.read_text().replace("<<MODEL_URL>>", model_url) | |
| ) | |
| if test_mode in ("backend-ops", "all"): | |
| shutil.copy(_RUN_BACKEND_OPS, tests_dir / "test_backend_ops_posix.py") | |
| shutil.copy(_REQUIREMENTS, stage_dir / "requirements.txt") | |
| (stage_dir / "pytest.ini").write_text( | |
| "[pytest]\naddopts = --junitxml=results.xml\n" | |
| ) | |
| zip_base = str(stage_dir / "artifact") | |
| shutil.make_archive(zip_base, "zip", stage_dir) | |
| return Path(f"{zip_base}.zip") | |
| def _build_linux_artifact( | |
| pkg_dir: Path, | |
| stage_dir: Path, | |
| test_mode: str, | |
| model_url: str | None, | |
| ) -> Path: | |
| """Linux IoT zip (BASH framework). Extracted by QDC to /data/local/tmp/TestContent/. | |
| Zip structure: | |
| run_linux.sh entry script (placeholder-substituted, LF line endings) | |
| llama_cpp_bundle/ installed package | |
| """ | |
| bundle_dir = stage_dir / "llama_cpp_bundle" | |
| shutil.copytree(pkg_dir, bundle_dir) | |
| template = _RUN_LINUX_TEMPLATE.read_text(encoding="utf-8") | |
| rendered = template.replace("{MODEL_URL}", model_url or "").replace( | |
| "{TEST_MODE}", test_mode | |
| ) | |
| script_path = stage_dir / "run_linux.sh" | |
| _write_lf(script_path, rendered) | |
| script_path.chmod(0o755) | |
| zip_base = str(stage_dir / "artifact") | |
| shutil.make_archive(zip_base, "zip", stage_dir) | |
| return Path(f"{zip_base}.zip") | |
| # ============================================================================= | |
| # Platform enum + strategy table | |
| # ============================================================================= | |
| class Platform(enum.Enum): | |
| ANDROID = "android" | |
| LINUX = "linux" | |
| class PlatformSpec: | |
| test_framework: TestFramework | |
| entry_script: str | None | |
| build_artifact: Callable[[Path, Path, str, str | None], Path] | |
| job_name_fmt: str | |
| PLATFORM_SPECS: dict[Platform, PlatformSpec] = { | |
| Platform.ANDROID: PlatformSpec( | |
| test_framework=TestFramework.APPIUM, | |
| entry_script=None, | |
| build_artifact=_build_android_artifact, | |
| job_name_fmt="{base}", | |
| ), | |
| Platform.LINUX: PlatformSpec( | |
| test_framework=TestFramework.BASH, | |
| entry_script=_LINUX_ENTRY_SCRIPT, | |
| build_artifact=_build_linux_artifact, | |
| job_name_fmt="{base} (Linux)", | |
| ), | |
| } | |
| DEVICE_PLATFORM: dict[str, Platform] = { | |
| "SM8750": Platform.ANDROID, | |
| "SM8650": Platform.ANDROID, | |
| "SM8850": Platform.ANDROID, | |
| "QCS9075M": Platform.LINUX, | |
| } | |
| # ============================================================================= | |
| # Shared QDC job plumbing | |
| # ============================================================================= | |
| def wait_for_job(client, job_id: str, timeout: int) -> str: | |
| elapsed = 0 | |
| last_state = None | |
| consecutive_errors = 0 | |
| max_consecutive_errors = 5 | |
| while elapsed < timeout: | |
| try: | |
| raw = qdc_api.get_job_status(client, job_id) | |
| consecutive_errors = 0 | |
| except Exception as e: | |
| consecutive_errors += 1 | |
| log.warning( | |
| "Transient error polling job %s (%d/%d): %s", | |
| job_id, | |
| consecutive_errors, | |
| max_consecutive_errors, | |
| e, | |
| ) | |
| if consecutive_errors >= max_consecutive_errors: | |
| raise | |
| time.sleep(POLL_INTERVAL) | |
| elapsed += POLL_INTERVAL | |
| continue | |
| try: | |
| status = JobState(raw) | |
| except ValueError: | |
| status = raw | |
| if status in TERMINAL_STATES: | |
| return raw.lower() | |
| if raw != last_state: | |
| log.info("Job %s: %s", job_id, raw) | |
| last_state = raw | |
| time.sleep(POLL_INTERVAL) | |
| elapsed += POLL_INTERVAL | |
| # Abort to free the QDC concurrency slot instead of leaking it. | |
| try: | |
| qdc_api.abort_job(client, job_id) | |
| log.warning("Aborted job %s after timeout to free concurrency slot", job_id) | |
| except Exception as e: | |
| log.warning("Failed to abort job %s: %s", job_id, e) | |
| raise TimeoutError(f"Job {job_id} did not finish within {timeout}s") | |
| def wait_for_log_upload(client, job_id: str) -> None: | |
| elapsed = 0 | |
| while elapsed <= LOG_UPLOAD_TIMEOUT: | |
| try: | |
| status = (qdc_api.get_job_log_upload_status(client, job_id) or "").lower() | |
| except Exception as e: | |
| log.warning("get_job_log_upload_status failed: %s — will retry", e) | |
| status = "" | |
| if status in {"completed", "failed"}: | |
| return | |
| log.info("Waiting for log upload (status=%s) ...", status) | |
| time.sleep(POLL_INTERVAL) | |
| elapsed += POLL_INTERVAL | |
| log.warning("Timed out waiting for log upload after %ds", LOG_UPLOAD_TIMEOUT) | |
| def wait_for_capacity(client, max_jobs: int = MAX_CONCURRENT_JOBS) -> None: | |
| """Block until the user's active (non-terminal) QDC job count is below max_jobs.""" | |
| elapsed = 0 | |
| while elapsed < CAPACITY_TIMEOUT: | |
| jobs_page = qdc_api.get_jobs_list(client, page_number=0, page_size=50) | |
| if jobs_page is None: | |
| log.warning( | |
| "Could not retrieve job list; proceeding without capacity check" | |
| ) | |
| return | |
| items = getattr(jobs_page, "data", []) or [] | |
| active = sum( | |
| 1 for j in items if getattr(j, "state", None) in _NON_TERMINAL_STATE_VALUES | |
| ) | |
| if active < max_jobs: | |
| log.info("Active QDC jobs: %d / %d — proceeding", active, max_jobs) | |
| return | |
| log.info( | |
| "Active QDC jobs: %d / %d — waiting %ds ...", | |
| active, | |
| max_jobs, | |
| CAPACITY_POLL, | |
| ) | |
| time.sleep(CAPACITY_POLL) | |
| elapsed += CAPACITY_POLL | |
| raise TimeoutError( | |
| f"Capacity wait timed out after {CAPACITY_TIMEOUT}s" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Log parsing helpers | |
| # --------------------------------------------------------------------------- | |
| def _parse_junit_xml(content: str) -> tuple[dict[str, bool], dict[str, str]]: | |
| try: | |
| root = ET.fromstring(content) | |
| except ET.ParseError: | |
| return {}, {} | |
| results: dict[str, bool] = {} | |
| failures: dict[str, str] = {} | |
| for tc in root.iter("testcase"): | |
| name = tc.get("name", "") | |
| if classname := tc.get("classname", ""): | |
| name = f"{classname}.{name}" | |
| failure_el = tc.find("failure") | |
| if failure_el is None: | |
| failure_el = tc.find("error") | |
| results[name] = failure_el is None | |
| if failure_el is not None: | |
| parts = [failure_el.get("message", ""), failure_el.text or ""] | |
| failures[name] = "\n".join(p for p in parts if p).strip() | |
| return results, failures | |
| def _parse_pytest_output(content: str) -> dict[str, bool]: | |
| results: dict[str, bool] = {} | |
| for m in _PYTEST_LINE_RE.finditer(content): | |
| results[m.group(1)] = m.group(2) == "PASSED" | |
| return results | |
| def fetch_logs_and_parse_tests( | |
| client, job_id: str, max_retries: int = 5, retry_delay: int = 30 | |
| ) -> tuple[dict[str, bool], dict[str, str], dict[str, str]]: | |
| """Returns (test_results, raw_logs, failure_details).""" | |
| log_files = None | |
| for attempt in range(1, max_retries + 1): | |
| try: | |
| log_files = qdc_api.get_job_log_files(client, job_id) | |
| break | |
| except Exception as e: | |
| if attempt < max_retries: | |
| log.warning( | |
| "get_job_log_files failed (attempt %d/%d): %s — retrying in %ds", | |
| attempt, max_retries, e, retry_delay, | |
| ) | |
| time.sleep(retry_delay) | |
| else: | |
| log.error( | |
| "get_job_log_files failed after %d attempts: %s", max_retries, e | |
| ) | |
| return {}, {}, {} | |
| if not log_files: | |
| log.warning("No log files returned for job %s", job_id) | |
| return {}, {}, {} | |
| test_results: dict[str, bool] = {} | |
| pytest_fallback: dict[str, bool] = {} | |
| raw_logs: dict[str, str] = {} | |
| failure_details: dict[str, str] = {} | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| for lf in log_files: | |
| zip_path = os.path.join(tmpdir, "log.zip") | |
| log.info("Downloading log file: %s", lf.filename) | |
| qdc_api.download_job_log_files(client, lf.filename, zip_path) | |
| try: | |
| shutil.unpack_archive(zip_path, tmpdir, "zip") | |
| except Exception as e: | |
| log.warning("Could not unpack %s as zip: %s", lf.filename, e) | |
| for root_dir, _, files in os.walk(tmpdir): | |
| for fname in sorted(files): | |
| fpath = os.path.join(root_dir, fname) | |
| content = Path(fpath).read_text(errors="replace") | |
| if fname.endswith(".xml"): | |
| results, failures = _parse_junit_xml(content) | |
| test_results.update(results) | |
| failure_details.update(failures) | |
| elif fname.endswith(".log"): | |
| if fname in _EXCLUDED_LOGS: | |
| continue | |
| log.info("--- %s ---\n%s", fname, content) | |
| raw_logs[fname] = content | |
| pytest_fallback.update(_parse_pytest_output(content)) | |
| return ( | |
| (test_results if test_results else pytest_fallback), | |
| raw_logs, | |
| failure_details, | |
| ) | |
| def write_summary(result: JobResult, title: str = "QDC Test Results") -> None: | |
| summary_path = os.environ.get("GITHUB_STEP_SUMMARY") | |
| if not summary_path: | |
| return | |
| icon = "✅" if result.passed else "❌" | |
| lines = [ | |
| f"## {title}\n", | |
| f"Overall: {icon} {'PASSED' if result.passed else 'FAILED'}\n", | |
| ] | |
| reportable = {n: ok for n, ok in result.tests.items() if "test_install" not in n} | |
| if reportable: | |
| lines += ["| Test | Result |", "| ---- | ------ |"] | |
| for name, ok in reportable.items(): | |
| lines.append(f"| `{name}` | {'✅' if ok else '❌'} |") | |
| passed_n = sum(1 for v in reportable.values() if v) | |
| failed_n = sum(1 for v in reportable.values() if not v) | |
| lines += ["", f"**{passed_n} passed, {failed_n} failed**"] | |
| else: | |
| lines.append("_No per-test data available._") | |
| failed_names = [n for n, ok in reportable.items() if not ok] | |
| if failed_names: | |
| lines += ["", "### Failures"] | |
| for name in failed_names: | |
| detail = result.failure_details.get(name) | |
| if detail: | |
| lines += [ | |
| f"<details><summary><code>{name}</code></summary>", | |
| "", | |
| "```", | |
| detail, | |
| "```", | |
| "", | |
| "</details>", | |
| ] | |
| if result.raw_logs: | |
| lines += ["", "### Raw Logs"] | |
| for fname, content in sorted(result.raw_logs.items()): | |
| lines += [ | |
| f"<details><summary>{fname}</summary>", | |
| "", | |
| "```", | |
| content.rstrip(), | |
| "```", | |
| "", | |
| "</details>", | |
| ] | |
| with open(summary_path, "a") as f: | |
| f.write("\n".join(lines) + "\n") | |
| # ============================================================================= | |
| # CLI + main | |
| # ============================================================================= | |
| def parse_args() -> argparse.Namespace: | |
| p = argparse.ArgumentParser( | |
| description=__doc__, | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| ) | |
| p.add_argument("--pkg-dir", required=True, type=Path, | |
| help="Installed llama.cpp package directory (contains bin/ and lib/)") | |
| p.add_argument("--model-url", | |
| help="Direct URL to the GGUF model file (required for --test bench)") | |
| p.add_argument("--device", required=True, | |
| help="QDC chipset name, e.g. SM8750") | |
| p.add_argument("--test", choices=["bench", "backend-ops", "all"], default="bench", | |
| help="Test suite to run (default: bench)") | |
| p.add_argument("--job-timeout", type=int, default=JOB_TIMEOUT, metavar="SECONDS", | |
| help=f"Max seconds to wait for job completion (default: {JOB_TIMEOUT})") | |
| p.add_argument("--retries", type=int, default=DEFAULT_RETRIES, metavar="N", | |
| help="Number of retries when device is unavailable (default: 0)") | |
| p.add_argument("--retry-delay", type=int, default=RETRY_DELAY, metavar="SECONDS", | |
| help=f"Seconds to wait between retries (default: {RETRY_DELAY})") | |
| args = p.parse_args() | |
| if args.test in ("bench", "all") and not args.model_url: | |
| p.error("--model-url is required when --test bench or --test all") | |
| return args | |
| def _submit_and_run_job(client, args, spec, target_id, artifact_id) -> JobResult: | |
| """Submit a QDC job and wait for results. | |
| Raises DeviceUnavailableError for transient device/resource issues that | |
| are worth retrying. Returns JobResult for definitive outcomes (pass or | |
| test failure). | |
| """ | |
| try: | |
| wait_for_capacity(client) | |
| except TimeoutError: | |
| raise DeviceUnavailableError("Capacity wait timed out — device busy") | |
| job_name = spec.job_name_fmt.format(base="llama.cpp Hexagon tests") | |
| job_id = qdc_api.submit_job( | |
| public_api_client=client, | |
| target_id=target_id, | |
| job_name=job_name, | |
| external_job_id=None, | |
| job_type=JobType.AUTOMATED, | |
| job_mode=JobMode.APPLICATION, | |
| timeout=max(1, args.job_timeout // 60), | |
| test_framework=spec.test_framework, | |
| entry_script=spec.entry_script, | |
| job_artifacts=[artifact_id], | |
| monkey_events=None, | |
| monkey_session_timeout=None, | |
| job_parameters=[JobSubmissionParameter.WIFIENABLED], | |
| ) | |
| if job_id is None: | |
| raise DeviceUnavailableError("Job submission failed — device may be unavailable") | |
| log.info("Job submitted: %s (device=%s)", job_id, args.device) | |
| try: | |
| job_status = wait_for_job(client, job_id, timeout=args.job_timeout) | |
| except TimeoutError as e: | |
| raise DeviceUnavailableError(str(e)) | |
| log.info("Job %s finished: %s", job_id, job_status) | |
| wait_for_log_upload(client, job_id) | |
| tests, raw_logs, failure_details = fetch_logs_and_parse_tests(client, job_id) | |
| job_ok = job_status == JobState.COMPLETED.value.lower() | |
| if not job_ok and not tests: | |
| raise DeviceUnavailableError( | |
| f"Job did not complete (status={job_status}) and produced no test results" | |
| ) | |
| passed = job_ok and all(tests.values()) if tests else job_ok | |
| if spec.test_framework == TestFramework.BASH and not tests: | |
| log.error("No test results recovered (state=%s). Script likely never ran.", job_status) | |
| passed = False | |
| if not passed: | |
| log.error("Job did not complete successfully or tests failed (status=%s)", job_status) | |
| return JobResult(passed=passed, tests=tests, raw_logs=raw_logs, failure_details=failure_details) | |
| def main() -> int: | |
| args = parse_args() | |
| platform = DEVICE_PLATFORM.get(args.device) | |
| if platform is None: | |
| log.error( | |
| "Unknown device %r. Known: %s", | |
| args.device, ", ".join(sorted(DEVICE_PLATFORM.keys())), | |
| ) | |
| return 1 | |
| spec = PLATFORM_SPECS[platform] | |
| api_key = os.environ.get("QDC_API_KEY") | |
| if not api_key: | |
| log.error("QDC_API_KEY environment variable must be set") | |
| return 1 | |
| if not args.pkg_dir.is_dir(): | |
| log.error("--pkg-dir %s does not exist", args.pkg_dir) | |
| return 1 | |
| client = qdc_api.get_public_api_client_using_api_key( | |
| api_key_header=api_key, | |
| app_name_header="llama-cpp-ci", | |
| on_behalf_of_header="llama-cpp-ci", | |
| client_type_header="Python", | |
| ) | |
| target_id = qdc_api.get_target_id(client, args.device) | |
| if target_id is None: | |
| log.error("Could not find QDC target for device %r", args.device) | |
| return 1 | |
| with tempfile.TemporaryDirectory() as tmpdir: | |
| log.info("Building %s artifact (test=%s) ...", platform.value, args.test) | |
| zip_path = spec.build_artifact( | |
| args.pkg_dir, Path(tmpdir), args.test, args.model_url | |
| ) | |
| log.info("Uploading artifact (%d MB) ...", zip_path.stat().st_size // 1_000_000) | |
| artifact_id = qdc_api.upload_file(client, str(zip_path), ArtifactType.TESTSCRIPT) | |
| if artifact_id is None: | |
| log.error("Artifact upload failed") | |
| return 1 | |
| max_attempts = 1 + args.retries | |
| for attempt in range(1, max_attempts + 1): | |
| try: | |
| result = _submit_and_run_job(client, args, spec, target_id, artifact_id) | |
| break | |
| except DeviceUnavailableError as e: | |
| if attempt < max_attempts: | |
| log.warning( | |
| "Attempt %d/%d failed (device unavailable): %s — retrying in %ds", | |
| attempt, max_attempts, e, args.retry_delay, | |
| ) | |
| time.sleep(args.retry_delay) | |
| else: | |
| log.error( | |
| "Attempt %d/%d failed (device unavailable): %s — no retries left", | |
| attempt, max_attempts, e, | |
| ) | |
| write_summary( | |
| JobResult(passed=False, tests={}), | |
| title=f"QDC Device Unavailable ({args.device})", | |
| ) | |
| return 1 | |
| else: | |
| return 1 | |
| if args.test == "backend-ops": | |
| title = f"Backend Ops — HTP0 ({args.device})" | |
| elif args.test == "all": | |
| title = f"QDC Tests ({args.device})" | |
| else: | |
| title = f"QDC Test Results ({args.device})" | |
| write_summary(result, title=title) | |
| return 0 if result.passed else 1 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |