# Step 级并行架构迁移指南

**版本**: v1.0.0 → v2.0.0
**日期**: 2025-11-04

本指南帮助你将现有代码从旧架构（实验级/特征级并行）迁移到新架构（Step 级并行）。

---

## 快速迁移（用户代码）

### 旧代码

```python
from infra.catalog import UnifiedExperimentManager

manager = UnifiedExperimentManager('catalog_config.yaml')
experiments = manager.search(chip_id="#20250804008")

# 旧方式：实验级并行
result = manager.batch_extract_features_v2(
    experiments=experiments,
    feature_config='v2_transfer_basic',
    n_workers=48  # 实验级并行
)
```

### 新代码

```python
from infra.catalog import UnifiedExperimentManager

manager = UnifiedExperimentManager('catalog_config.yaml')
experiments = manager.search(chip_id="#20250804008")

# 新方式：Step 级并行（只需修改一个参数）
result = manager.batch_extract_features_v2(
    experiments=experiments,
    feature_config='v2_transfer_basic',
    execution_mode='step',  # ← 添加这一行
    n_workers=47  # ← 改为 47（配合 1 consumer）
)
```

**变化**：
- ✅ 添加 `execution_mode='step'`
- ✅ 调整 `n_workers=47`（配合 1 consumer = 48核）
- ✅ 其他参数保持不变

**好处**：
- ⚡ 性能提升 2-10x（取决于特征）
- 🎯 内存占用降低 75%
- ✅ 结果完全一致

---

## 提取器迁移（开发者）

### 旧提取器模式

```python
@register('my.feature')
class MyFeatureExtractor(BaseExtractor):
    def extract(self, data, params):
        """处理所有 steps"""
        data_list = data['transfer'] if isinstance(data, dict) else data

        results = []
        for step_data in data_list:  # ← 手动循环
            # 处理单 step
            value = self._process_step(step_data, params)
            results.append(value)

        return np.array(results)

    @property
    def output_shape(self):
        return ('n_steps',)  # ← 包含 n_steps
```

### 新提取器模式

```python
@register('my.feature')
class MyFeatureExtractor(BaseExtractor):
    def extract(self, data, params):
        """批量提取（通过调用 extract_single_step 实现）"""
        data_list = data['transfer'] if isinstance(data, dict) else data

        results = []
        for step_data in data_list:
            result = self.extract_single_step(step_data, params)  # ← 调用新方法
            results.append(result)

        return np.array(results)

    def extract_single_step(self, step_data, params):
        """单 step 提取（核心实现）"""
        # 只处理一个 step，无需循环
        value = self._process_step(step_data, params)
        return value

    @property
    def output_shape(self):
        return ()  # ← 不包含 n_steps（单 step 输出）
```

**变化**：
1. ✅ 添加 `extract_single_step()` 方法
2. ✅ 将原有 for 循环逻辑移到 `extract_single_step()`
3. ✅ `extract()` 调用 `extract_single_step()` 并聚合
4. ✅ `output_shape` 改为单 step 输出形状

---

## 典型迁移示例

### 示例 1: Transient Cycles（多维特征）

#### 旧实现

```python
@register('transient.cycles')
class TransientCyclesExtractor(BaseExtractor):
    def extract(self, data, params):
        transient_list = data['transient'] if isinstance(data, dict) else data
        n_cycles = params.get('n_cycles', 100)
        n_steps = len(transient_list)

        result = np.zeros((n_steps, n_cycles), dtype=np.float32)

        # ❌ 手动循环所有 steps
        for i, step_data in enumerate(transient_list):
            drain_current = step_data['drain_current']
            cycles = self._extract_by_peaks(drain_current, n_cycles, params)

            actual_cycles = min(len(cycles), n_cycles)
            result[i, :actual_cycles] = cycles[:actual_cycles]
            if actual_cycles < n_cycles:
                result[i, actual_cycles:] = np.nan

        return result

    @property
    def output_shape(self):
        n_cycles = self.params.get('n_cycles', 100)
        return ('n_steps', n_cycles)  # ← 包含 n_steps
```

#### 新实现

```python
@register('transient.cycles')
class TransientCyclesExtractor(BaseExtractor):
    def extract(self, data, params):
        """批量提取（调用 extract_single_step）"""
        transient_list = data['transient'] if isinstance(data, dict) else data
        n_cycles = params.get('n_cycles', 100)

        # ✅ 调用单 step 方法
        results = []
        for step_data in transient_list:
            cycles = self.extract_single_step(step_data, params)
            results.append(cycles)

        # ✅ 聚合
        return self._aggregate_cycles(results, n_cycles)

    def extract_single_step(self, step_data, params):
        """✅ 单 step 提取（核心实现）"""
        drain_current = step_data['drain_current']
        n_cycles = params.get('n_cycles', 100)
        method = params.get('method', 'peak_detection')

        if method == 'peak_detection':
            return self._extract_by_peaks(drain_current, n_cycles, params)
        # ...
        # 返回 (n_cycles,) 数组

    def _aggregate_cycles(self, results, n_cycles):
        """聚合助手"""
        n_steps = len(results)
        aggregated = np.zeros((n_steps, n_cycles), dtype=np.float32)

        for i, cycles in enumerate(results):
            actual_cycles = min(len(cycles), n_cycles)
            aggregated[i, :actual_cycles] = cycles[:actual_cycles]
            if actual_cycles < n_cycles:
                aggregated[i, actual_cycles:] = np.nan

        return aggregated

    @property
    def output_shape(self):
        n_cycles = self.params.get('n_cycles', 100)
        return (n_cycles,)  # ← 单 step 输出（不包含 n_steps）
```

**变化总结**：
1. ✅ `extract()` 改为调用 `extract_single_step()` 并聚合
2. ✅ `extract_single_step()` 包含原有的单 step 处理逻辑
3. ✅ 添加 `_aggregate_cycles()` 辅助方法（可选）
4. ✅ `output_shape` 改为 `(n_cycles,)` 而不是 `('n_steps', n_cycles)`

---

### 示例 2: Transfer Gm Max（标量特征）

#### 旧实现

```python
@register('transfer.gm_max')
class GmMaxExtractor(BaseExtractor):
    def extract(self, data, params):
        transfer_list = data['transfer'] if isinstance(data, dict) else data
        transfer_3d = _convert_to_3d_array(transfer_list)  # (n_steps, 2, max_points)

        device_type = params.get('device_type', 'N')
        batch_transfer = BatchTransfer(transfer_3d, device_type=device_type)

        return batch_transfer.absgm_max.forward  # (n_steps,)

    @property
    def output_shape(self):
        return ('n_steps',)  # ← 包含 n_steps
```

#### 新实现

```python
@register('transfer.gm_max')
class GmMaxExtractor(BaseExtractor):
    def extract(self, data, params):
        """批量提取（保持不变，向后兼容）"""
        transfer_list = data['transfer'] if isinstance(data, dict) else data
        transfer_3d = _convert_to_3d_array(transfer_list)

        device_type = params.get('device_type', 'N')
        batch_transfer = BatchTransfer(transfer_3d, device_type=device_type)

        return batch_transfer.absgm_max.forward

    def extract_single_step(self, step_data, params):
        """✅ 单 step 提取（新增）"""
        # step_data: {'Vg': array, 'Id': array}

        # 转换为 BatchTransfer 格式（添加 batch 维度）
        transfer_3d = _step_to_batch(step_data)  # (1, 2, n_points)

        device_type = params.get('device_type', 'N')
        batch_transfer = BatchTransfer(transfer_3d, device_type=device_type)

        return float(batch_transfer.absgm_max.forward[0])  # 返回标量

    @property
    def output_shape(self):
        return ()  # ← 标量（单 step 输出）
```

**变化总结**：
1. ✅ `extract()` 保持不变（向后兼容）
2. ✅ 添加 `extract_single_step()`（新方法）
3. ✅ `output_shape` 改为 `()` 表示标量

---

### 示例 3: AutoTau（高维特征）

#### 旧实现

```python
@register('transient.tau_on_off')
class TauOnOffExtractor(BaseExtractor):
    def extract(self, data, params):
        transient_list = data
        n_steps = len(transient_list)

        # 获取参数（一次性）
        period, sample_rate = self._get_sampling_params(params)

        all_tau_on_off = []
        max_cycles = 0

        # ❌ 手动循环所有 steps
        for i, step_data in enumerate(transient_list):
            time = step_data['continuous_time']
            current = step_data['drain_current']

            fitter = CyclesAutoTauFitter(time, current, period, sample_rate)
            fitter.fit_all_cycles(r_squared_threshold=0.99)

            summary_df = fitter.get_summary_data()
            if summary_df is not None:
                tau_on = summary_df['tau_on'].to_numpy()
                tau_off = summary_df['tau_off'].to_numpy()
                tau_on_off = np.stack([tau_on, tau_off], axis=1)
                all_tau_on_off.append(tau_on_off)
                max_cycles = max(max_cycles, len(tau_on))

        # 聚合
        result = np.full((n_steps, max_cycles, 2), np.nan)
        for i, tau in enumerate(all_tau_on_off):
            result[i, :len(tau), :] = tau

        return result

    @property
    def output_shape(self):
        return ('n_steps', 'n_cycles', 2)  # ← 包含 n_steps
```

#### 新实现

```python
@register('transient.tau_on_off')
class TauOnOffExtractor(BaseExtractor):
    def extract(self, data, params):
        """批量提取（调用 extract_single_step）"""
        transient_list = data['transient'] if isinstance(data, dict) else data

        results = []
        for step_data in transient_list:
            tau_on_off = self.extract_single_step(step_data, params)  # ← 调用
            results.append(tau_on_off)

        return self._aggregate_tau_on_off(results)

    def extract_single_step(self, step_data, params):
        """✅ 单 step 提取（核心实现）"""
        # 获取参数（每次调用都获取，支持 context）
        period, sample_rate = self._get_sampling_params(params)

        time = step_data['continuous_time']
        current = step_data['drain_current']

        fitter = CyclesAutoTauFitter(time, current, period, sample_rate)
        fitter.fit_all_cycles(r_squared_threshold=params['r_squared_threshold'])

        summary_df = fitter.get_summary_data()
        if summary_df is not None and not summary_df.empty:
            tau_on = summary_df['tau_on'].to_numpy()
            tau_off = summary_df['tau_off'].to_numpy()
            return np.stack([tau_on, tau_off], axis=1)  # (n_cycles, 2)
        else:
            return np.empty((0, 2))

    def _aggregate_tau_on_off(self, results):
        """聚合助手"""
        n_steps = len(results)
        max_cycles = max(len(r) for r in results) if results else 0

        aggregated = np.full((n_steps, max_cycles, 2), np.nan, dtype=np.float32)
        for i, tau in enumerate(results):
            if len(tau) > 0:
                aggregated[i, :len(tau), :] = tau

        return aggregated

    @property
    def output_shape(self):
        return ('n_cycles', 2)  # ← 单 step 输出（不包含 n_steps）
```

**关键变化**：
1. ✅ `_get_sampling_params()` 在 `extract_single_step()` 中调用（支持 context）
2. ✅ 移除内部并行逻辑（`use_parallel`、`executor`）
3. ✅ 添加聚合助手方法
4. ✅ `output_shape` 改为 `('n_cycles', 2)`

---

## 参数迁移

### batch_extract_features_v2() 参数变化

| 参数 | 旧架构 | 新架构 | 说明 |
|------|--------|--------|------|
| `experiments` | ✅ | ✅ | 无变化 |
| `feature_config` | ✅ | ✅ | 无变化 |
| `output_dir` | ✅ | ✅ | 无变化 |
| `save_format` | ✅ | ✅ | 无变化 |
| `n_workers` | ✅ | ✅ | 含义不同（见下） |
| `execution_mode` | ❌ | ✅ **新增** | 'step' 或 'experiment' |
| `use_parallel_executor` | ✅ | ❌ **删除** | 特征级并行已废弃 |
| `n_step_workers` | ✅ | ❌ **删除** | 由 execution_mode 控制 |
| `force_recompute` | ✅ | ✅ | 无变化 |

**`n_workers` 含义变化**：
- **旧架构**: 实验级并行度（1-48）
- **新架构（step 模式）**: 固定为 47（配合 1 consumer）
- **新架构（experiment 模式）**: 同旧架构

### 推荐配置

```python
# 96核系统
result = manager.batch_extract_features_v2(
    experiments=experiments,
    feature_config='v2_transfer_basic',
    execution_mode='step',
    n_workers=95  # 95 workers + 1 consumer = 96核
)

# 48核系统
result = manager.batch_extract_features_v2(
    experiments=experiments,
    feature_config='v2_transfer_basic',
    execution_mode='step',
    n_workers=47  # 47 workers + 1 consumer = 48核
)

# 16核系统
result = manager.batch_extract_features_v2(
    experiments=experiments,
    feature_config='v2_transfer_basic',
    execution_mode='step',
    n_workers=15  # 15 workers + 1 consumer = 16核
)
```

---

## 行为变化

### 1. 执行顺序

**旧架构**：
- 按实验顺序执行
- 每个实验内按特征顺序执行
- 可预测的执行顺序

**新架构**：
- 按依赖层级执行（L0 → L1 → L2 → ...）
- 同层内任务乱序执行（并行）
- 执行顺序不确定（但依赖正确性保证）

### 2. 内存占用

**旧架构**：
- 所有实验结果在内存中
- 最后统一保存
- 峰值内存 = n_experiments × n_features × n_steps × feature_size

**新架构**：
- 实验完成立即保存并释放
- 只保留未完成实验的缓冲区
- 峰值内存 ≈ n_incomplete_experiments × n_features × n_steps × feature_size

**示例**（80实验，假设每次最多20个实验在缓冲区）：
- 旧架构: 80 × 10 × 100 × 4B = 320MB
- 新架构: 20 × 10 × 100 × 4B = 80MB（**75% 降低**）

### 3. 错误处理

**旧架构**：
- 单个实验失败，其他实验继续
- 失败记录在 `results['failed']`

**新架构**：
- 单个任务失败，其他任务继续
- 失败的 step 填充 NaN
- 实验部分失败也会保存（包含 NaN）

---

## 性能优化建议

### 1. 核心数配置

```python
import os

# 获取 CPU 核心数
n_cores = os.cpu_count()

# 推荐配置（留一个核心给系统）
n_workers = n_cores - 1

result = manager.batch_extract_features_v2(
    execution_mode='step',
    n_workers=n_workers
)
```

### 2. 特征配置优化

```yaml
# 高性能配置示例
features:
  # ✅ Transfer 特征（快速，已向量化）
  gm_max:
    extractor: transfer.gm_max
    input: transfer

  Von:
    extractor: transfer.Von
    input: transfer

  # ✅ Transient 特征（中等，step 级并行加速明显）
  peak_current:
    extractor: transient.peak_current
    input: transient

  cycles:
    extractor: transient.cycles
    input: transient
    params:
      n_cycles: 100

  # ⚡ AutoTau 特征（慢速，step 级并行加速最明显）
  tau_on_off:
    extractor: transient.tau_on_off
    input: transient
    params:
      r_squared_threshold: 0.99
```

### 3. 批量大小

```python
# ✅ 大批量（充分利用并行）
experiments = manager.search()  # 所有实验
result = manager.batch_extract_features_v2(
    experiments=experiments,  # 80+ 实验
    execution_mode='step',
    n_workers=47
)

# ⚠️ 小批量（并行收益有限）
result = manager.batch_extract_features_v2(
    experiments=experiments[:5],  # 仅5个实验
    execution_mode='experiment',  # 建议用旧模式
    n_workers=5
)
```

---

## 常见问题

### Q1: 旧代码会受影响吗？

**A**: 不会。旧代码完全兼容：

```python
# 这段代码仍然有效（默认 execution_mode='experiment'）
result = manager.batch_extract_features_v2(
    experiments=experiments,
    feature_config='v2_transfer_basic',
    n_workers=48
)
```

### Q2: 如何选择 execution_mode？

**A**: 根据场景选择：

| 场景 | 推荐模式 | 原因 |
|------|---------|------|
| 批量处理（>10实验） | `'step'` | 最大并行度，低内存 |
| 单实验处理 | `'experiment'` | 简单快速 |
| AutoTau 特征 | `'step'` | 性能提升巨大 |
| Transfer 特征 | 两者皆可 | 性能接近 |
| 调试阶段 | `'experiment'` | 错误追踪更简单 |

### Q3: 需要修改提取器吗？

**A**: 不强制，但推荐迁移：

- **不迁移**: 仍然可以使用 step 模式，执行器会调用 `extract()` 方法
- **迁移**: 添加 `extract_single_step()`，获得更好的性能和可维护性

### Q4: 如何验证结果一致性？

**A**: 使用对比测试：

```python
# 测试 1: 旧架构
result1 = manager.batch_extract_features_v2(
    experiments=[exp],
    feature_config='v2_transfer_basic',
    execution_mode='experiment'
)

# 测试 2: 新架构
result2 = manager.batch_extract_features_v2(
    experiments=[exp],
    feature_config='v2_transfer_basic',
    execution_mode='step'
)

# 对比结果
df1 = exp.get_v2_feature_dataframe()  # 旧架构结果
df2 = exp.get_v2_feature_dataframe()  # 新架构结果（重新提取）

assert df1.equals(df2), "结果不一致！"
```

---

## 迁移检查清单

### 用户代码迁移

- [ ] 修改 `batch_extract_features_v2()` 调用，添加 `execution_mode='step'`
- [ ] 调整 `n_workers` 为合适的值（n_cores - 1）
- [ ] 测试性能提升
- [ ] 测试结果一致性
- [ ] 监控内存占用

### 提取器迁移

- [ ] 添加 `extract_single_step()` 方法
- [ ] 修改 `output_shape` 为单 step 输出形状
- [ ] 移除内部并行逻辑（joblib/multiprocessing）
- [ ] `extract()` 改为调用 `extract_single_step()`
- [ ] 添加聚合助手方法（如果需要）
- [ ] 单元测试：验证单 step 提取正确性
- [ ] 集成测试：验证批量提取一致性

---

## 性能基准

### 测试环境

- CPU: 96 核 (Intel Xeon)
- 内存: 256GB
- 数据: 80 实验，每个 5 steps
- 特征: v2_transfer_basic (5 特征) + AutoTau (1 特征)

### 测试结果

| 场景 | 旧架构（实验级，48核） | 新架构（Step级，48核） | 提升 |
|------|---------------------|-------------------|------|
| Transfer only | 15秒 | 12秒 | 1.25x |
| Transient only | 80秒 | 20秒 | 4x ⚡ |
| AutoTau only | 42秒 | 10秒 | 4.2x ⚡ |
| 混合（Transfer+AutoTau） | 57秒 | 22秒 | 2.6x |

### 扩展性测试

| 核心数 | 实验级并行 | Step级并行 | Step级提升 |
|-------|-----------|-----------|----------|
| 8核   | 210秒 | 80秒 | 2.6x |
| 16核  | 105秒 | 40秒 | 2.6x |
| 48核  | 42秒 | 10秒 | 4.2x ⚡ |
| 96核  | 42秒 | 5秒 | 8.4x ⚡⚡ |

**结论**：核心数越多，Step 级并行优势越明显

---

## 总结

### 何时迁移？

**立即迁移** ✅：
- 使用 AutoTau 特征
- 大规模批量处理（>50 实验）
- 多核系统（>32 核）
- 内存受限环境

**暂缓迁移** ⏸：
- 少量实验（<10 个）
- 只使用 Transfer 特征（已优化）
- 调试开发阶段

### 迁移收益

| 指标 | 收益 |
|------|------|
| **性能提升** | 2-10x（取决于特征） |
| **内存降低** | 75% |
| **开发效率** | 提升（提取器更简单） |
| **可维护性** | 提升（职责分离） |
| **代码修改** | 最小（1-2行） |

---

**推荐**：所有新项目和批量处理任务使用 Step 级并行架构 🚀

---

**最后更新**: 2025-11-04
**状态**: ✅ Ready for Production
