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。

Logo

1331

更多推荐