Skip to content
Back to skills

Train Infer Op Diff Scanner

ASecurity

RL 强化学习训练与推理(rollout)路径的算子差异性扫描。当用户提供训练启动脚本路径,要求对比 Megatron 训练路径与 vLLM 推理路径的算子差异(尤其是融合算子 vs 单算子不一致)时使用此 skill。典型触发词包括"训推算子扫描"、"训推差异性"、"算子差异报告"、"融合算子对比"、"train vs infer op diff"。注意:本 skill 通过运行完整 RL 训练脚本(集成 profiling)来采集真实运行时算子(非静态源码扫描,非独立分离运行)。当训练、推理使用不同脚本时禁止触发。

  • 32 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 23, 2026
databasespythongobashsqlapidatabase

Works with

  • cli
  • api

Security analysis

A93/100
  • highPerforms destructive filesystem operations

Pro scans all 3 files and shows the line behind each finding

Scanned September 23, 2026

npx -y skills add kali20gakki/msAgent --skill train-infer-op-diff-scanner --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Train Infer Op Diff Scanner?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Train Infer Op Diff Scanner
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/kali20gakki-train-infer-op-diff-scanner/badge)](https://www.skillsdirectory.com/skills/kali20gakki-train-infer-op-diff-scanner)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: train-infer-op-diff-scanner
description: RL 强化学习训练与推理(rollout)路径的算子差异性扫描。当用户提供训练启动脚本路径,要求对比 Megatron 训练路径与 vLLM 推理路径的算子差异(尤其是融合算子 vs 单算子不一致)时使用此 skill。典型触发词包括"训推算子扫描"、"训推差异性"、"算子差异报告"、"融合算子对比"、"train vs infer op diff"。注意:本 skill 通过运行完整 RL 训练脚本(集成 profiling)来采集真实运行时算子(非静态源码扫描,非独立分离运行)。当训练、推理使用不同脚本时禁止触发。
---

# Train-Infer Operator Diff Scanner(RL 训推算子一致性扫描 )

面向 Ascend NPU 的 RL 训推算子差异性**运行时扫描**技能。

## 核心原则

1. **必须运行完整 RL 脚本**:算子采集必须通过运行完整 RL 训练脚本(集成 verl profiler)完成。**禁止**编写独立脚本分离运行 Megatron 训练和 vLLM 推理来分别采集算子数据
2. **运行时采集(非静态扫描)**:所有算子信息必须来自真实 NPU 运行时 profiling,静态源码扫描结果不作为最终结论
3. **profiler 集成到 RL 脚本**:通过 `global_profiler` + 各组件 profiler 配置在 RL 脚本中开启 profiling(训练路径用 e2e profiler,推理路径用 `discrete=True` 分离采集)
4. **代码栈贴入报告**:每个差异算子需附对应的代码调用栈(从 profiler DB 的 MSTX/CPU callstack 提取或从源码路径标注)

## 触发条件

- 对比 RL 训练(Megatron)和推理(vLLM)的算子差异
- 扫描训练和推理路径中的融合算子差异
- 生成训推算子一致性报告
- 用户提供了 RL 训练启动脚本路径

## 不适用场景

- 运行时 dump 数据的逐值对比(应使用 `rl-consistency-analysis` skill)
- 纯性能 profiling 分析(应使用 msprof-analyze-cli skill)
- 无法执行脚本或 NPU 不可用的环境

---

## 工作流程(5 阶段)

### 阶段 1:环境与配置解析(预计 2-3 分钟)

#### 1.1 解析训练脚本

**必须先读取用户提供的 RL 脚本**,提取以下配置项:

| 解析项 | 脚本字段 | 示例 |
|--------|---------|-----|
| 训练后端 | `actor_rollout_ref.actor.strategy` | `megatron` |
| 推理引擎 | `actor_rollout_ref.rollout.name` | `vllm` |
| 模型路径 | `MODEL_PATH` 或 `actor_rollout_ref.model.path` | `/workspace/models/Qwen3-0.6B` |
| 模型 ID | `MODEL_ID` | `Qwen/Qwen3-0.6B` |
| TP/PP/EP | `tensor_model_parallel_size` 等 | `1` |
| max_prompt_length | `data.max_prompt_length` | `512` |
| max_response_length | `data.max_response_length` | `1` |
| train_batch_size | `data.train_batch_size` | `4` |
| total_training_steps | `trainer.total_training_steps` | `1` |
| NPU 设备 | `ASCEND_RT_VISIBLE_DEVICES` | `1` |
| rollout 配置 | `enforce_eager`, `calculate_log_probs`, `n` | `True`, `True`, `2` |

#### 1.2 环境检查(必须全部通过,失败则告知用户并停止)

```bash
python -c "import torch; import torch_npu; print(torch.npu.device_count(), 'NPUs available')"
which msprof
pip show megatron-core vllm vllm-ascend mbridge 2>/dev/null | grep -E "^(Name|Version):"
ls -la ${MODEL_PATH}/config.json
```

#### 1.3 备份原始脚本

```bash
cp <script_path> <script_path>.bak_$(date +%Y%m%d_%H%M)
```

---

### 阶段 2:集成 Profiler 到 RL 脚本(预计 2-3 分钟)

**目的**:修改 RL 训练脚本,嵌入 profiling 配置,使单次运行即可同时采集训练和推理路径的 NPU 算子。

#### 2.1 检查脚本是否已含 profiler 配置

搜索脚本中是否存在 `global_profiler` 或 `PROFILER` 相关配置块。若已存在则跳过 2.2。

#### 2.2 在脚本中添加 Profiler 配置块

在脚本中 `launch` 命令之前插入以下配置,(参考 `references/run_qwen3_0_6b_megatron_vllm_ascend.sh` 的实际实现):

```bash
PROFILER_OUTPUT_DIR=${PROFILER_OUTPUT_DIR:-<脚本目录>/profiler_output}
rm -rf ${PROFILER_OUTPUT_DIR} 2>/dev/null || true
mkdir -p ${PROFILER_OUTPUT_DIR}

# 关键:训练路径用 e2e profiler,推理路径用 discrete=True 分离采集
PROFILER=(
    global_profiler.tool=npu
    global_profiler.steps=[1]
    global_profiler.save_path=${PROFILER_OUTPUT_DIR}
    actor_rollout_ref.actor.profiler.enable=True
    actor_rollout_ref.actor.profiler.tool_config.npu.contents="['npu','cpu','shapes']"
    actor_rollout_ref.actor.profiler.tool_config.npu.level=level1
    actor_rollout_ref.ref.profiler.enable=True
    actor_rollout_ref.ref.profiler.tool_config.npu.contents="['npu','cpu','shapes']"
    actor_rollout_ref.ref.profiler.tool_config.npu.level=level1
    actor_rollout_ref.rollout.profiler.enable=True
    actor_rollout_ref.rollout.profiler.tool=npu
    actor_rollout_ref.rollout.profiler.tool_config.npu.contents="['npu','cpu','shapes']"
    actor_rollout_ref.rollout.profiler.tool_config.npu.level=level1
    actor_rollout_ref.rollout.profiler.tool_config.npu.discrete=True
)
```

在 launch 命令参数末尾追加 `"${PROFILER[@]}"`。

#### 2.3 配置说明

| 配置项 | 说明 |
|--------|------|
| `global_profiler.tool=npu` | 全局开启 NPU profiling |
| `global_profiler.steps=[1]` | 仅采集 step 1(节省时间) |
| `actor.profiler.enable=True` | 训练 actor 路径采集 |
| `ref.profiler.enable=True` | 训练 ref 路径采集 |
| `rollout.profiler.enable=True` | 推理 rollout 路径采集 |
| `rollout.profiler.tool_config.npu.discrete=True` | **关键**:触发 vLLM 侧独立 `torch_npu.profiler` 采集,产出 raw `*_ascend_pt` 目录(**非 DB**),须事后 `analyse()` 才落 DB(见阶段 3.3) |
| `level=level1` | 采集 CANN 算子级别数据 |

#### 2.4 输出路径约定(训练/推理采集后端不同,产物形态不同)

运行完成后,profiler 输出目录结构如下。**关键:训练与推理两条路径的 profiler 后端不同,产物形态也不同**:

```
profiler_output/
├── e2e/                                              # 训练路径(Megatron actor+ref)
│   └── <hostname>_<pid>_<ts>_ascend_pt/
│       └── ASCEND_PROFILER_OUTPUT/
│           └── ascend_pytorch_profiler_0.db          # ← 训练算子(msprof,已自动解析)
└── agent_loop_rollout_replica_0/                     # 推理路径(vLLM rollout)
    └── <hostname>_<pid>_<ts>_ascend_pt/              # ← vllm-ascend torch_npu profiler 原始目录
        └── (需执行 analyse() 离线解析后才生成 ASCEND_PROFILER_OUTPUT/*.db)
```

**机制差异(决定后续流程)**:

| 路径 | profiler 后端 | export_type | 是否自动解析 | 产物 |
|------|--------------|-------------|:---:|------|
| 训练(e2e) | CANN msprof(verl) | Db | ✅ 自动 | 直接生成 `ascend_pytorch_profiler_0.db` |
| 推理(discrete=True) | `torch_npu.profiler`(vllm-ascend) | Text | ❌ 需手动 | 只落 `*_ascend_pt` 目录,须 `analyse()` 后才有 DB |

> 推理侧的 `discrete=True` 由 vLLM 引擎触发 profiler,走 `vllm_ascend/worker/worker.py` 的 `_init_profiler()`(`export_type=ExportType.Text`),不会像训练侧那样自动解析出 DB。

---

### 阶段 3:运行 RL 脚本采集算子数据(预计 5-15 分钟)

#### 3.1 运行完整 RL 训练脚本

```bash
cd <脚本所在目录>
bash <脚本名> 2>&1 | tee profiler_output/run.log
```

**注意**:必须等待脚本完整执行完毕。此脚本同时运行 Megatron 训练和 vLLM rollout,运行时间取决于模型大小(0.6B 模型约 5-10 分钟)。

#### 3.2 验证采集产物

**训练路径**(已自动解析为 DB):
```bash
find profiler_output/e2e -name "ascend_pytorch_profiler_0.db" -type f
```
确认存在 `profiler_output/e2e/*/ASCEND_PROFILER_OUTPUT/ascend_pytorch_profiler_0.db`。

**推理路径**(原始目录,尚未解析):
```bash
find profiler_output/agent_loop_rollout_replica_0 -maxdepth 2 -type d -name "*_ascend_pt"
```
确认存在 `profiler_output/agent_loop_rollout_replica_0/<hostname>_<pid>_<ts>_ascend_pt/` 目录(这是 vllm-ascend `torch_npu.profiler` 的原始输出,**不是 DB**,目录内含 `FRAMEWORK/torch.op_range` 与 `PROF_*/host/data`)。

#### 3.3 对推理侧 ascend_pt 目录执行离线解析(关键步骤,不可跳过)

推理侧 profiler 走 vLLM-Ascend 的 `torch_npu.profiler`(`export_type=Text`),**默认不自动解析**。必须手动调用 `analyse()` 生成 DB,否则推理侧拿不到 device 算子:

```bash
INFER_PT_DIR=$(find profiler_output/agent_loop_rollout_replica_0 -maxdepth 2 -type d -name "*_ascend_pt" | head -1)
python3 -c "
from torch_npu.profiler.profiler import analyse
analyse('${INFER_PT_DIR}')
"
```

解析完成后,推理侧会生成 `ASCEND_PROFILER_OUTPUT/ascend_pytorch_profiler_0.db`(含 `COMPUTE_TASK_INFO` 表 = device 侧 AI Core 真实执行算子),同时 text 模式还会额外产出 `api_statistic.csv` / `operator_details.csv` / `trace_view.json`(host 侧备选)。验证 DB 已生成:

```bash
find profiler_output/agent_loop_rollout_replica_0 -name "ascend_pytorch_profiler_0.db" -type f
```

> **机制说明(关键)**:`analyse()` 不带 `export_type` 时,会从 `profiler_info_*.json` 读取采集时的 `_export_type`。由于 vllm-ascend 采集时是 `["text"]`,本应只走 text 解析;但 **CANN 8.5.0 的 msprof 支持 `text(which will also export the database)` 默认行为**(`torch_npu` 内部 `CannPackageManager.is_support_default_export_db()` 检测),因此会自动补上 db parser,从而同时产出 DB 与 CSV。**若某 CANN 版本不支持该默认行为**(`msprof --help` 输出无 `text(which will also export the database)`),则须显式传 `export_type=['db']`:
>
> ```bash
> python3 -c "
> from torch_npu.profiler.profiler import analyse
> analyse('${INFER_PT_DIR}', export_type=['db'])
> "
> ```
>
> **告警说明**:analyse 过程中出现的 `Failed to get acl to npu flow events`、`Failed to get task data from db`、`no such table: TASK` 等告警是 `export_type=Text` 模式的正常现象(推理侧 msprof 格式的 device 二进制 `device_*/data/` 为空),**不影响**最终 `COMPUTE_TASK_INFO` 中 device 算子的解析——真实算子来自 host 侧 CANN api_event 记录(`CANN_API` 表),analyse 会将其还原为 device 侧算子。

#### 3.4 验证推理侧 device 算子已落地(证据闭环)

解析完成后,必须用 SQL 确认 `COMPUTE_TASK_INFO` 表非空(该表 = device 侧 AI Core 真实执行算子),**不能只确认 DB 文件存在**:

```bash
python3 -c "
import sqlite3, glob
dbs = glob.glob('profiler_output/agent_loop_rollout_replica_0/*/ASCEND_PROFILER_OUTPUT/ascend_pytorch_profiler_0.db')
assert dbs, '推理侧 DB 缺失'
c = sqlite3.connect(dbs[0])
n = c.execute('SELECT COUNT(*) FROM COMPUTE_TASK_INFO').fetchone()[0]
print('COMPUTE_TASK_INFO rows =', n)
"
```

- ✅ `n > 0`:推理侧 device 算子已落地,进入阶段 4。
- ❌ DB 缺失或 `n == 0`:按以下顺序兜底:

| 兜底 | 触发条件 | 操作 |
|------|---------|------|
| 1. 显式导出 DB | `analyse()` 后无 `ascend_pytorch_profiler_0.db`(CANN 不支持默认 `text` 同时导 db) | `analyse('${INFER_PT_DIR}', export_type=['db'])` 后重新验证 |
| 2. host 侧 CSV 备选 | DB 存在但 `COMPUTE_TASK_INFO` 为空 | 改用 `ASCEND_PROFILER_OUTPUT/api_statistic.csv`(或 `operator_details.csv`)中的 CANN 算子名统计,报告中标注「host 侧备选,非 device 侧」 |
| 3. 终止扫描 | 1、2 兜底操作失败 | 跳过阶段 4、5,直接输出报告,记录已进行的操作与结果 |


### 阶段 4:算子提取与对比分析(预计 3-5 分钟)

#### 4.1 从 Profiler DB 提取算子列表

使用 `ascend_pytorch_profiler_db_explorer` skill 或直接 SQL 查询 DB。

**重要**:`COMPUTE_TASK_INFO` 表中算子名字段是 `name`(INTEGER 外键),真实算子名字符串在 `STRING_IDS` 字典表中,必须 JOIN 查询,不能直接查 `task_name`(该列不存在)。

**训练路径算子提取**(从 `profiler_output/e2e/*/ASCEND_PROFILER_OUTPUT/ascend_pytorch_profiler_0.db`):

```sql
-- 提取训练路径所有唯一 CANN 算子名及调用次数(device 侧真实执行算子)
SELECT s.value AS task_name, COUNT(*) AS cnt
FROM COMPUTE_TASK_INFO t
JOIN STRING_IDS s ON t.name = s.id
GROUP BY s.value
ORDER BY cnt DESC;
```

**推理路径算子提取**(从 `profiler_output/agent_loop_rollout_replica_0/*/ASCEND_PROFILER_OUTPUT/ascend_pytorch_profiler_0.db`,即 3.3 解析后产物):

```sql
-- 同上,提取推理路径所有唯一 CANN 算子(device 侧真实执行算子)
SELECT s.value AS task_name, COUNT(*) AS cnt
FROM COMPUTE_TASK_INFO t
JOIN STRING_IDS s ON t.name = s.id
GROUP BY s.value
ORDER BY cnt DESC;
```

将结果分别保存为:
- `profiler_output/megatron_runtime_ops.json`
- `profiler_output/vllm_runtime_ops.json`

#### 4.2 构建算子全量对比表

按 Transformer 层结构逐模块对比,生成标准对比表:

| 维度 | 训练路径 (Megatron) | 推理路径 (vLLM) | 差异类型 | 差异等级 |
|------|---------------------|-----------------|----------|:--------:|
| 注意力 | 列出运行时算子(如 `FlashAttentionScore`) | 列出运行时算子(如 `FusedInferAttentionScore`) | 算子不同 / 融合vs非融合 | 🔴 |
| 残差+归一化 | `Add` + `RmsNorm` | `AddRmsNormBias` | 融合程度不同 | 🟠 |
| 激活函数 | `Swish`(独立 SiLU) | `SwiGlu`(SiLU+Gate 融合) | 融合程度不同 | 🟠 |
| RoPE | `Sin` + `Cos`(独立) | `_triton_rope`(融合) | 后端实现不同 | 🟠 |
| MatMul(线性层) | `MatMulV2` | `MatMulV2` | 相同 | 🟢 |
| KV Cache | 无 | `ReshapeAndCacheNdKernel` | 推理独有 | 🟡 |
| 反向传播 | `RmsNormGrad`, `FlashAttentionScoreGrad` 等 | 无 | 训练独有 | 🟡 |

#### 4.3 差异等级判定规则

| 标记 | 颜色 | 判定条件 | 精度风险 |
|:----:|:----:|---------|:--------:|
| 🔴 | 红 | 一方使用融合 kernel,另一方使用多个独立 kernel(如 Attention:分离 MatMul+Softmax+MatMul vs 单 kernel FusedInferAttentionScore) | 高 |
| 🟠 | 橙 | 同类操作但融合程度不同(如 Add+RMSNorm vs AddRmsNormBias),或不同后端实现同一数学运算(如 NPU RoPE vs Triton RoPE) | 中 |
| 🟡 | 黄 | 实现方式不同但数学语义等价,或仅单侧存在且另一侧有等价替代 | 低 |
| 🟢 | 绿 | 相同基本算子,或训练/推理单侧独占且无等价替代 | 极低 |

#### 4.4 提取代码调用栈(关键)

对每个 🔴 和 🟠 差异算子,从以下源码路径标注代码调用栈:

| 算子类别 | 训练路径关键源码 | 推理路径关键源码 |
|---------|----------------|----------------|
| Attention | `megatron/core/transformer/dot_product_attention.py` → `FlashAttentionScore` | `vllm_ascend/attention/` → `FusedInferAttentionScore` |
| RMSNorm | `megatron/core/transformer/torch_norm.py` | `vllm_ascend/layers/layernorm.py` → `AscendRMSNorm` |
| SwiGLU | `megatron/core/transformer/mlp.py` | `vllm_ascend/layers/activation.py` → `AscendSiluAndMul` |
| RoPE | `megatron/core/transformer/attention.py` → `Sin`/`Cos` | `vllm_ascend/layers/rotary_embedding.py` → `_triton_rope` |

**提取方式**(优先使用 MSTX 标记):
1. 查询 DB 中 `MSTX` 表获取算子与 Python 调用栈的对应关系
2. 若 MSTX 不可用,则标注算子对应的源码文件路径和关键函数名
3. 在报告中以 call stack 形式展示:`上层调用路径 → 算子类/函数 → CANN 算子名`

---

### 阶段 5:生成最终产物(预计 3-5 分钟)

必须生成以下 **3 个文件**:

#### 产物 1:完整报告 (Markdown) → `<工作目录>/train_infer_op_diff_report.md`

**必须包含**以下章节(参考 `references/report_template.md`):

1. **标题 + 元信息**(模型名、配置、脚本路径、采集方式、生成时间)
2. **算子证据来源**(表格:路径、数据源、DB 文件、算子数)
3. **训练路径算子全量列表**(运行时采集的 CANN 算子 TOP30+,含调用次数和用途分类)
4. **推理路径算子全量列表**(同上)
5. **核心差异:训练 vs 推理算子对比表**(含差异类型和 🔴🟠🟡🟢 等级标记)
6. **代码调用栈**(每个 🔴🟠 差异算子附源码路径和调用链)
7. **结论与建议**(P0-P3 优先级,含具体操作)

格式要求:
- 使用 HTML 颜色标记差异等级:`<span style="color:red">🔴 高差异</span>`、`<span style="color:orange">🟠 中差异</span>`、`<span style="color:gold">🟡 低差异</span>`、`<span style="color:green">🟢 无差异</span>`
- 代码调用栈使用折叠块 `<details><summary>调用栈详情</summary>...</details>`

#### 产物 2:算子对比表 (CSV UTF-8 BOM) → `<工作目录>/operator_diff_table.csv`

```csv
操作,训练算子,推理算子,训练调用次数,推理调用次数,差异类型,差异等级,训练代码路径,推理代码路径
注意力,FlashAttentionScore,FusedInferAttentionScore,672,224,算子不同,🔴,megatron/core/transformer/dot_product_attention.py,vllm_ascend/attention/ascend_attention.py
残差+归一化,Add(6484)+RmsNorm(2712),AddRmsNormBias(448),6484+2712,448,融合vs非融合,🟠,megatron/core/transformer/transformer_layer.py,vllm_ascend/layers/layernorm.py
```

**保存格式**:文件开头写入 UTF-8 BOM `\xef\xbb\xbf`


---

## 执行顺序约束

```
阶段 1 (配置解析 + 备份脚本)
  └─→ 阶段 2 (集成 Profiler 到脚本)
        └─→ 阶段 3 (运行 RL 脚本采集算子)  ← 最耗时
              └─→ 阶段 4 (提取算子 + 对比分析)
                    └─→ 阶段 5 (生成报告)
```

- 各阶段严格串行,不可跳过
- 阶段 3 运行期间需等待脚本执行完成
- 每个阶段完成后立即整理中间结果,避免丢失

---

## 时间控制

| 阶段   | 预计耗时 | 说明 |
|------|---------|------|
| 阶段 1 | 2-3 min | 纯文本解析 + 环境检查 |
| 阶段 2 | 2-3 min | 修改脚本(文本操作) |
| 阶段 3 | 5-15 min | 运行 RL 脚本(取决于模型大小) |
| 阶段 4 | 3-5 min | SQL 查询 + 对比分析 |
| 阶段 5 | 3-5 min | 生成报告和 CSV |

---

## 重要约束

1. **绝对禁止独立分离运行**:不允许编写独立脚本分别运行 Megatron 训练和 vLLM 推理来采集算子数据
2. **绝对禁止跳过运行时采集**:不允许仅凭源码静态扫描生成最终对比表
3. **备份优先**:修改脚本前必须先备份
4. **证据闭环**:每个结论必须附 profiler DB 提取的运行时算子证据

## 注意事项

- 脚本运行时间可能较长(0.6B 模型约 5-10 分钟),需在开始前告知用户预计等待时间
- 如果 profiler DB 表结构不同(如 `ge_task_merge` vs `ge_summary`),需先查询 schema
- **推理侧必须先 `analyse()` 离线解析**:推理 profiler 产物是 `*_ascend_pt` 目录(vllm-ascend `torch_npu.profiler`,`export_type=Text`),不解析就没有 DB。`device_*/data` 为空(无 `ffts_profile.data`/`stars_soc.data`)是该模式的正常现象,**不影响** device 算子提取——`analyse()` 会从 host 侧 `CANN_API`(api_event 记录)还原出 `COMPUTE_TASK_INFO`(device 侧算子);仅在 `COMPUTE_TASK_INFO` 仍为空时才需要走 3.4 的兜底(`export_type=['db']` 或 `api_statistic.csv`)
- CSV 文件必须使用 UTF-8 with BOM 编码(`\xef\xbb\xbf` 文件头)

## References

- `references/report_template.md` — 报告模板
- `references/run_qwen3_0_6b_megatron_vllm_ascend.sh` — Qwen3-0.6B GRPO 训练脚本

Files in this skill

  • SKILL.md19.4 KB
  • references/report_template.md5.6 KB
  • references/run_qwen3_0_6b_megatron_vllm_ascend.sh5.1 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in databases →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.