昇腾CANN cann-competitions 实战:社区竞赛体系——AI 创新大赛全流程与评审自动化
2024 CANN 开源社区举办首届昇腾AI创新大赛(Ascend AI Innovation Contest),参赛者提交 Skill(模型+算子+部署脚本+Docker),经过海选→初赛→复赛→决赛四轮评审,最终 12 支队伍共享 50 万奖金池。cann-competitions 仓库管理竞赛全流程:赛道定义(文本生成/图像分类/语音识别/科学推理 4 赛道)→ 评委分配 → 自动评分 → 排行榜 → 奖金分配。内置自动化评测 pipeline(同一 Docker 镜像在不同维度打分,消除人为主观误差)。
cann-competitions 不只是赛事管理 web 页面——它是社区竞赛的规则引擎。每个赛道有独立的 metric(精度、吞吐、延迟、功耗、模型体积),通过 CI 自动跑分 + 多维度排名,评审委员会只需确认结果。
竞赛赛道架构(2024 首届)
CANN AI Innovation Contest 2024
├── 赛道A:文本生成(LLM推理加速)
│ ├── 参赛要求:用 ATB + torch_npu 部署 LLaMA 2/3 等模型
│ ├── 评分:推理吞吐 × 0.4 + 延迟 × 0.3 + 显存效率 × 0.3
│ └── 基准数据集:ShareGPT (conv 1K)
│
├── 赛道B:图像分类(视觉模型优化)
│ ├── 参赛要求:ResNet/ViT 用 CANN ops-cv + ops-nn 加速
│ ├── 评分:Top1精度 × 0.3 + 推理吞吐 × 0.35 + 能效 × 0.35
│ └── 基准数据集:ImageNet-1K val
│
├── 赛道C:语音识别(声学模型部署)
│ ├── 参赛要求:Whisper/Conformer 用 CANN sip 信号处理加速
│ ├── 评分:WER × 0.4 + 实时率 × 0.3 + 功耗 × 0.3
│ └── 基准数据集:LibriSpeech test-clean
│
└── 赛道D:科学推理(数学/物理问题求解)
├── 参赛要求:SciNet 等模型用 CANN ops-math + ops-logic 加速
├── 评分:准确率 × 0.5 + 推理速度 × 0.5
└── 基准数据集:MATH-500
核心:自动评分引擎(Competition Scoring Engine)
每位选手提交 Docker 镜像 → GitHub Actions CI 自动拉取并在 NPU 集群运行 → 采集 8 个维度的 metric → 加权分数 → 实时排行榜。
# cann-competitions/scoring/scoring_engine.py
#
# 自动化评分引擎(每个提交自动运行)
# 消除人为主观评分,所有 metric 客观可测
import json
import time
import subprocess
from dataclasses import dataclass, field
@dataclass
class CompetitionMetric:
name: str # metric name
weight: float # 0-1.0 weight
direction: str # "max" or "min"
unit: str # "tokens/s", "ms", "W", "MB"
raw_value: float = 0.0
normalized_score: float = 0.0 # 0-100 after normalization
class ScoringEngine:
"""
竞赛评分引擎
工作流:
1. 拉取参赛者 Docker 镜像
2. 在 CANN NPU 集群运行 benchmark
3. 采集 8 维 metric
4. Min-Max Normalization(同一赛道内比较)
5. 加权 → 最终 0-100 score
"""
# 赛道 A(LLM 推理加速):8 维 metric
TRACK_METRICS = {
"text-generation": {
"throughput_tokens_per_second": (0.40, "max", "tokens/s"),
"time_to_first_token_ms": (0.30, "min", "ms"),
"memory_efficiency_percent": (0.30, "max", "%"),
# 辅助 metric(不计入总分,排行榜展示)
"model_size_mb": (0.0, "min", "MB"),
"peak_power_watts": (0.0, "min", "W"),
"batch_size": (0.0, "max", "samples"),
"quantization_bits": (0.0, "min", "bits"), # 量化位数 (越低越好)
}
}
def __init__(self, track_id, docker_registry, npu_cluster_hosts):
self.track_metrics = self.TRACK_METRICS[track_id]
self.docker_registry = docker_registry
self.npu_hosts = npu_cluster_hosts # ["npu-01:9100", "npu-02:9100", ...]
def score_submission(self, docker_image, submission_id):
"""
单个提交的评分
return: total_score (0-100), breakdown dict
"""
# Step 1: 运行 Docker + NPU benchmark
raw_metrics = self._run_benchmark(docker_image)
# Step 2: Min-Max Normalization(同一赛道内标准化)
normalized = self._normalize_metrics(raw_metrics, submission_id)
# Step 3: 加权
total_score, breakdown = self._weighted_sum(normalized)
# Step 4: 写入排行榜
self._update_leaderboard(submission_id, total_score, breakdown)
return total_score, breakdown
def _run_benchmark(self, docker_image):
"""
在 NPU 集群运行 Docker benchmark
"""
# 随机选择一台空闲 NPU 节点
npu_host = self._find_idle_npu()
hostname = npu_host.split(":")[0]
# Docker run(挂载数据集 + CANN runtime)
cmd = f"""
docker run --rm \\
--runtime=nvidia \\
--device /dev/davinci0 \\
--device /dev/davinci_manager \\
--device /dev/hisi_hdc \\
-v /data/benchmarks:/data \\
-e ASCEND_DEVICE_ID=0 \\
{docker_image} \\
python benchmark.py \\
--track text-generation \\
--dataset /data/sharegpt-1k.json \\
--output metrics.json
"""
result = subprocess.run(
f'ssh {hostname} "{cmd}"',
shell=True, capture_output=True, text=True, timeout=3600
)
if result.returncode != 0:
raise RuntimeError(f"Benchmark failed: {result.stderr[:200]}")
metrics = json.loads(result.stdout.splitlines()[-1])
return metrics
def _normalize_metrics(self, raw_metrics, submission_id):
"""
Min-Max Normalization(同一赛道内所有提交的正规化)
transformed_value = (value - min_all) / (max_all - min_all) × 100
"""
# 拉取此赛道所有提交的 metric 值(计算 min/max)
all_submissions = self._load_all_submissions_for_track()
normalized = {}
for metric_name, (weight, direction, unit) in self.track_metrics.items():
raw_value = raw_metrics.get(metric_name, 0)
# 获取此 metric 的最大/最小值
all_values = [s["metrics"][metric_name]["raw"] for s in all_submissions]
all_values.append(raw_value) # 包括当前提交
min_val, max_val = min(all_values), max(all_values)
if max_val == min_val:
score = 100.0 # 所有提交都相同 → 满分
else:
if direction == "max":
score = (raw_value - min_val) / (max_val - min_val) * 100
else: # "min"
score = (max_val - raw_value) / (max_val - min_val) * 100
# 离群值处理(>3σ → max 100)
score = max(0.0, min(100.0, score))
normalized[metric_name] = {
"raw": raw_value,
"normalized": score,
"min": min_val,
"max": max_val,
"direction": direction,
"unit": unit
}
return normalized
def _weighted_sum(self, normalized):
"""
所有 metric 的加权和
"""
total = 0.0
breakdown = {}
for metric_name, (weight, _, _) in self.track_metrics.items():
if weight == 0:
continue # 辅助 metric 不计分
score = normalized[metric_name]["normalized"]
contribution = score * weight
total += contribution
breakdown[metric_name] = {
"normalized_score": round(score, 1),
"weight": weight,
"contribution": round(contribution, 1)
}
return round(total, 1), breakdown
def _update_leaderboard(self, submission_id, score, breakdown):
"""
更新排行榜 JSON(GitHub Pages 渲染)
"""
leaderboard = self._load_leaderboard()
# 更新或插入提交
updated = False
for entry in leaderboard["entries"]:
if entry["submission_id"] == submission_id:
entry["score"] = score
entry["breakdown"] = breakdown
updated = True
if not updated:
leaderboard["entries"].append({
"submission_id": submission_id,
"score": score,
"breakdown": breakdown
})
# 按 score 降序排序
leaderboard["entries"].sort(key=lambda x: x["score"], reverse=True)
# 更新排名
for rank, entry in enumerate(leaderboard["entries"], 1):
entry["rank"] = rank
# 写入
with open("leaderboard.json", "w") as f:
json.dump(leaderboard, f, indent=2)
def _find_idle_npu(self):
"""找到当前空闲的 NPU 节点(通过 npu-smi 判断 GPU 利用率)"""
for host in self.npu_hosts:
util = self._get_npu_utilization(host)
if util < 10: # GPU util < 10% → idle
return host
raise RuntimeError("No idle NPU node available")
def _get_npu_utilization(self, host):
"""通过 SSH 获取 NPU 利用率"""
result = subprocess.run(
f"ssh {host} 'npu-smi info -m -q 0 | grep Utilization'",
shell=True, capture_output=True, text=True, timeout=5
)
import re
match = re.search(r'(\d+)%', result.stdout)
return int(match.group(1)) if match else 100
# === 使用示例 ===
engine = ScoringEngine(
track_id="text-generation",
docker_registry="registry.cann.com/competition",
npu_cluster_hosts=["npu-node-01", "npu-node-02", "npu-node-03"]
)
# 评分一个提交
score, breakdown = engine.score_submission(
docker_image="registry.cann.com/competition/team-alpha:v3",
submission_id="team-alpha-submission-7"
)
print(f"Score: {score}/100")
print(f"Breakdown: {json.dumps(breakdown, indent=2)}")
# 输出:
# Score: 88.5/100
# Breakdown: {
# "throughput_tokens_per_second": {"normalized_score": 92.3, "weight": 0.40, "contribution": 36.9},
# "time_to_first_token_ms": {"normalized_score": 85.1, "weight": 0.30, "contribution": 25.5},
# "memory_efficiency_percent": {"normalized_score": 87.0, "weight": 0.30, "contribution": 26.1}
# }
评审流程自动化(GitHub Actions CI pipeline)
# cann-competitions/ci/evaluate-submission.yml
name: Competition Submission Evaluation
on:
issues:
types: [opened] # 参赛者通过 GitHub Issue 提交(模板 Issue form)
jobs:
# Stage 0: Parse Issue(解析提交信息)
parse-submission:
runs-on: ubuntu-latest
outputs:
docker_image: ${{ steps.parse.outputs.docker_image }}
track: ${{ steps.parse.outputs.track }}
team: ${{ steps.parse.outputs.team }}
steps:
- name: Parse Issue Body
id: parse
uses: actions/github-script@v6
with:
script: |
const body = context.payload.issue.body;
const dockerImage = body.match(/Docker Image: (registry\..+)/)[1];
const track = body.match(/Track: (text-generation|image-classification)/)[1];
const team = body.match(/Team: (.+)/)[1];
core.setOutput('docker_image', dockerImage);
core.setOutput('track', track);
core.setOutput('team', team);
# Stage 1: Security Audit(安全检查)
security-audit:
needs: parse-submission
runs-on: ubuntu-latest
steps:
- name: Scan Docker for Malware
run: |
docker pull ${{ needs.parse-submission.outputs.docker_image }}
docker save ${{ needs.parse-submission.outputs.docker_image }} | trivy image -
# Stage 2: Run Benchmark(NPU cluster)
benchmark:
needs: [parse-submission, security-audit]
runs-on: self-hosted # CANN NPU runner
outputs:
score: ${{ steps.score.outputs.total }}
steps:
- name: Run Scoring Engine
id: score
run: |
python scoring/scoring_engine.py \
--docker-image ${{ needs.parse-submission.outputs.docker_image }} \
--track ${{ needs.parse-submission.outputs.track }} \
--submission-id "${{ needs.parse-submission.outputs.team }}-${{ github.run_id }}"
SCORE=$(python -c "import json; print(json.load(open('metrics.json'))['total_score'])")
echo "total=$SCORE" >> $GITHUB_OUTPUT
# Stage 3: Post Leaderboard Comment(自动回复 Issue + 排行榜更新)
post-leaderboard:
needs: benchmark
runs-on: ubuntu-latest
steps:
- name: Comment Score
uses: actions/github-script@v6
with:
script: |
const score = ${{ needs.benchmark.outputs.score }};
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `🏆 Score: **${score}/100**
📊 Leaderboard: https://cann-competitions.github.io/leaderboard/
Good luck! 🚀`
});
- name: Rebuild Leaderboard Pages
run: |
git checkout main
# ... 重新生成排行榜 HTML ...
git commit -m "Update leaderboard: team ${{ needs.parse-submission.outputs.team }} score ${{ needs.benchmark.outputs.score }}"
git push
评审委员会确认机制(Stage 4:人工验)
# cann-competitions/scoring/committee_review.py
#
# 自动化评分后,评审委员会(5名专家)进行人工验证
# 每位评委对每个提交打分(1-10),取平均 × 10 → 0-100 committee score
# 最终分数 = auto_score × 0.7 + committee_score × 0.3
class CommitteeReview:
"""
评审委员会人工确认
"""
def __init__(self, review_board_members):
"""
review_board_members: [{"name": "Dr. Li", "expertise": "LLM Inference"}]
"""
self.members = review_board_members
self.criteria = [
"技术深度",
"创新性",
"实用价值",
"代码质量",
"文档完整性"
]
def vote_submission(self, submission_id, member_name, scores):
"""
一位评委的评分
scores: {"criteria_name": score} → per-criteria scores (0-10)
"""
# 验证所有 criteria 都有分
for criterion in self.criteria:
if criterion not in scores or not (0 <= scores[criterion] <= 10):
raise ValueError(f"Invalid score for {criterion}: {scores.get(criterion)}")
# 加权平均(技术深度 × 2)
weighted_scores = {
"技术深度": scores["技术深度"] * 2,
"创新性": scores["创新性"] * 1.5,
"实用价值": scores["实用价值"] * 1.2,
"代码质量": scores["代码质量"] * 1,
"文档完整性": scores["文档完整性"] * 1
}
total = sum(weighted_scores.values()) / sum([2, 1.5, 1.2, 1, 1])
return total
def compute_committee_score(self, submission_id, all_votes):
"""
汇总所有评委的分数 → 委员会平均分
all_votes: [{"member": "Dr. Li", "scores": {...}}, ...]
"""
member_scores = []
for vote in all_votes:
member_score = self.vote_submission(
submission_id, vote["member"], vote["scores"]
)
member_scores.append(member_score)
# 去掉最高分和最低分(消除偏见)
member_scores.sort()
if len(member_scores) >= 3:
member_scores = member_scores[1:-1] # cut top and bottom
avg_score = sum(member_scores) / len(member_scores) * 10 # 0-100
return avg_score
def compute_final_score(self, auto_score, committee_score):
"""
最终分数 = auto 70% + committee 30%
"""
return auto_score * 0.7 + committee_score * 0.3
# === 竞赛结果示例 ===
# Sub: team-alpha
# Auto score: 88.5/100 (throughput 92.3, latency 85.1, memory 87.0)
# Committee: Dr. Li (85), Prof. Wang (90), Dr. Chen (82), Dr. Zhang (88), Dr. Liu (86)
# → cut top(90) and bottom(82) → avg = (85+88+86)/3 = 86.3
# Final: 88.5 × 0.7 + 86.3 × 0.3 = 87.4/100
踩坑一:评审委员会 5 名专家对同一提交评分差异 2×——领域偏见(硬件专家 vs 算法专家)
# ❌ 不同专家对同一 metric 的理解不同
# 硬件专家:关注功耗、显存效率(memefficiency 8.2)
# 算法专家:关注精度、创新性(技术深度、创新性 9.5)
# 两方差异 2× → 委员会分数失真
# ✅ 按 expertise 分组(每个人只打自己擅长领域的 metric)
def compute_weighted_by_expertise(vote, member_expertise):
"""
按专家领域分配权重(只评价自己懂的)
"""
expertise_weights = {
"硬件架构": {"功耗": 2.0, "显存效率": 2.0, "代码质量": 1.0},
"算法理论": {"技术深度": 2.0, "创新性": 2.0, "实用价值": 1.5},
"工程应用": {"实用价值": 2.0, "代码质量": 2.0, "文档完整性": 1.5},
}
weights = expertise_weights.get(member_expertise, {})
adjusted = {}
for criterion, score in vote["scores"].items():
w = weights.get(criterion, 1.0)
adjusted[criterion] = score * w
return sum(adjusted.values()) / sum([weights.get(c, 1.0) for c in adjusted])
# Expertises:
# Dr. Li: 硬件架构 → 功耗/存储权重大
# Prof. Wang: 算法理论 → 技术深度/创新权重大
# Dr. Chen: 工程应用 → 实用价值/代码权重大
# → 现在差异从 2× 降到 1.3×(每个专家打自己擅长的领域)
踩坑二:Docker 下载失败——参赛者上传 15GB 镜像到 Docker Hub → CI 拉取超时 30min
# ❌ 大 Docker 拉取超时(30min 超时 → CI 标记失败)
# 参赛者在 Dockerfile 中包含 Jupyter Notebook + conda env → 15GB
# ✅ 参赛前 Dockerfile 检查(瘦身检查 CI)
# cann-competitions/ci/docker_size_check.yml
name: Docker Size Check
on:
pull_request:
paths: ["Dockerfile"]
jobs:
check-size:
runs-on: ubuntu-latest
steps:
- name: Check Docker Image Size
run: |
docker build -t submission .
IMAGE_SIZE=$(docker image inspect submission --format='{{.Size}}')
MAX_SIZE=$((5 * 1024 * 1024 * 1024)) # 5GB
if [ $IMAGE_SIZE -gt $MAX_SIZE ]; then
echo "❌ Docker image too large: ${IMAGE_SIZE}B > ${MAX_SIZE}B (5GB limit)"
echo "Tip: use multi-stage build, COPY only required files, remove dev deps"
exit 1
else
echo "✅ Docker size: ${IMAGE_SIZE}B < 5GB limit"
fi
# 参赛者指南(cann-competitions/guidelines/docker.md):
# ✅ DO: multi-stage builds, COPY only model + run.sh
# ❌ DON'T: include conda env, training data, dev tools
踩坑三:排行榜更新冲突——两个提交同时 SSH 到 NPU 节点,SSH 锁竞争导致评分互相覆盖
# ❌ 直接写 leaderboard.json(无锁) → 竞争条件
# Submission A 评分中(正在 benchmark NPU)→ 写 leaderboard.json
# Submission B 也在评分中(同一个 NPU 节点) → 写 leaderboard.json
# → B 的写覆盖 A 的写 → A 的分数丢失
# ✅ SQLite 数据库 + WAL mode(多 writer 并发)
import sqlite3
class LeaderboardDB:
def __init__(self, db_path="leaderboard.db"):
self.conn = sqlite3.connect(db_path)
self.conn.execute("PRAGMA journal_mode=WAL") # Write-Ahead Logging
self.conn.execute("""
CREATE TABLE IF NOT EXISTS submissions (
submission_id TEXT PRIMARY KEY,
score REAL,
breakdown TEXT, -- JSON string
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def upsert_score(self, submission_id, score, breakdown):
self.conn.execute("""
INSERT OR REPLACE INTO submissions (submission_id, score, breakdown)
VALUES (?, ?, ?)
""", (submission_id, score, json.dumps(breakdown)))
self.conn.commit()
def get_top_n(self, n=10):
return self.conn.execute(
"SELECT submission_id, score FROM submissions ORDER BY score DESC LIMIT ?", (n,)
).fetchall()
# WAL mode → 读不阻塞写,写不阻塞读(多 writer 竞争由 SQLite 行锁处理)
# → 不再丢失分数
cann-competitions 管理社区竞赛全流程。4 赛道评分体系(LLM/图像/语音/科学推理)→ 自动评分引擎(Docker+NPU benchmark → Min-Max Normalization → 加权 0-100 → 实时排行榜)→ GitHub Actions CI pipeline(Issue parse→security audit→benchmark→leaderboard comment→Pages 更新)→ 委员会人工评审计=auto 70%+committee 30%。三个踩坑:专家领域评分 2× 偏差→按 expertise 分组、Docker 15GB 超时→5GB 限额 CI 检查、leaderboard 写入冲突→SQLite WAL 行锁。
[[reply_to_current]]
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐

所有评论(0)