昇腾CANN release-management 实战:版本发布管理——Release Notes 自动化与社区维护策略
55 个 CANN 仓库的版本零散管理:ops-transformer 在 8.0.1、hccl 在 8.0.2、ge 在 8.0.0-rc3 → 用户不知道该用哪个版本。release-management 仓库定义统一的语义化版本规范 + 集中发布管道:每月一次集中发布(Monthly Release Train)→ 所有仓库同步发布 8.x.0 大版本 + 8.x.p 补丁版,GitHub Release Notes 自动生成(Python 脚本扫 CHANGELOG + git log + closed Issues/PRs)。
release-management 不是 CI 或 Helm Chart 仓库——它是 CANN 的版本治理仓库。定义版本号规范、发布流程、Release Notes 模板、LTS(长期支持)策略。
CANN 版本语义化规范
版本格式:MAJOR.MINOR.PATCH[-PRE_RELEASE][+BUILD_META]
示例:8.0.2-rc3+ascend910
CANN 版本:
8.0.0 → 首个主版本(2024-10 发布)
8.0.1 → 第一个补丁版(Bug fix)
8.0.2 → 第二个补丁版
8.5.0 → 次要版本(新功能,兼容 8.0)
8.5.0-rc1 → 候选发布版(Release Candidate)
9.0.0-dev → 开发版(下一个主版本)
版本号含义:
MAJOR: 不兼容 API 变更(如 CANN 8→9,移除旧接口)
MINOR: 向后兼容新功能(如 8.0→8.5,新增算子不删旧算子)
PATCH: Bug fix + 安全补丁
预发布后缀:
-dev → 开发版(Daily builds,不可用于生产)
-alpha → Alpha 版(功能完整,内部测试)
-beta → Beta 版(外测,冻结特性能)
-rc1/2/3 → 候选发布版(Release Candidate,仅修 bug,不接新 feature)
构建元数据:
+ascend910 → 针对 Ascend 910 编译
+ascend950pr → 针对 Ascend 950PR 编译
+cuda12.1 → 兼容 CUDA 12.1 的变体
Release Manager(发布管理器:协调 55 个仓库统一发布)
# release-management/release_manager.py
#
# CANN 发布管理器:集中管理 55 个仓库的版本与发布流程
# 工作流:
# 1. 每月第 2 周三:所有仓库 freeze feature(冻结新功能)
# 2. 第 3 周一:所有仓库 tag RC1
# 3. 第 3 周四:所有仓库 tag 正式版
# 4. 自动生成 Release Notes(从 CHANGELOG + git log + Issues/PRs)
import json
import re
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import subprocess
from dataclasses import dataclass
@dataclass
class ReleaseVersion:
major: int # 8
minor: int # 0
patch: int # 2
pre_release: str = None # "rc1", "beta2", None
build_meta: str = None # "+ascend910"
def __str__(self):
version = f"{self.major}.{self.minor}.{self.patch}"
if self.pre_release:
version += f"-{self.pre_release}"
if self.build_meta:
version += f"+{self.build_meta}"
return version
def bump_patch(self) -> "ReleaseVersion":
"""补丁版本递增(8.0.2 → 8.0.3)"""
return ReleaseVersion(self.major, self.minor, self.patch + 1)
def bump_minor(self) -> "ReleaseVersion":
"""次要版本递增(8.0.2 → 8.1.0)"""
return ReleaseVersion(self.major, self.minor + 1, 0)
class CANNReleaseManager:
"""
CANN 版本发布管理器
协调 55 个仓库的版本统一发布
"""
MONTHLY_RELEASE_SCHEDULE = {
"feature_freeze": (2, "Wednesday"), # 第 2 周三:冻结功能
"rc1_tag": (3, "Monday"), # 第 3 周一:RC1
"rc2_tag": (3, "Wednesday"), # 第 3 周三:RC2 (if needed)
"stable_release": (3, "Thursday"), # 第 3 周四:正式版
"release_blog": (3, "Friday"), # 第 3 周五:发布公告
}
# 55 个仓库的当前版本映射
repo_versions: Dict[str, ReleaseVersion] = {
"ops-transformer": ReleaseVersion(8, 0, 2),
"ops-nn": ReleaseVersion(8, 0, 1),
"hccl": ReleaseVersion(8, 0, 2),
"ge": ReleaseVersion(8, 0, 0),
"ascend-transformer-boost": ReleaseVersion(8, 0, 2),
"opbase": ReleaseVersion(8, 0, 2),
"runtime": ReleaseVersion(8, 0, 1),
# ... 55 个仓库的版本
}
def __init__(self, repos_root="/repos"):
self.repos_root = repos_root
def get_release_schedule(self, month, year):
"""
返回本月发布计划(Feature freeze / RC / Stable),具体日期
"""
import calendar
first_day = datetime(year, month, 1)
month_cal = calendar.monthcalendar(year, month)
schedule = {}
for event, (week, weekday) in self.MONTHLY_RELEASE_SCHEDULE.items():
weekday_map = {
"Monday": 0, "Tuesday": 1, "Wednesday": 2,
"Thursday": 3, "Friday": 4
}
day = month_cal[week - 1][weekday_map[weekday]]
schedule[event] = datetime(year, month, day).strftime("%Y-%m-%d")
return schedule
def freeze_features(self, release_version: ReleaseVersion):
"""
Feature Freeze(第 2 周三):所有仓库冻结新功能
- 只接受 bug fix + doc update + test 改进
- 拒绝新 feature PR
"""
for repo_name in self.repo_versions:
repo_dir = f"{self.repos_root}/{repo_name}"
# 在 repo 根目录写入 freeze 标记
with open(f"{repo_dir}/.feature_freeze", "w") as f:
f.write(f"Freeze for {release_version} at {datetime.now()}\n")
# 在 GitHub issue/PR labels 上加 freeze label
self._add_freeze_label_to_github(repo_name, release_version)
print(f"❄️ {repo_name} feature freeze for {release_version}")
def tag_release_candidate(self, rc_version: ReleaseVersion):
"""
RC 标签(第 3 周一):所有仓库 tag RC1
- 流水线自动构建
- smoke test 通过 → 等待 RC2 or stable
"""
for repo_name, current_version in self.repo_versions.items():
repo_dir = f"{self.repos_root}/{repo_name}"
tag = f"v{rc_version}"
# git tag
subprocess.run(
f"cd {repo_dir} && git tag -a {tag} -m 'Release {rc_version}' && "
f"git push origin {tag}",
shell=True
)
print(f"🏷️ {repo_name}: tagged {tag}")
def generate_release_notes(self, release_version: ReleaseVersion) -> str:
"""
自动生成 Release Notes(从 CHANGELOG + git log + closed Issues/PRs)
"""
all_changes = {}
for repo_name in self.repo_versions:
repo_dir = f"{self.repos_root}/{repo_name}"
prev_tag = self._get_previous_tag(repo_name)
current_tag = f"v{release_version}"
# 1. 从 CHANGELOG.md 提取变更(结构化)
changelog_changes = self._parse_changelog(repo_name, release_version)
# 2. 从 git log 提取 commit 消息
git_log = subprocess.run(
f"cd {repo_dir} && git log {prev_tag}..{current_tag} --pretty=format:'%s'",
shell=True, capture_output=True, text=True
).stdout
# 3. 从 GitHub closed Issues/PRs 提取
github_changes = self._fetch_closed_issues_prs(repo_name)
# 合并
all_changes[repo_name] = {
"features": self._categorize("feature", changelog_changes, git_log, github_changes),
"bug_fixes": self._categorize("fix", changelog_changes, git_log, github_changes),
"performance": self._categorize("perf", changelog_changes, git_log, github_changes),
"docs": self._categorize("docs", changelog_changes, git_log, github_changes),
"breaking": self._categorize("breaking", changelog_changes, git_log, github_changes),
}
# 渲染为 Release Notes Markdown
return self._render_release_notes(release_version, all_changes)
def _categorize(self, category, changelog_changes, git_log, github_changes):
"""分类变更(feat / fix / perf / docs / breaking)"""
changes = []
# CHANGELOG 条目(结构化,最高优先级)
for entry in changelog_changes:
if category in entry.get("tags", []):
changes.append({
"text": entry["description"],
"source": "changelog",
"pr": entry.get("pr_number")
})
# git log 消息
for line in git_log.split("\n"):
if category == "feature" and line.startswith("feat"):
pr_num = re.search(r'#(\d+)', line)
changes.append({
"text": line.replace("feat:", "").strip(),
"source": "git",
"pr": pr_num.group(1) if pr_num else None
})
# GitHub Issues/PRs
for gh_item in github_changes:
if category == "feature" and gh_item.get("label") == "enhancement":
changes.append({
"text": gh_item["title"],
"source": "github",
"pr": gh_item["number"]
})
return changes
def _render_release_notes(self, version: ReleaseVersion, all_changes: dict) -> str:
"""
渲染 Release Notes Markdown(GitHub Release 样式)
"""
notes = f"# CANN {version} Release Notes\n\n"
notes += f"Release Date: {datetime.now().strftime('%Y-%m-%d')}\n\n"
notes += f"## 📦 Repositories ({len(all_changes)} total)\n\n"
for repo_name, changes in all_changes.items():
total = sum(len(v) for v in changes.values())
if total == 0:
continue # 跳过无变更仓库
notes += f"### {repo_name}\n\n"
# 新功能
if changes["features"]:
notes += "#### ✨ New Features\n\n"
for c in changes["features"]:
pr_link = f"(#{c['pr']})" if c['pr'] else ""
notes += f"- {c['text']} {pr_link}\n"
notes += "\n"
# Bug 修复
if changes["bug_fixes"]:
notes += "#### 🐛 Bug Fixes\n\n"
for c in changes["bug_fixes"]:
pr_link = f"(#{c['pr']})" if c['pr'] else ""
notes += f"- {c['text']} {pr_link}\n"
notes += "\n"
# 性能优化
if changes["performance"]:
notes += "#### ⚡ Performance\n\n"
for c in changes["performance"]:
notes += f"- {c['text']}\n"
notes += "\n"
# 破坏性变更
if changes["breaking"]:
notes += "#### ⚠️ Breaking Changes\n\n"
for c in changes["breaking"]:
notes += f"- **BREAKING**: {c['text']}\n"
notes += "\n"
# 升级指南
notes += self._upgrade_guide()
return notes
def _upgrade_guide(self) -> str:
"""升级指南"""
return """
## 📖 Upgrade Guide
Upgrading from CANN 8.0.1 to 8.0.2:
```bash
# 1. Update ascend-toolkit
sudo spack install ascend-toolkit@8.0.2
# 2. Update all CANN repos
spack install cann-stack@8.0.2 --reuse
# 3. Verify
python -c "import torch_npu; assert torch_npu.cann_version == '8.0.2'"
See Migration Guide for detailed API changes.
“”"
def announce_release(self, release_notes_md: str):
"""
发布公告(GitHub Releases + Slack/Discord + 邮件列表)
"""
# 1. 自动创建 55 个仓库的 GitHub Release
for repo_name in self.repo_versions:
self._create_github_release(repo_name, release_notes_md)
# 2. 更新主页 RELEASE_NOTES.md
with open("RELEASE_NOTES.md", "w") as f:
f.write(release_notes_md)
# 3. Slack/Discord 通知(通过 community 仓库的 webhook)
self._notify_community(release_notes_md)
print(f"📢 Release {self.repo_versions} announced!")
def _fetch_closed_issues_prs(self, repo_name):
"""拉取 GitHub closed Issues/PRs(通过 API)"""
# 由 GitHub API 拉取 (canned)
return []
def _parse_changelog(self, repo_name, version):
"""解析 CHANGELOG.md"""
return []
def _get_previous_tag(self, repo_name):
"""获取上一版本的 git tag"""
return f"v{self.repo_versions[repo_name]}"
=== 用法 ===
manager = CANNReleaseManager(repos_root=“/home/user/cann-repos”)
查看 2025 年 6 月的发布计划
schedule = manager.get_release_schedule(month=6, year=2025)
{‘feature_freeze’: ‘2025-06-12’, ‘rc1_tag’: ‘2025-06-17’, …}
发布 CANN 8.0.3
release = ReleaseVersion(8, 0, 3)
Step 1: Feature Freeze
manager.freeze_features(release)
Step 2: Tag RC1
manager.tag_release_candidate(ReleaseVersion(8, 0, 3, “rc1”))
Step 3: Generate Release Notes
notes = manager.generate_release_notes(release)
with open(f"RELEASE_NOTES_{release}.md", “w”) as f:
f.write(notes)
Step 4: Announce
manager.announce_release(notes)
Step 5: Bump version for next release
for repo_name in manager.repo_versions:
manager.repo_versions[repo_name] = release.bump_patch()
## LTS(长期支持)与滚动发布策略
```python
# release-management/lts_policy.py
class LTSPolicy:
"""
LTS(Long Term Support)策略
CANN 有两种发布轨:
1. LTS: 每 12 个月发布一个 LTS 版本,18 个月安全支持
2. Rolling: 每月发布滚动版本(latest features,无 LTS 保证)
"""
LTS_VERSIONS = {
"8.0": {
"release_date": "2024-10-15",
"eol_date": "2026-04-15", # 18 个月后 end of life
"security_support_until": "2026-04-15",
"bug_fix_until": "2025-10-15", # 12 个月后停止 bug fix
},
"8.5": {
"release_date": "2025-06-15",
"eol_date": "2026-12-15",
"security_support_until": "2026-12-15",
"bug_fix_until": "2026-06-15",
},
}
# 当前活跃(仍安全支持)的 LTS 版本
ACTIVE_LTS = ["8.5"] # 8.0 LTS 在 2025-10 停止 bug fix
@classmethod
def is_still_supported(cls, version_str):
"""
检查此版本是否仍在支持周期内
"""
major_minor = version_str[:3] # "8.5"
lts_info = cls.LTS_VERSIONS.get(major_minor)
if not lts_info:
return False
from datetime import date
today = date.today()
eol = date.fromisoformat(lts_info["eol_date"])
return today < eol
@classmethod
def get_upgrade_path(cls, current_version):
"""
升级路径(当前版本 → 最新 LTS 版本)
"""
return {
"8.0": "8.5",
"8.5": "9.0", # 将来
}.get(current_version[:3], "unknown")
@classmethod
def recommend_version(cls, requirements):
"""
根据用户需求推荐版本
requirements: {"stability": "high", "features": ["moe", "fp8"]}
"""
if requirements.get("stability") == "high":
# 生产环境 → LTS
return "8.5.2"
else:
# 新功能 → 滚动版
return "8.6.0"
# 版本支持矩阵渲染 → website
def generate_support_matrix():
import pandas as pd
data = []
for version, info in LTSPolicy.LTS_VERSIONS.items():
data.append({
"Version": version,
"Release": info["release_date"],
"EOL": info["eol_date"],
"Bug Fixes Until": info["bug_fix_until"],
"Security Patches Until": info["security_support_until"],
"Status": "✅ Active" if version in LTSPolicy.ACTIVE_LTS else "❌ EOL"
})
df = pd.DataFrame(data)
df.to_markdown("docs/support_matrix.md", index=False)
print("Support matrix generated")
踩坑一:Release Manager 同时 tag 55 个仓库→部分仓库构建失败(其依赖还未 tag)→版本不匹配
# ❌ 所有 55 个仓库同时 tag(依赖未就绪)
# ops-transformer v8.0.3 → 依赖 opbase v8.0.3(但 opbase 还未 tag!)
# CI:ops-transformer build 等待 opbase@8.0.3 → 找不到 → 失败
# ✅ 按依赖图顺序 tag(拓扑排序)
# 先 tag 底层仓库(opbase, driver, runtime)
# 再 tag 中层仓库(ops-nn, hccl, ge → 依赖底层)
# 最后 tag 顶层仓库(ops-transformer, ATB, torchtitan-npu → 依赖中层)
class ReleaseOrderScheduler:
"""
Release tag 拓扑排序
"""
# 依赖图(有向无环图 DAG)
DEP_GRAPH = {
"ascend-toolkit": [], # 最底层
"driver": ["ascend-toolkit"],
"runtime": ["ascend-toolkit"],
"opbase": ["ascend-toolkit", "driver"],
"ops-nn": ["opbase"],
"ops-transformer": ["ops-nn", "opbase"],
"hccl": ["ascend-toolkit", "driver"],
"ge": ["ascend-toolkit", "opbase"],
"ascend-transformer-boost": ["ops-transformer", "hccl", "ge"],
"torchtitan-npu": ["ascend-transformer-boost", "hccl"],
}
def get_tag_order(self):
"""拓扑排序 → tag 顺序"""
from collections import deque
in_degree = {name: 0 for name in self.DEP_GRAPH}
for name, deps in self.DEP_GRAPH.items():
for dep in deps:
in_degree[name] += 1
# Kahn 算法
queue = deque([name for name, deg in in_degree.items() if deg == 0])
order = []
while queue:
name = queue.popleft()
order.append(name)
# 更新依赖此仓库的其他仓库
for other, deps in self.DEP_GRAPH.items():
if name in deps:
in_degree[other] -= 1
if in_degree[other] == 0:
queue.append(other)
return order
# 输出示例:
# Tag order:
# 1. ascend-toolkit
# 2. driver, opbase (独立路径:driver←ascend-toolkit, opbase←ascend-toolkit)
# 3. runtime (depends on ascend-toolkit)
# 4. ops-nn + hccl (depends on opbase / driver)
# 5. ops-transformer (depends on ops-nn + opbase)
# 6. ascend-transformer-boost (depends on ops-transformer + hccl)
# 7. torchtitan-npu (depends on ascend-transformer-boost + hccl)
踩坑二:Release Notes 从 CHANGELOG.md 自动生成 → CHANGELOG 不一致(多 contributor 格式混乱)
<!-- ❌ 多 contributor 的 CHANGELOG.md 格式混乱 -->
## v8.0.3
- fixed a bug with memory leak
- feat: add new FlashAttention backward kernel
- performance improvement for MatMul
- docs: update README
<!-- 问题:格式不统一(有的带前缀,有的没有;有的带 PR number,有的没有) -->
<!-- ✅ 规范化 CHANGELOG(keep a changelog 规范) -->
# release-management/changelog_validator.py
#
# CI verify CHANGELOG format(keep a changelog 规范)
class CHANGELOGValidator:
"""
验证 CHANGELOG.md 格式
规范:keep a changelog (https://keepachangelog.com) + Conventional Commits
"""
ALLOWED_SECTIONS = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
def validate(self, changelog_path):
lines = open(changelog_path).readlines()
errors = []
for i, line in enumerate(lines, 1):
# 只检查 "--" 开头的条目
if not line.strip().startswith("- "):
continue
# Check: 必须有 PR number (#数字)
if "#" not in line:
errors.append(f"Line {i}: missing PR number: {line.strip()[:80]}")
# Check: 必须在正确的 section 下
section = self._get_current_section(lines, i)
if section not in self.ALLOWED_SECTIONS:
errors.append(f"Line {i}: invalid section '{section}'")
if errors:
for err in errors:
print(f"❌ {err}")
raise RuntimeError(f"CHANGELOG format check failed: {len(errors)} error(s)")
print("✅ CHANGELOG format: PASS")
def _get_current_section(self, lines, line_no):
"""找到条目所在 section"""
for j in range(line_no - 1, -1, -1):
if lines[j].startswith("## "):
return lines[j].replace("## ", "").strip()
return "Unknown"
# CI: 每个 PR 必须通过 CHANGELOG 格式检查
# .github/workflows/changelog-check.yml
# - name: Check CHANGELOG format
# run: python release-management/changelog_validator.py
踩坑三:LTS 版本 8.0 已 EOL(2025-10),但 30% 用户仍在使用——升级后 API 破坏(升级 8.0→8.5 破坏旧算子名称)
# ❌ 强行 EOL → 用户被迫升级 → API 破坏 → 用户流失
# ✅ 迁移自动化(deprecation script + API 映射)
# 提供自动升级脚本(scan + rewrite 旧 API→新 API)
import re
API_MIGRATION_MAP = {
# 旧 API (CANN 8.0) → 新 API (CANN 8.5)
r"aclmdlSetDatasetTensorDesc": "aclmdlDestroyDataset", # 重命名
r"atc --soc_version=Ascend910B": "atc --soc_version=Ascend910", # 统一型号名
r"opbase:\s*register_op\(\"relu\"": "ops-nn: ops_nn_relu", # relu 移到 ops-nn
r"hcclAllReduce\(\s*\(\s*void\s*\*\)\s*": "hcclAllReduce_fp16\(", # 类型安全包装
# 函数的参数变化
r"SetInput\(([^,]+),\s*([^,]+)": r"SetInputByName(\1, \2", # positional→by name
}
def auto_migrate_old_code(source_files):
"""自动迁移旧代码(扫描 + 替换旧 API→新 API)"""
import os
for filepath in source_files:
if not filepath.endswith(('.cpp', '.py', '.h', '.cmake')):
continue
with open(filepath, 'r') as f:
content = f.read()
original = content
for old_pattern, new_pattern in API_MIGRATION_MAP.items():
content = re.sub(old_pattern, new_pattern, content)
if content != original:
with open(filepath + ".migrated", 'w') as f:
f.write(content)
print(f"🔄 Migrated {filepath}: {content.count(new_pattern)} replacement(s)")
# 写迁移日志
with open("migration.log", 'a') as log:
log.write(f"{filepath}: {sum(1 for p in API_MIGRATION_MAP if p in original)} APIs changed\n")
# 运行:auto_migrate_old_code(["src/", "include/", "CMakeLists.txt"])
release-management 定义 CANN 的版本治理体系。语义化版本规范(SemVer 8.0.2-rc1+ascend910)→ 月定期发布(第 2 周 freeze/第 3 周 tag stable)→ Release Manager(自动协调 55 仓统一发布 + Release Notes 从 CHANGELOG+git log+closed Issues/PRs 自动生成)→ LTS 策略(每 12 月一个 LTS + 18 月安全支持 + 滚动版)→ 支持矩阵。三个踩坑:55 仓同时 tag 依赖未就绪→拓扑排序 tag 顺序、CHANGELOG 格式混乱→CI validator 强制 keepachangelog、EOL 用户 30%→自动迁移脚本旧 API→新 API。
更多推荐




所有评论(0)