diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index de99b2f..97df5be 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -14,12 +14,13 @@ "version": "0.1.0", "strict": false, "skills": [ + "./skills/amd-skill-finder", "./skills/local-ai-use", "./skills/local-ai-app-integration", "./skills/serving-llms-on-instinct", "./skills/tracelens-analysis-orchestrator" ], - "description": "AMD's verified Agent Skills in one plugin: route image/audio through local AI on Ryzen AI, serve LLMs on AMD Instinct GPUs with vLLM, and analyze GPU kernel and PyTorch trace performance." + "description": "AMD's verified Agent Skills in one plugin: discover AMD and ROCm capabilities, route image/audio through local AI on Ryzen AI, serve LLMs on AMD Instinct GPUs with vLLM, and analyze GPU kernel and PyTorch trace performance." } ] } diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index cd38ee1..5243916 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -12,12 +12,14 @@ "amd", "rocm", "hip", + "skill-discovery", "ryzen-ai", "vllm", "lemonade", "local-ai" ], "skills": [ + "./skills/amd-skill-finder", "./skills/local-ai-use", "./skills/local-ai-app-integration", "./skills/serving-llms-on-instinct", @@ -26,7 +28,7 @@ "interface": { "displayName": "AMD Skills", "shortDescription": "Agent Skills for AMD-optimized workflows.", - "longDescription": "AMD's verified Agent Skills in one plugin: route image/audio through local AI on Ryzen AI, serve LLMs on AMD Instinct GPUs with vLLM, and analyze GPU kernel and PyTorch trace performance.", + "longDescription": "AMD's verified Agent Skills in one plugin: discover AMD and ROCm capabilities, route image/audio through local AI on Ryzen AI, serve LLMs on AMD Instinct GPUs with vLLM, and analyze GPU kernel and PyTorch trace performance.", "developerName": "AMD", "category": "Developer Tools", "capabilities": [ diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index 62c7697..8749253 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -13,12 +13,13 @@ "source": "./", "version": "0.1.0", "skills": [ + "./skills/amd-skill-finder", "./skills/local-ai-use", "./skills/local-ai-app-integration", "./skills/serving-llms-on-instinct", "./skills/tracelens-analysis-orchestrator" ], - "description": "AMD's verified Agent Skills in one plugin: route image/audio through local AI on Ryzen AI, serve LLMs on AMD Instinct GPUs with vLLM, and analyze GPU kernel and PyTorch trace performance.", + "description": "AMD's verified Agent Skills in one plugin: discover AMD and ROCm capabilities, route image/audio through local AI on Ryzen AI, serve LLMs on AMD Instinct GPUs with vLLM, and analyze GPU kernel and PyTorch trace performance.", "author": { "name": "AMD" }, @@ -29,6 +30,7 @@ "amd", "rocm", "hip", + "skill-discovery", "ryzen-ai", "vllm", "lemonade", diff --git a/plugin-metadata.json b/plugin-metadata.json index d1f513c..acc420b 100644 --- a/plugin-metadata.json +++ b/plugin-metadata.json @@ -14,6 +14,7 @@ "amd", "rocm", "hip", + "skill-discovery", "ryzen-ai", "vllm", "lemonade", diff --git a/skills/amd-skill-finder/SKILL.md b/skills/amd-skill-finder/SKILL.md new file mode 100644 index 0000000..e0524ec --- /dev/null +++ b/skills/amd-skill-finder/SKILL.md @@ -0,0 +1,122 @@ +--- +name: amd-skill-finder +description: >- + Finds and recommends installed AMD skills, current AMD catalog skills, + official AMD and ROCm source repositories, and reviewed upstream projects. + Use whenever the user asks whether an AMD skill exists, wants to browse or + install AMD skills, or has an AMD/ROCm task that may benefit from a specialized + workflow. Trigger on AMD, ROCm, HIP, AMD Instinct, Ryzen AI, Radeon, MI300X, + MI325X, MI350X, MI355X, gfx942, gfx950, PyTorch/JAX/Triton on AMD, AMD + Quark, model quantization, PTQ, FP8, INT8, INT4, MXFP4, PyTorch or ONNX + quantization, vLLM, SGLang, AITER, ATOM, MORI, Hyperloom, LMCache, Mooncake, + NIXL, TileLang, TorchTitan, Miles, VERL, or VIME in an AMD context. Do not use + for generic non-AMD programming, deployment, optimization, AI, or + infrastructure tasks. +--- + +# AMD Skill Finder + +Discover the best available AMD skill before falling back to source material or +general advice. Keep installable skills distinct from repositories that contain +useful documentation, examples, code, or embedded agent instructions. + +## Prerequisites and safety + +- Require Python 3.10 or newer for `scripts/find_skills.py`. +- Use the bundled registry without network access by passing `--offline`. +- Require an authenticated `gh` CLI only for `--live` repository-code search. +- Remove credentials, private URLs, customer names, and proprietary code from + search text before sending it to GitHub. The script rejects common token and + private-key patterns but cannot identify every sensitive value. +- Never install a skill or change agent capabilities without explicit user + approval. Discovery and recommendation do not imply installation consent. + +## Discovery workflow + +1. Check whether a matching skill is already installed or present in the + current agent context. Hand off to that skill when it is available. +2. Run the finder against installed skills, the AMD catalog, and the curated + source registry: + + ```bash + python3 scripts/find_skills.py "serve a model with vLLM on MI355X" + ``` + +3. Add `--live` when current repository files are needed. This searches only + the highest-ranked allowlisted repositories by default: + + ```bash + python3 scripts/find_skills.py \ + "disaggregated KV-cache transfer over RDMA on MI355X" --live + ``` + +4. Use `--scope catalog` for installable skills only, `--scope amd` for AMD-owned + projects, or the default `--scope curated` for AMD plus reviewed upstream + projects. +5. Use `--repo OWNER/REPO --live` when the user explicitly names another + repository. Use `--general-github --live` only after curated search fails or + the user explicitly requests broad GitHub discovery. Label those results + unreviewed. +6. Select at most three recommendations. Prefer, in order: + installed skill, AMD catalog skill, AMD official source, reviewed upstream + source, then unreviewed GitHub fallback. + +Use `--json` when another tool or agent needs structured output. Run +`python3 scripts/find_skills.py --help` for the complete CLI. + +## Interpret result types + +- `installed_skill`: use it directly; do not recommend reinstalling it. +- `installable_skill`: a published `amd/skills` catalog entry. It may be + recommended for installation after user approval. +- `source_project`: a curated logical project and its repositories. It is not + installable. +- `embedded_skill`: a `SKILL.md` found inside a product repository. Treat it as + product-owned agent guidance until it is reviewed and packaged for the AMD + catalog. +- `guide`, `code_example`, or `source_code`: use as evidence or implementation + context, not as a skill package. +- `unreviewed_repository`: inspect ownership, license, activity, security, and + AMD relevance before recommending or porting anything. + +For paired projects such as PyTorch, JAX, and Triton, prefer the ROCm fork for +AMD implementation details and the upstream repository for public API semantics. +Present them as one project rather than duplicate recommendations. + +## Recommendation format + +For each recommendation, provide: + +1. Name and result type. +2. Why it matches the user's concrete task. +3. Provenance: installed, AMD catalog, AMD official, reviewed upstream, or + unreviewed. +4. The next useful action or prompt. +5. For an uninstalled catalog skill only, the proposed install command. + +Use this pattern for a strong catalog match: + +```text +`` is an AMD catalog skill that matches because . +Would you like me to install it and then use it for ? +``` + +Do not run the proposed command until the user says yes: + +```bash +npx skills add amd/skills --skill --global --yes +``` + +If the target agent is known, add its supported `--agent` value. If installation +is unavailable, provide the catalog URL and continue with source-backed help. + +## When no catalog skill matches + +Say that no strong installable AMD skill was found. Then use the top curated +source projects to help with the task, or propose creating/porting a skill when +the workflow is repeated, well-bounded, and testable. Never invent a catalog +slug from a repository name. + +Read [references/routing-and-source-policy.md](references/routing-and-source-policy.md) +when the query is ambiguous, spans multiple domains, needs a source added, or +requires deciding whether repository material is ready to become a skill. diff --git a/skills/amd-skill-finder/agents/openai.yaml b/skills/amd-skill-finder/agents/openai.yaml new file mode 100644 index 0000000..07a37e1 --- /dev/null +++ b/skills/amd-skill-finder/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "AMD Skill Finder" + short_description: "Find trusted AMD and ROCm skills and sources." + default_prompt: "Use $amd-skill-finder to find the best AMD skill or ROCm source for my task." diff --git a/skills/amd-skill-finder/data/catalog-snapshot.json b/skills/amd-skill-finder/data/catalog-snapshot.json new file mode 100644 index 0000000..7de2aad --- /dev/null +++ b/skills/amd-skill-finder/data/catalog-snapshot.json @@ -0,0 +1,26 @@ +{ + "schema_version": 1, + "repository": "amd/skills", + "skills": [ + { + "name": "amd-skill-finder", + "description": "Finds and recommends installed AMD skills, current AMD catalog skills, official AMD and ROCm repositories, and reviewed upstream projects for AMD GPU work." + }, + { + "name": "local-ai-app-integration", + "description": "Converts cloud-LLM applications to use local inference through Lemonade Server on AMD Ryzen AI systems." + }, + { + "name": "local-ai-use", + "description": "Uses local AI models through Lemonade Server for chat, image generation, transcription, embeddings, and reranking on AMD systems." + }, + { + "name": "serving-llms-on-instinct", + "description": "Serves AI models on AMD Instinct GPUs using vLLM, including environment validation, model configuration, launch, and health verification." + }, + { + "name": "tracelens-analysis-orchestrator", + "description": "Analyzes PyTorch and GPU traces, identifies performance bottlenecks, and produces AMD-focused optimization recommendations." + } + ] +} diff --git a/skills/amd-skill-finder/data/sources.json b/skills/amd-skill-finder/data/sources.json new file mode 100644 index 0000000..cd19911 --- /dev/null +++ b/skills/amd-skill-finder/data/sources.json @@ -0,0 +1,287 @@ +{ + "schema_version": 1, + "excluded_repositories": [ + "ai-dynamo/dynamo" + ], + "amd_signals": [ + "amd", + "amd quark", + "quark", + "rocm", + "hip", + "instinct", + "ryzen ai", + "mi100", + "mi200", + "mi210", + "mi250", + "mi250x", + "mi300", + "mi300a", + "mi300x", + "mi325x", + "mi350x", + "mi355x", + "gfx90a", + "gfx942", + "gfx950", + "radeon" + ], + "projects": [ + { + "id": "rocm-platform", + "display_name": "ROCm platform and documentation", + "description": "ROCm installation, compatibility, system components, release information, and platform documentation.", + "categories": ["system-platform", "gpu-development"], + "keywords": ["install rocm", "rocm compatibility", "rocm release", "driver", "runtime", "linux", "system management", "rocm docs"], + "repositories": [ + {"repo": "ROCm/ROCm", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["README.md", "docs"]}, + {"repo": "ROCm/rocm-docs-core", "role": "documentation", "tier": "amd-official", "priority": 98, "path_hints": ["docs"]}, + {"repo": "ROCm/rocm-systems", "role": "systems", "tier": "amd-official", "priority": 94, "path_hints": ["projects", "docs"]} + ] + }, + { + "id": "hip", + "display_name": "HIP", + "description": "AMD GPU programming, CUDA-to-HIP porting, HIP runtime APIs, compilation, and debugging.", + "categories": ["gpu-development", "kernel-compilers"], + "keywords": ["hip", "hipify", "cuda port", "cuda to hip", "hip runtime", "hip kernel", "hipcc", "gpu programming"], + "repositories": [ + {"repo": "ROCm/hip", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["docs", "include", "samples"]} + ] + }, + { + "id": "rccl", + "display_name": "RCCL", + "description": "Collective communication and multi-GPU or multi-node training on AMD GPUs.", + "categories": ["networking", "training", "system-platform"], + "keywords": ["rccl", "collective", "allreduce", "all gather", "multi gpu", "multi node", "nccl", "distributed training"], + "repositories": [ + {"repo": "ROCm/rccl", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["docs", "tools", "test"]} + ] + }, + { + "id": "rocprofiler-sdk", + "display_name": "ROCprofiler SDK", + "description": "ROCm profiling, tracing, performance counters, and workload analysis.", + "categories": ["profiling", "gpu-development"], + "keywords": ["rocprofiler", "rocprof", "profile", "profiling", "trace", "performance counter", "roofline", "bottleneck"], + "repositories": [ + {"repo": "ROCm/rocprofiler-sdk", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["source/docs", "samples", "tests"]} + ] + }, + { + "id": "aiter", + "display_name": "AITER", + "description": "AMD inference operators and optimized kernels for transformer workloads.", + "categories": ["inference", "kernel-compilers"], + "keywords": ["aiter", "attention", "mla", "moe", "gemm", "operator", "optimized kernel", "transformer kernel", "inference kernel"], + "repositories": [ + {"repo": "ROCm/aiter", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["aiter", "op_tests", "docs", "examples"]} + ] + }, + { + "id": "atom", + "display_name": "ATOM", + "description": "Composable AMD inference kernels and integrations for high-performance model serving.", + "categories": ["inference", "kernel-compilers"], + "keywords": ["atom", "attention", "moe", "gemm", "kernel", "inference optimization", "composable kernel"], + "repositories": [ + {"repo": "ROCm/ATOM", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["aiter", "csrc", "examples", "docs"]} + ] + }, + { + "id": "quark", + "display_name": "AMD Quark", + "description": "Model quantization, calibration, export, and low-precision deployment on AMD hardware.", + "categories": ["quantization", "inference", "training"], + "keywords": ["quark", "quantization", "quantize", "fp8", "mxfp4", "int8", "int4", "calibration", "onnx quantization", "pytorch quantization"], + "repositories": [ + {"repo": "amd/Quark", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["docs", "examples", "quark"]} + ] + }, + { + "id": "magpie", + "display_name": "Magpie", + "description": "AMD GPU workload trace collection and kernel performance evaluation.", + "categories": ["profiling", "gpu-development"], + "keywords": ["magpie", "kernel evaluator", "benchmark kernel", "collect trace", "gpu trace", "performance measurement"], + "repositories": [ + {"repo": "AMD-AGI/Magpie", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["skills", "docs", "src"]} + ] + }, + { + "id": "tracelens", + "display_name": "TraceLens", + "description": "Agent-assisted AMD GPU trace analysis, bottleneck identification, and optimization planning.", + "categories": ["profiling", "gpu-development"], + "keywords": ["tracelens", "analyze trace", "trace analysis", "bottleneck", "roofline", "kernel analysis", "performance trace"], + "repositories": [ + {"repo": "AMD-AGI/TraceLens", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["TraceLens/Agent", "docs"]} + ] + }, + { + "id": "mori", + "display_name": "MORI", + "description": "Modular RDMA, expert-parallel communication, shared memory, collectives, and KV-cache transfer on AMD GPUs.", + "categories": ["networking", "kv-cache", "inference"], + "keywords": ["mori", "rdma", "gpu direct", "gpudirect", "ibgda", "expert parallel", "dispatch combine", "mori ep", "mori io", "mori shmem", "kv cache transfer", "disaggregated serving"], + "repositories": [ + {"repo": "ROCm/mori", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["docs", "examples", "python", "include"]} + ] + }, + { + "id": "hyperloom", + "display_name": "ROCm Hyperloom", + "description": "Agentic end-to-end inference and GPU-kernel optimization on AMD GPUs.", + "categories": ["profiling", "inference", "kernel-compilers"], + "keywords": ["hyperloom", "auto optimize", "agentic optimization", "optimize workload", "optimize kernel", "performance bottleneck", "inference optimization", "benchmark optimization"], + "repositories": [ + {"repo": "AMD-AGI/Hyperloom", "role": "canonical", "tier": "amd-official", "priority": 100, "path_hints": ["src/hyperloom/inference_optimizer/SKILL.md", "docs", "examples", "src"]} + ] + }, + { + "id": "pytorch", + "display_name": "PyTorch", + "description": "PyTorch development, compilation, distributed training, and inference on ROCm.", + "categories": ["training", "inference", "kernel-compilers"], + "keywords": ["pytorch", "torch", "torch compile", "torch distributed", "inductor", "fsdp", "dtensor", "torchrun", "pytorch rocm"], + "repositories": [ + {"repo": "ROCm/pytorch", "role": "amd-fork", "tier": "amd-official", "priority": 100, "path_hints": ["docs", "torch", ".ci"]}, + {"repo": "pytorch/pytorch", "role": "upstream", "tier": "reviewed-upstream", "priority": 82, "path_hints": ["docs", "torch", ".ci"]} + ] + }, + { + "id": "jax", + "display_name": "JAX", + "description": "JAX, XLA compilation, sharding, and distributed workloads on ROCm.", + "categories": ["training", "kernel-compilers"], + "keywords": ["jax", "xla", "pjit", "sharding", "jax rocm", "jax distributed", "jax gpu"], + "repositories": [ + {"repo": "ROCm/jax", "role": "amd-fork", "tier": "amd-official", "priority": 100, "path_hints": ["docs", "jax", "tests"]}, + {"repo": "jax-ml/jax", "role": "upstream", "tier": "reviewed-upstream", "priority": 82, "path_hints": ["docs", "jax", "tests"]} + ] + }, + { + "id": "triton", + "display_name": "Triton", + "description": "Triton language, compiler, kernels, and performance work on AMD GPUs.", + "categories": ["kernel-compilers", "gpu-development"], + "keywords": ["triton", "triton language", "triton kernel", "triton compiler", "tl dot", "autotune", "kernel compiler", "triton rocm"], + "repositories": [ + {"repo": "ROCm/triton", "role": "amd-fork", "tier": "amd-official", "priority": 100, "path_hints": ["docs", "python/tutorials", "python/test", "third_party/amd"]}, + {"repo": "triton-lang/triton", "role": "upstream", "tier": "reviewed-upstream", "priority": 82, "path_hints": ["docs", "python/tutorials", "python/test"]} + ] + }, + { + "id": "vllm", + "display_name": "vLLM", + "description": "High-throughput LLM inference and serving, including AMD ROCm integrations.", + "categories": ["inference"], + "keywords": ["vllm", "serve llm", "llm server", "openai server", "tensor parallel", "expert parallel", "paged attention", "prefix cache", "speculative decoding"], + "repositories": [ + {"repo": "vllm-project/vllm", "role": "upstream", "tier": "reviewed-upstream", "priority": 90, "path_hints": ["docs", "examples", "vllm", "tests"]} + ] + }, + { + "id": "vllm-recipes", + "display_name": "vLLM recipes", + "description": "Known-good model serving recipes, feature flags, and AMD hardware overrides for vLLM.", + "categories": ["inference"], + "keywords": ["vllm recipe", "model recipe", "serve kimi", "serve qwen", "serve deepseek", "tool calling", "reasoning parser", "hardware override"], + "repositories": [ + {"repo": "vllm-project/recipes", "role": "recipes", "tier": "reviewed-upstream", "priority": 92, "path_hints": ["models", "docs"]} + ] + }, + { + "id": "sglang", + "display_name": "SGLang", + "description": "High-performance LLM serving, distributed inference, and AMD ROCm integrations.", + "categories": ["inference"], + "keywords": ["sglang", "sgl kernel", "sglang server", "radix cache", "disaggregated serving", "expert parallel", "prefill decode", "pd disaggregation"], + "repositories": [ + {"repo": "sgl-project/sglang", "role": "upstream", "tier": "reviewed-upstream", "priority": 90, "path_hints": ["docs", "examples", "python", "sgl-kernel", "test"]} + ] + }, + { + "id": "torchtitan", + "display_name": "TorchTitan", + "description": "PyTorch-native large-model training and distributed training recipes.", + "categories": ["training"], + "keywords": ["torchtitan", "train llm", "large model training", "fsdp", "tensor parallel", "pipeline parallel", "distributed checkpoint", "float8 training"], + "repositories": [ + {"repo": "pytorch/torchtitan", "role": "upstream", "tier": "reviewed-upstream", "priority": 90, "path_hints": ["docs", "torchtitan", "train_configs", ".ci"]} + ] + }, + { + "id": "lmcache", + "display_name": "LMCache", + "description": "KV-cache reuse, offload, transfer, and disaggregated LLM serving.", + "categories": ["kv-cache", "inference"], + "keywords": ["lmcache", "kv cache", "kv cache reuse", "cache offload", "cache transfer", "disaggregated serving", "prefill decode", "remote cache"], + "repositories": [ + {"repo": "LMCache/LMCache", "role": "upstream", "tier": "reviewed-upstream", "priority": 90, "path_hints": ["docs", "examples", "lmcache"]} + ] + }, + { + "id": "mooncake", + "display_name": "Mooncake", + "description": "Distributed KV-cache transfer, storage, and prefill/decode disaggregation.", + "categories": ["kv-cache", "networking", "inference"], + "keywords": ["mooncake", "kv cache", "transfer engine", "rdma", "prefill decode", "disaggregated serving", "distributed cache", "gpu direct"], + "repositories": [ + {"repo": "kvcache-ai/Mooncake", "role": "upstream", "tier": "reviewed-upstream", "priority": 90, "path_hints": ["docs", "examples", "mooncake-transfer-engine", "scripts"]} + ] + }, + { + "id": "tilelang", + "display_name": "TileLang", + "description": "A tile-oriented language for authoring and tuning high-performance accelerator kernels.", + "categories": ["kernel-compilers", "gpu-development"], + "keywords": ["tilelang", "tile language", "kernel dsl", "gpu kernel", "kernel optimization", "tensor core", "matrix core", "autotune"], + "repositories": [ + {"repo": "tile-ai/tilelang", "role": "upstream", "tier": "reviewed-upstream", "priority": 88, "path_hints": ["docs", "examples", "tilelang"]} + ] + }, + { + "id": "nixl", + "display_name": "NIXL", + "description": "Cross-vendor inference data transfer and memory movement, including ROCm and UCX paths.", + "categories": ["networking", "kv-cache", "inference"], + "keywords": ["nixl", "data transfer", "memory transfer", "kv transfer", "ucx", "rdma", "gpu direct", "prefill decode", "disaggregated serving"], + "repositories": [ + {"repo": "ai-dynamo/nixl", "role": "upstream", "tier": "reviewed-upstream", "priority": 88, "path_hints": ["docs", "examples", "src", "contrib"]} + ] + }, + { + "id": "miles", + "display_name": "Miles", + "description": "Reinforcement-learning post-training for large language and vision-language models.", + "categories": ["post-training", "training"], + "keywords": ["miles", "reinforcement learning", "rl post training", "post training", "reward model", "rollout", "grpo", "ppo", "rlhf"], + "repositories": [ + {"repo": "radixark/miles", "role": "upstream", "tier": "reviewed-upstream", "priority": 88, "path_hints": ["docs", "examples", "miles"]} + ] + }, + { + "id": "verl", + "display_name": "VERL", + "description": "Flexible reinforcement-learning post-training with hybrid distributed execution.", + "categories": ["post-training", "training"], + "keywords": ["verl", "hybridflow", "reinforcement learning", "rl post training", "post training", "rollout", "grpo", "ppo", "rlhf"], + "repositories": [ + {"repo": "verl-project/verl", "role": "upstream", "tier": "reviewed-upstream", "priority": 90, "path_hints": ["docs", "examples", "verl"]} + ] + }, + { + "id": "vime", + "display_name": "VIME", + "description": "vLLM-based reinforcement-learning post-training and scalable rollout generation.", + "categories": ["post-training", "training", "inference"], + "keywords": ["vime", "vllm post training", "reinforcement learning", "rl post training", "post training", "rl scaling", "rollout backend", "megatron", "grpo", "ppo", "slime"], + "repositories": [ + {"repo": "vllm-project/vime", "role": "upstream", "tier": "reviewed-upstream", "priority": 90, "path_hints": ["docs", "examples", "vime"]} + ] + } + ] +} diff --git a/skills/amd-skill-finder/evals/evals.py b/skills/amd-skill-finder/evals/evals.py new file mode 100644 index 0000000..5a8e017 --- /dev/null +++ b/skills/amd-skill-finder/evals/evals.py @@ -0,0 +1,43 @@ +# Copyright (c) 2026 Advanced Micro Devices, Inc. All rights reserved. +# +# See LICENSE for license information. + +"""Behavioral tests for the `amd-skill-finder` skill.""" + +from harness import claude + + +def test_catalog_skill_precedes_general_guidance(): + with claude("opus", skill="amd-skill-finder") as agent: + run = agent.prompt( + "I have an MI355X and want to serve a Qwen model with vLLM. " + "Find an AMD skill that can help." + ) + + run.logs_contains("amd-skill-finder") + run.should( + "Check installed skills or the AMD skills catalog before relying on general product advice" + ) + run.should( + "Recommend serving-llms-on-instinct when it is present in the catalog" + ) + run.should("Explain why the skill matches the MI355X vLLM serving task") + run.should_not("Install any skill without first asking for approval") + + +def test_source_projects_are_not_presented_as_skills(): + with claude("opus", skill="amd-skill-finder") as agent: + run = agent.prompt( + "I need disaggregated KV-cache transfer over RDMA on MI355X. " + "Is there an AMD skill or useful source project for this?" + ) + + run.logs_contains("amd-skill-finder") + run.should( + "Use the finder script to check the catalog and curated source registry" + ) + run.should( + "Identify relevant projects such as MORI, Mooncake, NIXL, or LMCache" + ) + run.should("Distinguish source projects from installable AMD catalog skills") + run.should_not("Invent an install command for MORI, Mooncake, NIXL, or LMCache") diff --git a/skills/amd-skill-finder/references/routing-and-source-policy.md b/skills/amd-skill-finder/references/routing-and-source-policy.md new file mode 100644 index 0000000..f15355a --- /dev/null +++ b/skills/amd-skill-finder/references/routing-and-source-policy.md @@ -0,0 +1,135 @@ +# AMD Skill Finder Routing and Source Policy + +Use this reference for ambiguous discovery, source-registry maintenance, or +deciding whether source material is ready to become an installable skill. + +## Contents + +- [Search tiers](#search-tiers) +- [Stable routing lanes](#stable-routing-lanes) +- [Matching rules](#matching-rules) +- [Paired upstream and AMD repositories](#paired-upstream-and-amd-repositories) +- [Result provenance](#result-provenance) +- [Promoting source material into a skill](#promoting-source-material-into-a-skill) +- [Registry maintenance](#registry-maintenance) + +## Search tiers + +Search in this order: + +0. Skills installed in the current agent environment. +1. Published skills in the live `amd/skills` catalog. +2. Official repositories owned by AMD, ROCm, or AMD-AGI. +3. Reviewed upstream repositories in `data/sources.json`. +4. General GitHub results only when explicitly enabled. + +Higher trust does not erase relevance: an exact upstream API answer can be more +useful than an unrelated AMD document. When two sources answer the same AMD +question equally well, rank the AMD source first. + +## Stable routing lanes + +- System platform: ROCm installation, compatibility, releases, runtime, driver, + HIP, and system-management questions. +- GPU development: HIP porting, Triton and TileLang kernels, AITER, ATOM, + compilation, profiling, tracing, and debugging. +- Training: PyTorch, JAX, TorchTitan, RCCL, FSDP, tensor/pipeline parallelism, + distributed checkpointing, and low-precision training. +- Inference: vLLM, vLLM recipes, SGLang, model serving, attention, MoE, prefix + caching, speculative decoding, and quantization with Quark. +- KV-cache and networking: MORI, LMCache, Mooncake, NIXL, RDMA, UCX, + prefill/decode disaggregation, expert parallelism, and cache transfer. +- Post-training: Miles, VERL, VIME, RLHF, GRPO, PPO, reward models, rollout + generation, and vLLM-backed RL scaling. +- Performance optimization: Hyperloom, TraceLens, Magpie, ROCprofiler SDK, + kernel bottlenecks, roofline analysis, and validated optimization campaigns. +- Local AI: Lemonade Server, Ryzen AI, local chat, image generation, + transcription, embeddings, reranking, and cloud-to-local application ports. + +Route by the user's task verb and artifact, not merely a product token. For +example, "profile a Triton kernel" should include the profiling lane as well as +the Triton project. + +## Matching rules + +For very large repositories such as PyTorch, JAX, and Triton, require an AMD +signal for implicit broad searches: AMD, ROCm, HIP, an AMD product, an Instinct +model, or a `gfx` architecture. Relax that requirement when the user explicitly +names the project and asks an upstream API question. + +Treat ordinary uses of these words as negative signals unless AMD or accelerated +computing is also present: + +- `route`: HTTP or file routing. +- `optimize`: CSS, SQL, bundle size, or generic refactoring. +- `deploy`: generic web hosting, Kubernetes, or CI/CD. +- `AI`, `data`, `training`, or `infrastructure`: generic uses without an AMD, + GPU, ROCm, or distinctive project signal. + +Do not send a full user prompt to an external search service when a short, +non-sensitive capability phrase is sufficient. + +## Paired upstream and AMD repositories + +Represent a fork pair with one project identifier and ordered repositories: + +```json +{ + "id": "triton", + "repositories": [ + {"repo": "ROCm/triton", "role": "amd-fork", "tier": "amd-official"}, + {"repo": "triton-lang/triton", "role": "upstream", "tier": "reviewed-upstream"} + ] +} +``` + +Use AMD forks for ROCm patches, CI, architecture support, and AMD-specific +performance behavior. Use upstream for public interfaces and cross-platform +design. Cite the exact repository and file used; do not silently blend them. + +## Result provenance + +Only a validated catalog folder is `installable_skill`. A source repository can +contain a `SKILL.md` without being installable from `amd/skills`; label that +`embedded_skill`. Hyperloom currently provides a useful example of embedded +product-owned agent guidance. + +Issues and pull requests are current but less stable than versioned docs or +code. Label them discussions and verify whether the change merged before using +them as instructions. Prefer commit-pinned live-search URLs when available. + +The repository `ai-dynamo/dynamo` is explicitly excluded. The separately +requested `ai-dynamo/nixl` repository remains a reviewed NIXL source; exclusion +is repository-specific, not organization-wide. + +## Promoting source material into a skill + +Recommend creating or porting a skill only when the workflow has: + +1. A single clear outcome. +2. Predictable inputs and outputs. +3. Repeated procedural value beyond ordinary documentation lookup. +4. Safe, deterministic scripts for fragile operations. +5. Maintainer ownership and an acceptable redistribution license. +6. AMD-specific validation, including supported hardware and ROCm versions. +7. Positive and negative routing tests plus an end-to-end behavioral test. + +Review embedded instructions for assumptions about repository layout, internal +tools, credentials, model-specific paths, or destructive actions before porting. +Do not copy third-party text or code unless its license permits redistribution. + +## Registry maintenance + +Edit `data/sources.json`, not `.github/scripts/sources.yml`, to add discovery +sources. The latter is reserved for repositories that actually federate +installable skill folders into the AMD catalog. + +For every registry change: + +1. Verify the canonical GitHub owner and repository. +2. Record whether the source is AMD official or reviewed upstream. +3. Group forks under one logical project. +4. Add task-oriented keywords and narrow path hints. +5. Confirm excluded repositories cannot appear. +6. Add or update deterministic routing tests. +7. Run the finder offline and live on representative queries. diff --git a/skills/amd-skill-finder/scripts/find_skills.py b/skills/amd-skill-finder/scripts/find_skills.py new file mode 100755 index 0000000..11a0850 --- /dev/null +++ b/skills/amd-skill-finder/scripts/find_skills.py @@ -0,0 +1,845 @@ +#!/usr/bin/env python3 +"""Find AMD skills and route AMD/ROCm questions to curated source projects. + +The default path is read-only and deterministic: inspect installed skills, read +the current AMD catalog when available, and rank a bundled curated registry. +Pass --live to search code in the top routed GitHub repositories. Arbitrary +GitHub repository search is disabled unless --general-github is also supplied. +""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import shutil +import subprocess +import sys +import urllib.error +import urllib.request +from collections.abc import Iterable +from pathlib import Path +from typing import Any + +SKILL_DIR = Path(__file__).resolve().parent.parent +DATA_DIR = SKILL_DIR / "data" +REGISTRY_PATH = DATA_DIR / "sources.json" +SNAPSHOT_PATH = DATA_DIR / "catalog-snapshot.json" +CATALOG_REPO = "amd/skills" +SELF_SKILL_NAME = "amd-skill-finder" +CATALOG_MANIFEST_URL = ( + "https://raw.githubusercontent.com/amd/skills/main/.claude-plugin/marketplace.json" +) +RAW_SKILL_URL = ( + "https://raw.githubusercontent.com/amd/skills/main/skills/{name}/SKILL.md" +) +SECRET_PATTERNS = ( + re.compile(r"\bghp_[A-Za-z0-9]{20,}\b"), + re.compile(r"\bgithub_pat_[A-Za-z0-9_]{20,}\b"), + re.compile(r"\bAKIA[0-9A-Z]{16}\b"), + re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"), + re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=-]{20,}", re.IGNORECASE), +) +STOP_WORDS = { + "a", + "an", + "and", + "are", + "as", + "at", + "be", + "can", + "do", + "for", + "from", + "help", + "how", + "i", + "in", + "is", + "it", + "me", + "my", + "of", + "on", + "or", + "please", + "skill", + "skills", + "the", + "this", + "to", + "use", + "using", + "want", + "with", +} +TIER_LABELS = { + "amd-official": "AMD official", + "reviewed-upstream": "reviewed upstream", + "user-specified": "user-specified", +} + + +class FinderError(RuntimeError): + """Expected user-facing finder failure.""" + + +def _read_json(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as handle: + data = json.load(handle) + if not isinstance(data, dict): + raise FinderError(f"Expected a JSON object in {path}") + return data + + +def load_registry(path: Path = REGISTRY_PATH) -> dict[str, Any]: + registry = _read_json(path) + if registry.get("schema_version") != 1: + raise FinderError(f"Unsupported registry schema in {path}") + if not isinstance(registry.get("projects"), list): + raise FinderError(f"Registry {path} is missing a projects array") + return registry + + +def normalize(text: str) -> str: + return " ".join(re.findall(r"[a-z0-9+#.]+", text.lower())) + + +def _stem(token: str) -> str: + if len(token) > 5 and token.endswith("ing"): + return token[:-3] + if len(token) > 4 and token.endswith("es"): + return token[:-2] + if len(token) > 3 and token.endswith("s"): + return token[:-1] + return token + + +def tokens(text: str) -> set[str]: + return { + _stem(token) + for token in normalize(text).split() + if len(token) >= 2 and token not in STOP_WORDS + } + + +def phrase_present(phrase: str, normalized_query: str) -> bool: + phrase = normalize(phrase) + return bool(phrase) and f" {phrase} " in f" {normalized_query} " + + +def reject_secrets(query: str) -> None: + if any(pattern.search(query) for pattern in SECRET_PATTERNS): + raise FinderError( + "The query appears to contain a credential or private key. Remove " + "secrets before sending search terms to a catalog or GitHub." + ) + + +def _parse_frontmatter(text: str) -> dict[str, str]: + match = re.match(r"\A---\r?\n(.*?)\r?\n---", text, re.DOTALL) + if not match: + return {} + lines = match.group(1).splitlines() + values: dict[str, str] = {} + index = 0 + while index < len(lines): + line = lines[index] + field = re.match(r"^(name|description):\s*(.*)$", line) + if not field: + index += 1 + continue + key, raw = field.groups() + if raw in {">", ">-", "|", "|-"}: + folded: list[str] = [] + index += 1 + while index < len(lines) and ( + not lines[index].strip() or lines[index].startswith((" ", "\t")) + ): + if lines[index].strip(): + folded.append(lines[index].strip()) + index += 1 + values[key] = " ".join(folded) + continue + values[key] = raw.strip().strip("'\"") + index += 1 + return values + + +def _catalog_from_directory( + skills_dir: Path, names: Iterable[str] | None = None +) -> list[dict[str, str]]: + if not skills_dir.is_dir(): + return [] + selected = set(names) if names is not None else None + entries: list[dict[str, str]] = [] + for skill_md in sorted(skills_dir.glob("*/SKILL.md")): + name = skill_md.parent.name + if selected is not None and name not in selected: + continue + metadata = _parse_frontmatter(skill_md.read_text(encoding="utf-8")) + if metadata.get("name") and metadata.get("description"): + entries.append( + { + "name": metadata["name"], + "description": metadata["description"], + "origin": "local catalog checkout", + "url": f"https://github.com/amd/skills/tree/main/skills/{name}", + } + ) + return entries + + +def _repo_root() -> Path | None: + candidate = SKILL_DIR.parents[1] + if (candidate / ".claude-plugin" / "marketplace.json").is_file(): + return candidate + return None + + +def local_catalog() -> list[dict[str, str]]: + root = _repo_root() + if root is None: + return [] + manifest_path = root / ".claude-plugin" / "marketplace.json" + try: + manifest = _read_json(manifest_path) + plugins = manifest.get("plugins", []) + paths = plugins[0].get("skills", []) if plugins else [] + names = [str(path).rstrip("/").split("/")[-1] for path in paths] + except (OSError, ValueError, IndexError, AttributeError): + names = None + return _catalog_from_directory(root / "skills", names) + + +def _fetch_text(url: str, timeout: int = 8) -> str: + request = urllib.request.Request( + url, + headers={"User-Agent": "amd-skill-finder/1.0", "Accept": "text/plain"}, + ) + with urllib.request.urlopen(request, timeout=timeout) as response: + return response.read().decode("utf-8") + + +def remote_catalog() -> list[dict[str, str]]: + manifest = json.loads(_fetch_text(CATALOG_MANIFEST_URL)) + plugins = manifest.get("plugins", []) + if not plugins: + raise FinderError("The AMD catalog manifest has no plugin entries") + names = [ + str(path).rstrip("/").split("/")[-1] for path in plugins[0].get("skills", []) + ] + entries: list[dict[str, str]] = [] + for name in names: + metadata = _parse_frontmatter(_fetch_text(RAW_SKILL_URL.format(name=name))) + if metadata.get("name") and metadata.get("description"): + entries.append( + { + "name": metadata["name"], + "description": metadata["description"], + "origin": "live AMD catalog", + "url": f"https://github.com/amd/skills/tree/main/skills/{name}", + } + ) + return entries + + +def snapshot_catalog() -> list[dict[str, str]]: + snapshot = _read_json(SNAPSHOT_PATH) + return [ + { + "name": str(entry["name"]), + "description": str(entry["description"]), + "origin": "bundled catalog snapshot", + "url": f"https://github.com/amd/skills/tree/main/skills/{entry['name']}", + } + for entry in snapshot.get("skills", []) + ] + + +def installed_roots(extra_roots: Iterable[str] = ()) -> list[Path]: + roots = [ + Path.cwd() / ".claude" / "skills", + Path.cwd() / ".agents" / "skills", + Path.home() / ".claude" / "skills", + Path.home() / ".codex" / "skills", + ] + codex_home = os.environ.get("CODEX_HOME") + if codex_home: + roots.append(Path(codex_home) / "skills") + roots.extend(Path(value).expanduser() for value in extra_roots) + unique: list[Path] = [] + seen: set[Path] = set() + for root in roots: + resolved = root.resolve() + if resolved not in seen: + seen.add(resolved) + unique.append(resolved) + return unique + + +def installed_skills(extra_roots: Iterable[str] = ()) -> list[dict[str, str]]: + entries: dict[str, dict[str, str]] = {} + for root in installed_roots(extra_roots): + for entry in _catalog_from_directory(root): + entry["origin"] = f"installed at {root}" + entry["url"] = str(root / entry["name"]) + entries.setdefault(entry["name"], entry) + return sorted(entries.values(), key=lambda entry: entry["name"]) + + +def has_amd_signal(query: str, registry: dict[str, Any]) -> bool: + normalized_query = normalize(query) + return any( + phrase_present(str(signal), normalized_query) + for signal in registry.get("amd_signals", []) + ) + + +def score_catalog_entry(query: str, entry: dict[str, str]) -> tuple[int, list[str]]: + normalized_query = normalize(query) + query_tokens = tokens(query) + name = entry["name"] + document = f"{name} {entry['description']}" + document_tokens = tokens(document) + score = 0 + reasons: list[str] = [] + if phrase_present(name, normalized_query): + score += 35 + reasons.append(f"explicit skill name `{name}`") + browse_phrases = ( + "browse amd skills", + "list amd skills", + "show amd skills", + "what amd skills", + "which amd skills", + ) + if any(phrase_present(phrase, normalized_query) for phrase in browse_phrases): + score += 10 + reasons.append("AMD catalog browse request") + overlap = sorted(query_tokens & document_tokens) + if overlap: + score += min(24, 4 * len(overlap)) + reasons.append("matched " + ", ".join(overlap[:5])) + if "skill" in normalize(query).split(): + score += 2 + return score, reasons + + +def score_project( + query: str, project: dict[str, Any], amd_query: bool +) -> tuple[int, list[str]]: + normalized_query = normalize(query) + query_tokens = tokens(query) + score = 0 + reasons: list[str] = [] + project_names = [str(project["id"]), str(project["display_name"])] + for name in project_names: + if phrase_present(name, normalized_query): + score += 18 + reasons.append(f"explicit project `{project['display_name']}`") + break + name_overlap = query_tokens & tokens(" ".join(project_names)) + if name_overlap: + score += 4 * len(name_overlap) + for keyword in project.get("keywords", []): + keyword = str(keyword) + if phrase_present(keyword, normalized_query): + score += 10 if " " in normalize(keyword) else 8 + reasons.append(f"matched `{keyword}`") + continue + overlap = query_tokens & tokens(keyword) + score += min(3, len(overlap)) + for category in project.get("categories", []): + if phrase_present(str(category).replace("-", " "), normalized_query): + score += 6 + reasons.append(f"matched {category} domain") + if ( + score > 0 + and amd_query + and any( + repo.get("tier") == "amd-official" + for repo in project.get("repositories", []) + ) + ): + score += 8 + return score, list(dict.fromkeys(reasons))[:4] + + +def route_projects( + query: str, + registry: dict[str, Any], + scope: str = "curated", + limit: int = 5, +) -> list[dict[str, Any]]: + amd_query = has_amd_signal(query, registry) + excluded = {repo.lower() for repo in registry.get("excluded_repositories", [])} + routed: list[dict[str, Any]] = [] + for project in registry["projects"]: + repositories = [ + repo + for repo in project.get("repositories", []) + if str(repo.get("repo", "")).lower() not in excluded + ] + if scope == "amd" and not any( + repo.get("tier") == "amd-official" for repo in repositories + ): + continue + score, reasons = score_project(query, project, amd_query) + if score < 7: + continue + repositories.sort(key=lambda repo: int(repo.get("priority", 0)), reverse=True) + routed.append( + { + "type": "source_project", + "installable": False, + "id": project["id"], + "name": project["display_name"], + "description": project["description"], + "categories": project.get("categories", []), + "score": score, + "why": reasons or ["matched project vocabulary"], + "repositories": [ + { + **repo, + "url": f"https://github.com/{repo['repo']}", + "tier_label": TIER_LABELS.get( + repo.get("tier"), repo.get("tier") + ), + } + for repo in repositories + ], + } + ) + routed.sort( + key=lambda item: ( + item["score"], + max((repo.get("priority", 0) for repo in item["repositories"]), default=0), + item["name"], + ), + reverse=True, + ) + return routed[:limit] + + +def _rank_skills( + query: str, entries: list[dict[str, str]], installed: bool +) -> list[dict[str, Any]]: + ranked: list[dict[str, Any]] = [] + for entry in entries: + score, reasons = score_catalog_entry(query, entry) + if score < 7: + continue + result: dict[str, Any] = { + "type": "installed_skill" if installed else "installable_skill", + "installable": not installed, + "name": entry["name"], + "description": entry["description"], + "score": score + (60 if installed else 45), + "why": reasons, + "origin": entry["origin"], + "url": entry["url"], + } + if not installed: + result["install_command"] = ( + f"npx skills add amd/skills --skill {entry['name']} --global --yes" + ) + ranked.append(result) + ranked.sort(key=lambda item: (item["score"], item["name"]), reverse=True) + return ranked + + +def _deduplicate_catalog(entries: Iterable[dict[str, str]]) -> list[dict[str, str]]: + return list({entry["name"]: entry for entry in entries}.values()) + + +def _gh_json(arguments: list[str], timeout: int = 25) -> Any: + gh = shutil.which("gh") + if not gh: + raise FinderError("Live GitHub search requires the `gh` CLI") + try: + process = subprocess.run( + [gh, *arguments], + check=False, + capture_output=True, + text=True, + encoding="utf-8", + timeout=timeout, + ) + except subprocess.TimeoutExpired as exc: + raise FinderError(f"GitHub search timed out after {timeout}s") from exc + if process.returncode != 0: + detail = (process.stderr or process.stdout).strip() + raise FinderError(f"GitHub search failed: {detail[:400]}") + return json.loads(process.stdout or "[]") + + +def _search_terms(query: str) -> list[str]: + ordered: list[str] = [] + for token in normalize(query).split(): + if token in STOP_WORDS or len(token) < 3: + continue + if token not in ordered: + ordered.append(token) + ordered.sort( + key=lambda value: (any(char.isdigit() for char in value), len(value)), + reverse=True, + ) + return ordered[:4] + + +def live_code_search( + query: str, + projects: list[dict[str, Any]], + extra_repositories: Iterable[str] = (), + repository_limit: int = 5, + result_limit: int = 2, +) -> tuple[list[dict[str, Any]], list[str]]: + repositories: list[tuple[str, str, str]] = [] + for project in projects: + for repo in project["repositories"]: + repositories.append((repo["repo"], project["id"], repo["tier"])) + repositories.extend( + (repo, repo.lower().replace("/", "-"), "user-specified") + for repo in extra_repositories + ) + unique_repositories = list(dict.fromkeys(repositories))[:repository_limit] + terms = _search_terms(query) + results: list[dict[str, Any]] = [] + warnings: list[str] = [] + for repository, project_id, tier in unique_repositories: + matches: list[dict[str, Any]] = [] + for term in terms[:2]: + try: + payload = _gh_json( + [ + "search", + "code", + term, + "--repo", + repository, + "--limit", + str(result_limit), + "--json", + "path,repository,textMatches,url", + ] + ) + except (FinderError, json.JSONDecodeError) as exc: + warnings.append(f"{repository}: {exc}") + break + if isinstance(payload, list) and payload: + matches = payload + break + for match in matches[:result_limit]: + path = str(match.get("path", "")) + lower_path = path.lower() + if lower_path.endswith("skill.md"): + result_type = "embedded_skill" + elif lower_path.startswith("docs/") or lower_path.endswith(".md"): + result_type = "guide" + elif "example" in lower_path: + result_type = "code_example" + else: + result_type = "source_code" + fragments = match.get("textMatches", []) or [] + fragment = "" + if fragments and isinstance(fragments[0], dict): + fragment = " ".join(str(fragments[0].get("fragment", "")).split())[:240] + results.append( + { + "type": result_type, + "installable": False, + "project_id": project_id, + "repository": repository, + "source_tier": tier, + "path": path, + "url": match.get("url"), + "excerpt": fragment, + } + ) + return results, list(dict.fromkeys(warnings)) + + +def general_repository_search(query: str, limit: int = 5) -> list[dict[str, Any]]: + payload = _gh_json( + [ + "search", + "repos", + query, + "--limit", + str(limit), + "--json", + "fullName,url,description,stargazersCount,updatedAt", + ] + ) + return [ + { + "type": "unreviewed_repository", + "installable": False, + "name": entry.get("fullName"), + "url": entry.get("url"), + "description": entry.get("description") or "", + "stars": entry.get("stargazersCount"), + "updated_at": entry.get("updatedAt"), + "source_tier": "unreviewed", + } + for entry in payload + ] + + +def find( + query: str, + *, + scope: str = "curated", + kind: str = "all", + limit: int = 5, + offline: bool = False, + live: bool = False, + general_github: bool = False, + extra_repositories: Iterable[str] = (), + extra_installed_roots: Iterable[str] = (), + live_repository_limit: int = 5, + live_result_limit: int = 2, +) -> dict[str, Any]: + query = query.strip() + if not query: + raise FinderError("Search query cannot be empty") + reject_secrets(query) + registry = load_registry() + warnings: list[str] = [] + + installed_results: list[dict[str, Any]] = [] + catalog_results: list[dict[str, Any]] = [] + if kind in {"all", "skills"}: + installed = [ + entry + for entry in installed_skills(extra_installed_roots) + if entry["name"] != SELF_SKILL_NAME + ] + installed_results = _rank_skills(query, installed, installed=True)[:limit] + + catalog = local_catalog() + if not catalog and not offline: + try: + catalog = remote_catalog() + except (FinderError, OSError, ValueError, urllib.error.URLError) as exc: + warnings.append(f"Live AMD catalog unavailable: {exc}") + if not catalog: + catalog = snapshot_catalog() + installed_names = {entry["name"] for entry in installed} + catalog = [ + entry + for entry in _deduplicate_catalog(catalog) + if entry["name"] not in installed_names and entry["name"] != SELF_SKILL_NAME + ] + catalog_results = _rank_skills(query, catalog, installed=False)[:limit] + + source_results: list[dict[str, Any]] = [] + if kind in {"all", "sources"} and scope != "catalog": + source_results = route_projects(query, registry, scope=scope, limit=limit) + + live_results: list[dict[str, Any]] = [] + if live and not offline: + live_results, live_warnings = live_code_search( + query, + source_results, + extra_repositories=extra_repositories, + repository_limit=live_repository_limit, + result_limit=live_result_limit, + ) + warnings.extend(live_warnings) + elif extra_repositories: + warnings.append("Explicit repositories require --live for code search") + + general_results: list[dict[str, Any]] = [] + if general_github: + if offline or not live: + warnings.append( + "General GitHub search requires --live and cannot run offline" + ) + else: + try: + general_results = general_repository_search(query, limit=limit) + except (FinderError, json.JSONDecodeError) as exc: + warnings.append(str(exc)) + + return { + "query": query, + "scope": scope, + "catalog_repository": CATALOG_REPO, + "installed_skills": installed_results, + "catalog_skills": catalog_results, + "source_projects": source_results, + "live_matches": live_results, + "unreviewed_repositories": general_results, + "warnings": list(dict.fromkeys(warnings)), + } + + +def _markdown_link(label: str, url: str | None) -> str: + return f"[{label}]({url})" if url else label + + +def render_markdown(result: dict[str, Any]) -> str: + lines = ["# AMD skill finder results", "", f"Query: `{result['query']}`"] + + sections = ( + ("Installed skills", result["installed_skills"]), + ("AMD catalog skills", result["catalog_skills"]), + ) + for heading, entries in sections: + if not entries: + continue + lines.extend(["", f"## {heading}", ""]) + for entry in entries: + lines.append(f"- **{entry['name']}** — {entry['description']}") + lines.append( + f" Provenance: {entry['origin']}. Why: {'; '.join(entry['why'])}." + ) + if entry.get("install_command"): + lines.append(f" Install after approval: `{entry['install_command']}`") + + if result["source_projects"]: + lines.extend(["", "## Curated source projects", ""]) + for project in result["source_projects"]: + lines.append(f"- **{project['name']}** — {project['description']}") + repo_links = [ + f"{_markdown_link(repo['repo'], repo['url'])} ({repo['tier_label']}, {repo['role']})" + for repo in project["repositories"] + ] + lines.append(f" Sources: {', '.join(repo_links)}.") + lines.append( + f" Why: {'; '.join(project['why'])}. Result type: source project, not installable." + ) + + if result["live_matches"]: + lines.extend(["", "## Live repository matches", ""]) + for match in result["live_matches"]: + label = f"{match['repository']}:{match['path']}" + lines.append( + f"- {_markdown_link(label, match.get('url'))} " + f"— `{match['type']}`, {TIER_LABELS.get(match['source_tier'], match['source_tier'])}" + ) + if match.get("excerpt"): + lines.append(f" {match['excerpt']}") + + if result["unreviewed_repositories"]: + lines.extend(["", "## Unreviewed GitHub fallback", ""]) + for entry in result["unreviewed_repositories"]: + lines.append( + f"- {_markdown_link(str(entry['name']), entry.get('url'))} — " + f"{entry['description']} (unreviewed; inspect before use)" + ) + + if not any( + result[key] + for key in ( + "installed_skills", + "catalog_skills", + "source_projects", + "live_matches", + "unreviewed_repositories", + ) + ): + lines.extend(["", "No strong match was found in the selected scope."]) + + if result["warnings"]: + lines.extend(["", "## Warnings", ""]) + lines.extend(f"- {warning}" for warning in result["warnings"]) + return "\n".join(lines) + "\n" + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("query", help="AMD/ROCm task or capability to find") + parser.add_argument( + "--scope", + choices=("catalog", "amd", "curated"), + default="curated", + help="catalog only, AMD-owned sources, or all curated sources", + ) + parser.add_argument( + "--kind", + choices=("all", "skills", "sources"), + default="all", + help="return skills, source projects, or both", + ) + parser.add_argument( + "--limit", type=int, default=5, help="maximum results per primary section" + ) + parser.add_argument( + "--offline", action="store_true", help="avoid all network access" + ) + parser.add_argument( + "--live", action="store_true", help="search code in routed repositories with gh" + ) + parser.add_argument( + "--general-github", + action="store_true", + help="also search unreviewed GitHub repositories (requires --live)", + ) + parser.add_argument( + "--repo", + action="append", + default=[], + metavar="OWNER/REPO", + help="also search an explicit repository; repeat as needed (requires --live)", + ) + parser.add_argument( + "--installed-root", + action="append", + default=[], + metavar="PATH", + help="additional directory containing installed skill folders", + ) + parser.add_argument( + "--live-repos", + type=int, + default=5, + help="maximum repositories for live code search", + ) + parser.add_argument( + "--live-results", + type=int, + default=2, + help="maximum code matches per repository", + ) + parser.add_argument( + "--json", action="store_true", help="emit machine-readable JSON" + ) + return parser + + +def main(argv: list[str] | None = None) -> int: + args = build_parser().parse_args(argv) + if args.limit < 1 or args.live_repos < 1 or args.live_results < 1: + print("Limits must be positive integers", file=sys.stderr) + return 2 + try: + result = find( + args.query, + scope=args.scope, + kind=args.kind, + limit=args.limit, + offline=args.offline, + live=args.live, + general_github=args.general_github, + extra_repositories=args.repo, + extra_installed_roots=args.installed_root, + live_repository_limit=args.live_repos, + live_result_limit=args.live_results, + ) + except FinderError as exc: + print(f"amd-skill-finder: {exc}", file=sys.stderr) + return 2 + if args.json: + print(json.dumps(result, indent=2, sort_keys=True)) + else: + print(render_markdown(result), end="") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/amd-skill-finder/skill-card.md b/skills/amd-skill-finder/skill-card.md new file mode 100644 index 0000000..b977019 --- /dev/null +++ b/skills/amd-skill-finder/skill-card.md @@ -0,0 +1,13 @@ +# Skill Card + +## Description + +Find trusted AMD and ROCm skills or curated source projects for an AMD GPU task while preserving installation and provenance boundaries. + +## Owner + +AMD + +## License + +MIT diff --git a/skills/amd-skill-finder/tests/test_find_skills.py b/skills/amd-skill-finder/tests/test_find_skills.py new file mode 100644 index 0000000..8a2b662 --- /dev/null +++ b/skills/amd-skill-finder/tests/test_find_skills.py @@ -0,0 +1,236 @@ +"""Deterministic tests for amd-skill-finder.""" + +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +import unittest +from pathlib import Path +from unittest import mock + +SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "find_skills.py" +SPEC = importlib.util.spec_from_file_location("amd_skill_finder", SCRIPT) +assert SPEC and SPEC.loader +finder = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(finder) + + +class RegistryTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.registry = finder.load_registry() + cls.projects = {project["id"]: project for project in cls.registry["projects"]} + + def test_requested_projects_and_canonical_repositories_are_present(self): + required = { + "quark": "amd/Quark", + "mori": "ROCm/mori", + "hyperloom": "AMD-AGI/Hyperloom", + "pytorch": "pytorch/pytorch", + "jax": "jax-ml/jax", + "triton": "triton-lang/triton", + "torchtitan": "pytorch/torchtitan", + "lmcache": "LMCache/LMCache", + "mooncake": "kvcache-ai/Mooncake", + "tilelang": "tile-ai/tilelang", + "nixl": "ai-dynamo/nixl", + "miles": "radixark/miles", + "verl": "verl-project/verl", + "vime": "vllm-project/vime", + } + for project_id, repository in required.items(): + with self.subTest(project=project_id): + repos = { + entry["repo"] for entry in self.projects[project_id]["repositories"] + } + self.assertIn(repository, repos) + + def test_dynamo_repository_is_excluded(self): + excluded = {repo.lower() for repo in self.registry["excluded_repositories"]} + all_repositories = { + repo["repo"].lower() + for project in self.registry["projects"] + for repo in project["repositories"] + } + self.assertIn("ai-dynamo/dynamo", excluded) + self.assertNotIn("ai-dynamo/dynamo", all_repositories) + self.assertIn("ai-dynamo/nixl", all_repositories) + + def test_project_ids_and_repositories_are_unique(self): + ids = [project["id"] for project in self.registry["projects"]] + self.assertEqual(len(ids), len(set(ids))) + for project in self.registry["projects"]: + repos = [repo["repo"].lower() for repo in project["repositories"]] + self.assertEqual(len(repos), len(set(repos)), project["id"]) + + def test_fork_pairs_are_grouped(self): + expected = { + "pytorch": {"ROCm/pytorch", "pytorch/pytorch"}, + "jax": {"ROCm/jax", "jax-ml/jax"}, + "triton": {"ROCm/triton", "triton-lang/triton"}, + } + for project_id, repositories in expected.items(): + actual = { + repo["repo"] for repo in self.projects[project_id]["repositories"] + } + self.assertEqual(actual, repositories) + + +class RoutingTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.registry = finder.load_registry() + + def test_kv_transfer_routes_to_official_mori_first(self): + results = finder.route_projects( + "disaggregated KV-cache transfer over RDMA on MI355X", + self.registry, + limit=6, + ) + ids = [result["id"] for result in results] + self.assertEqual(ids[0], "mori") + self.assertTrue({"mori", "lmcache", "mooncake", "nixl"}.issubset(ids)) + self.assertNotIn("triton", ids) + self.assertTrue(all(result["installable"] is False for result in results)) + + def test_quark_quantization_routes_to_official_quark_first(self): + self.assertTrue(finder.has_amd_signal("Quark PTQ", self.registry)) + results = finder.route_projects( + "Use AMD Quark PTQ to quantize a Qwen model to FP8", + self.registry, + limit=5, + ) + self.assertEqual(results[0]["id"], "quark") + self.assertEqual(results[0]["repositories"][0]["repo"], "amd/Quark") + self.assertEqual(results[0]["repositories"][0]["tier"], "amd-official") + + def test_post_training_routes_to_three_requested_frameworks(self): + results = finder.route_projects( + "RL post-training with vLLM on ROCm", + self.registry, + limit=10, + ) + ids = {result["id"] for result in results} + self.assertTrue({"miles", "verl", "vime"}.issubset(ids)) + + def test_triton_is_one_project_with_two_repositories(self): + results = finder.route_projects( + "optimize a Triton kernel on MI355X", + self.registry, + limit=6, + ) + triton = next(result for result in results if result["id"] == "triton") + self.assertEqual( + [repo["repo"] for repo in triton["repositories"]], + ["ROCm/triton", "triton-lang/triton"], + ) + + def test_generic_frontend_optimization_does_not_route(self): + results = finder.route_projects( + "Optimize my React page load and reduce the JavaScript bundle size", + self.registry, + ) + self.assertEqual(results, []) + + def test_amd_scope_excludes_upstream_only_projects(self): + results = finder.route_projects( + "RL post-training with vLLM on ROCm", + self.registry, + scope="amd", + limit=20, + ) + ids = {result["id"] for result in results} + self.assertFalse({"miles", "verl", "vime"} & ids) + + +class CatalogAndLiveSearchTests(unittest.TestCase): + def test_active_finder_does_not_recommend_itself(self): + result = finder.find( + "optimize a Triton kernel on MI355X", + offline=True, + ) + names = { + entry["name"] + for key in ("installed_skills", "catalog_skills") + for entry in result[key] + } + self.assertNotIn("amd-skill-finder", names) + + def test_catalog_browse_lists_published_skills(self): + result = finder.find("Show AMD skills", offline=True, scope="catalog") + names = {entry["name"] for entry in result["catalog_skills"]} + self.assertIn("serving-llms-on-instinct", names) + self.assertNotIn("amd-skill-finder", names) + + def test_installed_skill_ranks_above_catalog_copy(self): + entry = { + "name": "serving-llms-on-instinct", + "description": "Serve vLLM on AMD Instinct MI355X GPUs.", + "origin": "test", + "url": "test", + } + installed = finder._rank_skills( + "serve vLLM on MI355X", [entry], installed=True + )[0] + catalog = finder._rank_skills("serve vLLM on MI355X", [entry], installed=False)[ + 0 + ] + self.assertGreater(installed["score"], catalog["score"]) + self.assertEqual(installed["type"], "installed_skill") + self.assertNotIn("install_command", installed) + self.assertEqual(catalog["type"], "installable_skill") + + def test_live_skill_file_is_embedded_not_installable(self): + project = { + "id": "hyperloom", + "repositories": [{"repo": "AMD-AGI/Hyperloom", "tier": "amd-official"}], + } + payload = [ + { + "path": "src/hyperloom/inference_optimizer/SKILL.md", + "url": "https://example.invalid/SKILL.md", + "textMatches": [{"fragment": "GPU optimization skill"}], + } + ] + with mock.patch.object(finder, "_gh_json", return_value=payload): + results, warnings = finder.live_code_search( + "Hyperloom optimization", [project] + ) + self.assertEqual(warnings, []) + self.assertEqual(results[0]["type"], "embedded_skill") + self.assertFalse(results[0]["installable"]) + + def test_secret_like_query_is_rejected(self): + with self.assertRaises(finder.FinderError): + finder.reject_secrets("search github with ghp_abcdefghijklmnopqrstuvwxyz") + + def test_offline_cli_emits_json_without_dynamo_repository(self): + process = subprocess.run( + [ + sys.executable, + str(SCRIPT), + "disaggregated KV cache RDMA on MI355X", + "--offline", + "--kind", + "sources", + "--json", + ], + check=True, + capture_output=True, + text=True, + encoding="utf-8", + ) + payload = json.loads(process.stdout) + repositories = { + repo["repo"].lower() + for project in payload["source_projects"] + for repo in project["repositories"] + } + self.assertNotIn("ai-dynamo/dynamo", repositories) + self.assertIn("rocm/mori", repositories) + + +if __name__ == "__main__": + unittest.main()