Output:
./restart_crop_resnet18.sh
./train_cifar10_resnet18_fedprox_mu0.01_nopre_nose.log
./train_mobilenet_crop_fedproxv2.log
./restart_gpu5.sh
./run_fedproxv2_gpu2.sh
./train_ablation_v2.sh
./check_data.py
./run_cifar10_local.sh
./train_resnet18_crop_fedproxv2_full_v2.log
./check_bn_stats.py
./bg.py
./read_resnet_h5.py
./check_running_stats.py
./launch_train.py
./analyze_cifar10_models.py
./test_cifar10_models.py
./check_models.py
./check_model.py
./start_train_resnet18_crop.sh
./check_cifar10.py
./train_cifar10_resnet18_fedproxv2_full_bs64_gpu5_v2.log
./run_avg_lr0005.sh
./train.log
./test_fix_bn.py
./launch_5.sh
./check_model_keys.py
./train_resnet18_crop_fedproxv2_mixup_amp_gpu6.log
./dedup_images.py
./test_pil_vs_nopil.py
./train_ablation.sh
./final/crop_FedAvg_rerun1.log
./final/lr0.005_fedprox_pre_full.log
./final/avg_none.log
./final/plot_fedprox_h5.py
./final/crop_FedProxV2_no_mixup.log
./final/crop_FedAvg_rerun2.log
./final/proxv2_all_final.log
./final/crop_rerun1.log
./final/avg_pre_nomixup_rerun1.log
./final/crop_FedProxV2_no_pretrain.log
./final/lr0.005_fedprox_pre_nodmu.log
./final/avg_pre_nomixup.log
./final/fedprox_pre_final_0.8475.log
./final/avg_pre_nomixup_rerun2.log
./final/fedprox_onlypre1_0.05.log
./final/crop_FedProxV2_full.log
./final/prox_nopre.log
./final/table2_crop_prox.png
./final/fedprox_nomixup1_0.05.log
./final/mobilenet_full_lr0005.log
./final/proxv2_nomixup.log
./final/avg_pre_nomixup_rerun3.log
./final/table1_crop_avg.png
./final/fedprox_full_0.05.log
./final/fedprox_full_mobile.log
./final/crop_FedProxV2_no_dmu.log
./final/table2_crop_prox_h5.png
./final/proxv2_all.log
./final/fedprox_onlypre_0.05.log
./final/fedprox_full.log
./final/proxv2_all_final1.log
./final/avg_pre.log
./final/plot_two_tables.py
./final/lr0.005_fedprox_none.log
./final/table3_cifar10.png
./final/crop_rerun2.log
./final/fedprox_none_0.05.log
./final/plot_all_tables.py
./final/avg_lr0005_none.log
./final/proxv2_pre.log
./final/lr0.005_fedprox_pre_nodmu_nomixup.log
./final/crop_rerun0.log
./final/avg_lr0005_curve_en.png
./final/lr005_ablation_curve.png
./final/lr005_ablation_individual.png
./final/lr0.005_fedprox_pre_nomixup.log
./final/avg_pre_nomixup_rerun0.log
./final/avg_lr0005_pre_mixup.log
./final/plot_three_tables.py
./final/fedprox_mu_compare_lr0005.png
./final/fedprox_full1_0.05.log
./final/avg_lr0005_pre_nomixup.log
./test_reset_bn.py
./check_conv.py
./inspect_ckpt.py
./check_bn_gn.py
./main_v2.py.bak_20260428_resnet86
./analyze_model_bn_or_gn.py
./check_8653_bn.py
./dedup_by_filehash.py
./eval_npz_correct.py
./test_args.py
./run_ablation.sh
./train_cifar10_resnet18_fedavg_base.log
./plot_lr005.py
./train_cifar10_resnet18_local_bs32.log
./check_8149_detail.py
./check_se_weights.py
./check_model_structure.py
./FedNova_fix_cifar10_g3.log
./read_h5_acc.py
./train_crop_resnet18_local.log
./utils/mem_utils.py
./utils/data_utils1.py
./utils/dataset_utils.py
./utils/ALA.py
./utils/data_utils.py
./utils/dataset_utils_yolo.py
./utils/__init__.py
./utils/dlg.py
./utils/result_utils.py
./train_ablation_state.json
./train_resnet18_crop_fedproxv2_full.log
./launch_5.log
./main.py
./test_args2.py
./logs/crop_resnet18_fedavg_gpu3.log
./logs/crop_resnet18_fedproxv2_final1.log
./logs/cifar10_resnet18_fedproxv2_final.log
./logs/cifar10_resnet18_fedproxv2_final1.log
./logs/crop_resnet18_local_v2.log
./logs/crop_resnet18_fedproxv2_final.log
./logs/crop_resnet18_fedproxv2_gpu2.log
./logs/crop_resnet18_fedproxv2_gpu3.log
./reeval_cifar10_v2.py
./eval_real_images_v2.py
./check_8653_detail.py
./model_predict.py
./Cifar10_ResNet18_final_standard.png
./run_local_crop.sh
./eval_real_images_timing.py
./table2_crop_prox.png
./plot_result.py
./eval_train_npz.py
./main_v2.py.bak_20260425_1255
./train_cifar10_resnet18_fedavg_pre_nose.log
./eval_conf_time.py
./analyze_summary.py
./fix_auc_server.py
./train_cifar10_resnet18_fedproxv2_pre_nose.log
./run_crop.sh
./reeval_cifar10.py
./compare_models.py
./get_mean_std.py
./check_se.py
./main_v2.py.bak_20260428_deep_se
./eval_conf_time_v2.py
./main_v2.py.bak_20260428_resnet86_deeper
./table1_crop_avg.png
./restart_local_crop.sh
./check_keys2.py
./launch_ablation.sh
./FedNova_cifar10.log
./check_h5.py
./train_ablation_monitor.log
./lr005_ablation_curve_v2.png
./check_bn_or_gn.py
./run_mobilenet_full.sh
./run_train2.sh
./check_se_dim.py
./fix_auc.py
./inspect_load.py
./run_local_simple.sh
./check_shape.py
./SCAFFOLD_cifar10.log
./test_bn_mode.py
./run_train.sh
./nwafu_logo.png
./run_fedavg_gpu3.sh
./run_train.py
./test_se_loaded.py
./train_crop2.log
./reeval_cifar10_v3.py
./test_ui.py
./main_v2.py
./test_se_inject.py
./__pycache__/model_predict.cpython-311.pyc
./__pycache__/main_v2.cpython-311.pyc
./__pycache__/ui.cpython-311.pyc
./__pycache__/main_v2.cpython-36.pyc
./table3_cifar10.png
./FedProx_cifar10.log
./eval_real_images.py
./mobilenet_v2_imagenet.pth
./lr005_ablation_individual_v2.png
./test_argparse.py
./restart_mobile.sh
./check_m1.py
./verify_weights.py
./reeval_full.py
./eval_conf_time_v3.py
./check_gpu.py
./plot_all_tables.py
./inspect_8653.py
./check_conv2.py
./resnet18_imagenet.pth
./start_gn_se.sh
./help.py
./lr005_ablation_curve.png
./check_resnet_models.py


Explanation:
**Evaluating script options**

I'm considering the user's request about adding a module. They mentioned only including the corresponding module, but I wonder if the new main script could import the existing main_v2. If there are no changes in main_v2, it might be tricky to register the new algorithm. They want the full flow, so creating a new main script seems reasonable. I'll need to inspect the structure of main_v2 to see if any pieces can be reused, but maybe a standalone package is best. I need to determine the repository path from remote.


subagent-driven-development

---
name: subagent-driven-development
description: "Execute plans via delegate_task subagents (2-stage review)."
version: 1.1.0
author: Hermes Agent (adapted from obra/superpowers)
license: MIT
platforms: [linux, macos, windows]
metadata:
  hermes:
    tags: [delegation, subagent, implementation, workflow, parallel]
    related_skills: [writing-plans, requesting-code-review, test-driven-development]
---

# Subagent-Driven Development

## Overview

Execute implementation plans by dispatching fresh subagents per task with systematic two-stage review.

**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration.

## When to Use

Use this skill when:
- You have an implementation plan (from writing-plans skill or user requirements)
- Tasks are mostly independent
- Quality and spec compliance are important
- You want automated review between tasks

**vs. manual execution:**
- Fresh context per task (no confusion from accumulated state)
- Automated review process catches issues early
- Consistent quality checks across all tasks
- Subagents can ask questions before starting work

## The Process

### 1. Read and Parse Plan

Read the plan file. Extract ALL tasks with their full text and context upfront. Create a todo list:

```python
# Read the plan
read_file("docs/plans/feature-plan.md")

# Create todo list with all tasks
todo([
    {"id": "task-1", "content": "Create User model with email field", "status": "pending"},
    {"id": "task-2", "content": "Add password hashing utility", "status": "pending"},
    {"id": "task-3", "content": "Create login endpoint", "status": "pending"},
])
```

**Key:** Read the plan ONCE. Extract everything. Don't make subagents read the plan file — provide the full task text directly in context.

### 2. Per-Task Workflow

For EACH task in the plan:

#### Step 1: Dispatch Implementer Subagent

Use `delegate_task` with complete context:

```python
delegate_task(
    goal="Implement Task 1: Create User model with email and password_hash fields",
    context="""
    TASK FROM PLAN:
    - Create: src/models/user.py
    - Add User class with email (str) and password_hash (str) fields
    - Use bcrypt for password hashing
    - Include __repr__ for debugging

    FOLLOW TDD:
    1. Write failing test in tests/models/test_user.py
    2. Run: pytest tests/models/test_user.py -v (verify FAIL)
    3. Write minimal implementation
    4. Run: pytest tests/models/test_user.py -v (verify PASS)
    5. Run: pytest tests/ -q (verify no regressions)
    6. Commit: git add -A && git commit -m "feat: add User model with password hashing"

    PROJECT CONTEXT:
    - Python 3.11, Flask app in src/app.py
    - Existing models in src/models/
    - Tests use pytest, run from project root
    - bcrypt already in requirements.txt
    """,
    toolsets=['terminal', 'file']
)
```

#### Step 2: Dispatch Spec Compliance Reviewer

After the implementer completes, verify against the original spec:

```python
delegate_task(
    goal="Review if implementation matches the spec from the plan",
    context="""
    ORIGINAL TASK SPEC:
    - Create src/models/user.py with User class
    - Fields: email (str), password_hash (str)
    - Use bcrypt for password hashing
    - Include __repr__

    CHECK:
    - [ ] All requirements from spec implemented?
    - [ ] File paths match spec?
    - [ ] Function signatures match spec?
    - [ ] Behavior matches expected?
    - [ ] Nothing extra added (no scope creep)?

    OUTPUT: PASS or list of specific spec gaps to fix.
    """,
    toolsets=['file']
)
```

**If spec issues found:** Fix gaps, then re-run spec review. Continue only when spec-compliant.

#### Step 3: Dispatch Code Quality Reviewer

After spec compliance passes:

```python
delegate_task(
    goal="Review code quality for Task 1 implementation",
    context="""
    FILES TO REVIEW:
    - src/models/user.py
    - tests/models/test_user.py

    CHECK:
    - [ ] Follows project conventions and style?
    - [ ] Proper error handling?
    - [ ] Clear variable/function names?
    - [ ] Adequate test coverage?
    - [ ] No obvious bugs or missed edge cases?
    - [ ] No security issues?

    OUTPUT FORMAT:
    - Critical Issues: [must fix before proceeding]
    - Important Issues: [should fix]
    - Minor Issues: [optional]
    - Verdict: APPROVED or REQUEST_CHANGES
    """,
    toolsets=['file']
)
```

**If quality issues found:** Fix issues, re-review. Continue only when approved.

#### Step 4: Mark Complete

```python
todo([{"id": "task-1", "content": "Create User model with email field", "status": "completed"}], merge=True)
```

### 3. Final Review

After ALL tasks are complete, dispatch a final integration reviewer:

```python
delegate_task(
    goal="Review the entire implementation for consistency and integration issues",
    context="""
    All tasks from the plan are complete. Review the full implementation:
    - Do all components work together?
    - Any inconsistencies between tasks?
    - All tests passing?
    - Ready for merge?
    """,
    toolsets=['terminal', 'file']
)
```

### 4. Verify and Commit

```bash
# Run full test suite
pytest tests/ -q

# Review all changes
git diff --stat

# Final commit if needed
git add -A && git commit -m "feat: complete [feature name] implementation"
```

## Task Granularity

**Each task = 2-5 minutes of focused work.**

**Too big:**
- "Implement user authentication system"

**Right size:**
- "Create User model with email and password fields"
- "Add password hashing function"
- "Create login endpoint"
- "Add JWT token generation"
- "Create registration endpoint"

## Red Flags — Never Do These

- Start implementation without a plan
- Skip reviews (spec compliance OR code quality)
- Proceed with unfixed critical/important issues
- Dispatch multiple implementation subagents for tasks that touch the same files
- Make subagent read the plan file (provide full text in context instead)
- Skip scene-setting context (subagent needs to understand where the task fits)
- Ignore subagent questions (answer before letting them proceed)
- Accept "close enough" on spec compliance
- Skip review loops (reviewer found issues → implementer fixes → review again)
- Let implementer self-review replace actual review (both are needed)
- **Start code quality review before spec compliance is PASS** (wrong order)
- Move to next task while either review has open issues

## Handling Issues

### If Subagent Asks Questions

- Answer clearly and completely
- Provide additional context if needed
- Don't rush them into implementation

### If Reviewer Finds Issues

- Implementer subagent (or a new one) fixes them
- Reviewer reviews again
- Repeat until approved
- Don't skip the re-review

### If Subagent Fails a Task

- Dispatch a new fix subagent with specific instructions about what went wrong
- Don't try to fix manually in the controller session (context pollution)

## Efficiency Notes

**Why fresh subagent per task:**
- Prevents context pollution from accumulated state
- Each subagent gets clean, focused context
- No confusion from prior tasks' code or reasoning

**Why two-stage review:**
- Spec review catches under/over-building early
- Quality review ensures the implementation is well-built
- Catches issues before they compound across tasks

**Cost trade-off:**
- More subagent invocations (implementer + 2 reviewers per task)
- But catches issues early (cheaper than debugging compounded problems later)

## Integration with Other Skills

### With writing-plans

This skill EXECUTES plans created by the writing-plans skill:
1. User requirements → writing-plans → implementation plan
2. Implementation plan → subagent-driven-development → working code

### With test-driven-development

Implementer subagents should follow TDD:
1. Write failing test first
2. Implement minimal code
3. Verify test passes
4. Commit

Include TDD instructions in every implementer context.

### With requesting-code-review

The two-stage review process IS the code review. For final integration review, use the requesting-code-review skill's review dimensions.

### With systematic-debugging

If a subagent encounters bugs during implementation:
1. Follow systematic-debugging process
2. Find root cause before fixing
3. Write regression test
4. Resume implementation

## Example Workflow

```
[Read plan: docs/plans/auth-feature.md]
[Create todo list with 5 tasks]

--- Task 1: Create User model ---
[Dispatch implementer subagent]
  Implementer: "Should email be unique?"
  You: "Yes, email must be unique"
  Implementer: Implemented, 3/3 tests passing, committed.

[Dispatch spec reviewer]
  Spec reviewer: ✅ PASS — all requirements met

[Dispatch quality reviewer]
  Quality reviewer: ✅ APPROVED — clean code, good tests

[Mark Task 1 complete]

--- Task 2: Password hashing ---
[Dispatch implementer subagent]
  Implementer: No questions, implemented, 5/5 tests passing.

[Dispatch spec reviewer]
  Spec reviewer: ❌ Missing: password strength validation (spec says "min 8 chars")

[Implementer fixes]
  Implementer: Added validation, 7/7 tests passing.

[Dispatch spec reviewer again]
  Spec reviewer: ✅ PASS

[Dispatch quality reviewer]
  Quality reviewer: Important: Magic number 8, extract to constant
  Implementer: Extracted MIN_PASSWORD_LENGTH constant
  Quality reviewer: ✅ APPROVED

[Mark Task 2 complete]

... (continue for all tasks)

[After all tasks: dispatch final integration reviewer]
[Run full test suite: all passing]
[Done!]
```

## Remember

```
Fresh subagent per task
Two-stage review every time
Spec compliance FIRST
Code quality SECOND
Never skip reviews
Catch issues early
```

**Quality is not an accident. It's the result of systematic process.**

## Further reading (load when relevant)

When the orchestration involves significant context usage, long review loops, or complex validation checkpoints, load these references for the specific discipline:

- **`references/context-budget-discipline.md`** — Four-tier context degradation model (PEAK / GOOD / DEGRADING / POOR), read-depth rules that scale with context window size, and early warning signs of silent degradation. Load when a run will clearly consume significant context (multi-phase plans, many subagents, large artifacts).
- **`references/gates-taxonomy.md`** — The four canonical gate types (Pre-flight, Revision, Escalation, Abort) with behavior, recovery, and examples. Load when designing or reviewing any workflow that has validation checkpoints — use the vocabulary explicitly so each gate has defined entry, failure behavior, and resumption rules.

Both references adapted from gsd-build/get-shit-done (MIT © 2025 Lex Christopherson).

To view linked files, call skill_view(name, file_path) where file_path is e.g. 'references/api.md' or 'assets/config.yaml'


Explanation:
**Planning code module addition**

I need to respond to the user by adding a code module without changing the original logic. It seems I should create a new, independent module for DeComFL, which might involve inspecting the repository for path issues. Using terminal with SSH could help. I think adding a standalone module in a new directory is possible, but I need to avoid altering the original code. I might want to explore the current structure more thoroughly.

**Designing the decomfl module**

I’m considering creating a standalone `decomfl` module, possibly with a `decomfl_main.py` wrapper to ensure the original logic stays intact. I could add `decomfl_module.py`, `run_decomfl.sh`, and perhaps `main_decomfl.py`. The challenge is making sure it integrates well without modifying existing code. It sounds like I could explore making a standalone script that reuses functions from main_v2 without altering it, but I must be careful about duplicating code. So, examining the main_v2 interface is key.

**Exploring a new script for decomfl**

I’m thinking it might be easier to create a new script called `decomfl_main.py` that operates separately from the existing main_v2. This script would handle its own simple workflow, possibly using existing models and utilities without modifying main_v2. However, developing the complete flow from scratch seems complex. I need to find out if DeComFL can be streamlined into a module that integrates with the current training framework, all while avoiding any original code modifications.

**Considering algorithm implementation**

I'm thinking about implementing a DeComFL-like algorithm for CIFAR10 using ResNet18, but it might be a substantial task. I’ll need to look at existing model and dataset usage patterns. To start, I can use a remote terminal to check the main_v2 structure and its utility functions. The user mentioned adding a module, so it makes sense to create specific module files and a new entry script that imports from main_v2 logic without altering the original code.

**Inspecting for reusability**

The user wants to keep the original logic intact, so I can only create new files. I should review the functions and data utilities in `main_v2.py` for potential reuse. There’s an issue with the local search_files path, but I have remote access available. I think I’ll use SSH with grep and maybe sed to read through the files. It’s important to check `main_v2.py` for any imports or helper functions related to data model setup.

terminal

sshpass -p 'lbhzj' ssh -o StrictHostKeyChecking=no user-lbhzj@100.78.7.50 "cd /home/user-lbhzj/pw/PFLlib-master/system && sed -n '1,220p' main_v2.py | sed -n '1,220p'"


Output:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""

优化点：
1. 使用 FedProxV2（label_smoothing + momentum + CosineAnnealing + 梯度裁剪 + 动态mu衰减 + Warmup）
2. 支持离线加载预训练 ResNet18 / MobileNetV2 权重
3. 增强数据增强：ColorJitter + RandomErasing
4. 优化默认参数（更适合CIFAR10训练）
5. 新增 warmup_rounds 参数
"""

import sys
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')

import copy
import torch
import argparse
import os
import sys
import time
import warnings
import numpy as np
import torchvision
import torch
import torch.nn as nn
import copy
import logging
import torchvision.models as models

from flcore.servers.serveravg import FedAvg
from flcore.servers.serverpFedMe import pFedMe
from flcore.servers.serverperavg import PerAvg
from flcore.servers.serverprox import FedProx
from flcore.servers.servernova import FedNova
from flcore.servers.serverfomo import FedFomo
from flcore.servers.serveramp import FedAMP
from flcore.servers.servermtl import FedMTL
from flcore.servers.serverlocal import Local
from flcore.servers.serverper import FedPer
from flcore.servers.serverapfl import APFL
from flcore.servers.serverditto import Ditto
from flcore.servers.serverrep import FedRep
from flcore.servers.serverphp import FedPHP
from flcore.servers.serverbn import FedBN
from flcore.servers.serverrod import FedROD
from flcore.servers.serverproto import FedProto
from flcore.servers.serverdyn import FedDyn
from flcore.servers.servermoon import MOON
from flcore.servers.serverbabu import FedBABU
from flcore.servers.serverapple import APPLE
from flcore.servers.servergen import FedGen
from flcore.servers.serverscaffold import SCAFFOLD
from flcore.servers.serverfd import FD
from flcore.servers.serverala import FedALA
from flcore.servers.serverpac import FedPAC
from flcore.servers.serverlg import LG_FedAvg
from flcore.servers.servergc import FedGC
from flcore.servers.serverfml import FML
from flcore.servers.serverkd import FedKD
from flcore.servers.serverpcl import FedPCL
from flcore.servers.servercp import FedCP
from flcore.servers.servergpfl import GPFL
from flcore.servers.serverntd import FedNTD
from flcore.servers.servergh import FedGH
from flcore.servers.serverdbe import FedDBE
from flcore.servers.servercac import FedCAC
from flcore.servers.serverda import PFL_DA
from flcore.servers.serverlc import FedLC
from flcore.servers.serveras import FedAS
from flcore.servers.servercross import FedCross

# 优化版 FedProxV2
from flcore.servers.serverprox_v2 import FedProxV2
from flcore.trainmodel.attention import SEBlock, ECABlock, CBAM, SimAM

from flcore.trainmodel.models import *
from flcore.trainmodel.bilstm import *
from flcore.trainmodel.resnet import *
from flcore.trainmodel.alexnet import *
from flcore.trainmodel.mobilenet_v2 import *
from flcore.trainmodel.transformer import *

from utils.result_utils import average_data
from utils.mem_utils import MemReporter

class ResNet_Cifar(nn.Module):

    def __init__(self, block, layers, num_classes=10):
        super(ResNet_Cifar, self).__init__()
        self.inplanes = 16
        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(16)
        self.relu = nn.ReLU(inplace=True)
        self.layer1 = self._make_layer(block, 16, layers[0])
        self.layer2 = self._make_layer(block, 32, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 64, layers[2], stride=2)
        self.avgpool = nn.AvgPool2d(8, stride=1)
        self.fc = nn.Linear(64 * block.expansion, num_classes)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                n = m.kernel_size[0] * m.kernel_size[1] * m.out_channels
                m.weight.data.normal_(0, math.sqrt(2. / n))
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()

    def _make_layer(self, block, planes, blocks, stride=1):
        downsample = None
        if stride != 1 or self.inplanes != planes * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.inplanes, planes * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(planes * block.expansion)
            )

        layers = []
        layers.append(block(self.inplanes, planes, stride, downsample))
        self.inplanes = planes * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.inplanes, planes))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)

        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)

        return x

def setup_device(args):
    if args.device == "cuda":
        if args.device_id:
            os.environ["CUDA_VISIBLE_DEVICES"] = args.device_id
        if not torch.cuda.is_available():
            warnings.warn("CUDA不可用，自动切换到CPU")
            args.device = torch.device("cpu")
        else:
            args.device = torch.device("cuda")
            args.multi_gpu = torch.cuda.device_count() > 1
    else:
        args.device = torch.device("cpu")
        args.multi_gpu = False

def wrap_model_for_parallel(model, args):
    if args.device.type == "cuda":
        model = nn.DataParallel(model)
    return model

def load_pretrained_resnet18(model, pretrained_path, num_classes):
    """离线加载预训练ResNet18权重（不含fc层）- 保留旧接口兼容"""
    return load_pretrained_weights(model, pretrained_path, skip_keys=('fc',))

def load_pretrained_weights(model, pretrained_path, skip_keys=('fc', 'classifier')):
    """离线加载预训练权重

    - DataParallel 的 module. 前缀
    - fc / classifier 层类别数不匹配时跳过
    - 形状不匹配的层跳过（如 BN→GN 后参数形状变了）

    Args:
        model: 目标模型
        pretrained_path: 预训练权重文件路径
        skip_keys: 包含这些关键字的层如果形状不匹配则跳过（分类头）

    Returns:
        加载了预训练权重的模型（文件不存在则原样返回）
    """
    if not os.path.exists(pretrained_path):
        print(f"[预训练] 权重文件不存在: {pretrained_path}，使用随机初始化")
        return model

    print(f"[预训练] 正在加载: {pretrained_path}")
    try:
        state_dict = torch.load(pretrained_path, map_location='cpu')

        # 处理 DataParallel 的 module. 前缀
        if any(k.startswith('module.') for k in state_dict.keys()):
            new_state_dict = {}
            for k, v in state_dict.items():
                new_state_dict[k.replace('module.', '')] = v
            state_dict = new_state_dict

        model_dict = model.state_dict()
        loaded_count = 0
        skipped_mismatch = []
        skipped_head = []

        for k, v in state_dict.items():
            if k not in model_dict:
                continue
            if v.shape != model_dict[k].shape:
                if any(s in k for s in skip_keys):
                    skipped_head.append(k)
                else:
                    skipped_mismatch.append(k)
                continue
            model_dict[k] = v
            loaded_count += 1

        model.load_state_dict(model_dict)
        print(f"[预训练] 成功加载 {loaded_count}/{len(model_dict)} 层权重")
        if skipped_head:
            print(f"[预训练] 跳过分类头(类别数不匹配): {skipped_head}")


Explanation:
**Evaluating integration options**

It looks like I can create a standalone entry point script that uses existing datasets and models, but I'm aware that the user insists on no original code changes. If I need to integrate an algorithm into the existing selection, modifications might be necessary in the main_v2. Alternatively, I could develop an independent wrapper to utilize existing classes and add a new module. I need to check the repository path via SSH and assess if a modular addition is feasible. I'd propose adding a standalone script and a new module to keep things intact.

**Exploring system integration**

I need to figure out how to utilize the existing system effectively. I can add files like `flcore/clients/clientdecomfl.py`, `flcore/servers/serverdecomfl.py`, and a new entry script like `main_decomfl.py` or `decomfl_main.py`, along with a run script. Importantly, I'll ensure there are no changes to the original `main_v2.py`. I should inspect the code structure to replicate patterns from the existing FedAvg. To do this, I can locate relevant files using `search_files/read_file` and read the sections necessary to maintain the original configuration.


File not found: /home/user-lbhzj/pw/PFLlib-master/system/flcore/servers/serveravg.py


File not found: /home/user-lbhzj/pw/PFLlib-master/system/flcore/clients/clientavg.py


{"content": "", "total_lines": 0, "file_size": 0, "truncated": false, "is_binary": false, "is_image": false, "error": "File not found: /home/user-lbhzj/pw/PFLlib-master/system/main.py"}

[Tool loop warning: same_tool_failure_warning; count=3; read_file has failed 3 times this turn. This looks like a loop; change approach before retrying.]


Explanation:
**Planning module implementation**

I need to address a user request to add a module without changing the original code. I'll likely use the terminal to inspect current files and might create a new, independent wrapper script. It's crucial to avoid patching the original code since the user only wants the module added. I'll plan this out carefully.

Maybe I should examine system paths and use SSH since the local helper can’t access it. It's a bit complex, but I can certainly proceed!

**Considering module implementation**

I need to provide concise, actionable steps for adding a new module. First, I plan to inspect `main_v2.py`. The idea is to create a new directory for standalone scripts while running everything without modifying the original framework. I might set up scripts like `run_decomfl_cifar10.py` and modules under `flcore`. To avoid altering existing code, I could make a standalone entry point that doesn't use `main_v2`, perhaps leveraging current data and model utilities without changes! That might keep things simpler and more efficient.

**Planning module addition**

The user wants to add a module without changing any existing logic, which is feasible if the new module is independent. I need to understand the code structure, so I'll use SSH to read sections of `main_v2.py` focusing on data loading and model creation, perhaps with `grep`. Since I can't read local files directly, I’ll use SSH commands with `sed` outputs. I should consider creating a minimal training script for DeComFL that includes feature extraction and zeroth-order estimates, ensuring it’s runnable as the user requested.

sshpass -p 'lbhzj' ssh -o StrictHostKeyChecking=no user-lbhzj@100.78.7.50 "cd /home/user-lbhzj/pw/PFLlib-master/system && grep -n '^def .*dataset\|^def .*model\|^def .*load' main_v2.py | head -80"


Output:
159:def wrap_model_for_parallel(model, args):
165:def load_pretrained_resnet18(model, pretrained_path, num_classes):
170:def load_pretrained_weights(model, pretrained_path, skip_keys=('fc', 'classifier')):
229:def freeze_backbone(model, model_name, has_pretrained, freeze_enabled=True):
286:def auto_load_pretrained(model, model_name, args, skip_keys=('fc', 'classifier')):
337:def _inject_se_resnet18(model):
382:def _inject_se_mobilenetv2(model):


我已经确认了一个关键事实：这个仓库的**主入口实际是 `main_v2.py`**，不是另一个 `main.py`。
接下来我只做**新增模块**方案，不碰原逻辑。

我现在要做的是：
1. 新增一个 `flcore/servers/serverdecomfl.py`
2. 新增一个 `flcore/clients/clientdecomfl.py`
3. 在 `main_v2.py` 里只加**一条新 import + 一个新分支**
4. 保证 FedAvg/FedProx 等原代码不改逻辑

但在动之前，我还要先看一下 **main_v2.py 里算法分支是怎么写的**，只读确认，不改。

sshpass -p 'lbhzj' ssh -o StrictHostKeyChecking=no user-lbhzj@100.78.7.50 "cd /home/user-lbhzj/pw/PFLlib-master/system && sed -n '740,845p' main_v2.py"


Output:
elif args.algorithm == "pFedMe":
            server = pFedMe(args, i)

        elif args.algorithm == "FedProx":
            server = FedProx(args, i)

        # ===== V2 优化版 FedProx（含动态mu + 增强数据增强） =====
        elif args.algorithm == "FedProxV2":
            server = FedProxV2(args, i)

        elif args.algorithm == "FedFomo":
            server = FedFomo(args, i)

        elif args.algorithm == "FedAMP":
            server = FedAMP(args, i)

        elif args.algorithm == "APFL":
            server = APFL(args, i)

        elif args.algorithm == "FedPer":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedPer(args, i)

        elif args.algorithm == "Ditto":
            server = Ditto(args, i)

        elif args.algorithm == "FedRep":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedRep(args, i)

        elif args.algorithm == "FedPHP":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedPHP(args, i)

        elif args.algorithm == "FedBN":
            server = FedBN(args, i)

        elif args.algorithm == "FedROD":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedROD(args, i)

        elif args.algorithm == "FedProto":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedProto(args, i)

        elif args.algorithm == "FedDyn":
            server = FedDyn(args, i)

        elif args.algorithm == "MOON":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = MOON(args, i)

        elif args.algorithm == "FedBABU":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedBABU(args, i)

        elif args.algorithm == "APPLE":
            server = APPLE(args, i)

        elif args.algorithm == "FedGen":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedGen(args, i)

        elif args.algorithm == "FedNova":
            server = FedNova(args, i)

        elif args.algorithm == "SCAFFOLD":
            server = SCAFFOLD(args, i)

        elif args.algorithm == "FD":
            server = FD(args, i)

        elif args.algorithm == "FedALA":
            server = FedALA(args, i)

        elif args.algorithm == "FedPAC":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = FedPAC(args, i)

        elif args.algorithm == "LG-FedAvg":
            args.head = copy.deepcopy(args.model.fc)
            args.model.fc = nn.Identity()
            args.model = BaseHeadSplit(args.model, args.head)
            server = LG_FedAvg(args, i)

        elif args.algorithm == "FedGC":
            args.head = copy.deepcopy(args.model.fc)


我已经确认：**只要新增一个 `elif args.algorithm == "DeComFL":` 分支，就能接入，不会影响原算法逻辑。**
所以接下来我会做的是：

```bash
- 新增 DeComFL 相关模块文件
- 新增一个最小入口分支
- 保持原算法分支不动
- 跑通完整流程验证
```

现在我先去看这个仓库里有没有**类似“最小新增算法”**可复用的现成模板，尽量少写。

sshpass -p 'lbhzj' ssh -o StrictHostKeyChecking=no user-lbhzj@100.78.7.50 "cd /home/user-lbhzj/pw/PFLlib-master/system && sed -n '1,220p' flcore/servers/serverfomo.py && printf '\n---CLIENT---\n' && sed -n '1,220p' flcore/clients/clientfomo.py"


Output:
import torch
import time
import copy
import random
import numpy as np
from flcore.clients.clientfomo import clientFomo
from flcore.servers.serverbase import Server
from threading import Thread
from utils.dlg import DLG

class FedFomo(Server):
    def __init__(self, args, times):
        super().__init__(args, times)

        # select slow clients
        self.set_slow_clients()
        self.set_clients(clientFomo)

        self.P = torch.diag(torch.ones(self.num_clients, device=self.device))
        self.uploaded_ids = []
        self.M = min(args.M, self.num_join_clients)
        self.client_models = [copy.deepcopy(self.global_model) for _ in range(self.num_clients)]

        print(f"\nJoin ratio / total clients: {self.join_ratio} / {self.num_clients}")
        print("Finished creating server and clients.")
        self.Budget = []

    def train(self):
        for i in range(self.global_rounds+1):
            s_t = time.time()
            self.selected_clients = self.select_clients()
            self.send_models()

            if i%self.eval_gap == 0:
                print(f"\n-------------Round number: {i}-------------")
                print("\nEvaluate personalized models")
                self.evaluate()

            for client in self.selected_clients:
                client.train()

            # threads = [Thread(target=client.train)
            #            for client in self.selected_clients]
            # [t.start() for t in threads]
            # [t.join() for t in threads]

            if self.dlg_eval and i%self.dlg_gap == 0:
                self.call_dlg(i)
            self.receive_models()

            self.Budget.append(time.time() - s_t)
            print('-'*25, 'time cost', '-'*25, self.Budget[-1])

            if self.auto_break and self.check_done(acc_lss=[self.rs_test_acc], top_cnt=self.top_cnt):
                break

        print("\nBest accuracy.")
        # self.print_(max(self.rs_test_acc), max(
        #     self.rs_train_acc), min(self.rs_train_loss))
        print(max(self.rs_test_acc))
        print("\nAverage time cost per round.")
        print(sum(self.Budget[1:])/len(self.Budget[1:]))

        self.save_results()

    def send_models(self):
        assert (len(self.selected_clients) > 0)
        for client in self.clients:
            start_time = time.time()

            if client.send_slow:
                time.sleep(0.1 * np.abs(np.random.rand()))

            M_ = min(self.M, len(self.uploaded_ids)) # if clients dropped
            indices = torch.topk(self.P[client.id], M_).indices.tolist()

            send_ids = []
            send_models = []
            for i in indices:
                send_ids.append(i)
                send_models.append(self.client_models[i])

            client.receive_models(send_ids, send_models)

            client.send_time_cost['num_rounds'] += 1
            client.send_time_cost['total_cost'] += 2 * (time.time() - start_time)

    def receive_models(self):
        assert (len(self.selected_clients) > 0)

        active_clients = random.sample(
            self.selected_clients, int((1-self.client_drop_rate) * self.current_num_join_clients))

        self.uploaded_ids = []
        self.uploaded_weights = []
        tot_samples = 0
        for client in active_clients:
            try:
                client_time_cost = client.train_time_cost['total_cost'] / client.train_time_cost['num_rounds'] + \
                        client.send_time_cost['total_cost'] / client.send_time_cost['num_rounds']
            except ZeroDivisionError:
                client_time_cost = 0
            if client_time_cost <= self.time_threthold:
                tot_samples += client.train_samples
                self.uploaded_ids.append(client.id)
                self.uploaded_weights.append(client.train_samples)
                self.client_models[client.id] = copy.deepcopy(client.model)
                self.P[client.id] += client.weight_vector
        for i, w in enumerate(self.uploaded_weights):
            self.uploaded_weights[i] = w / tot_samples

    def call_dlg(self, R):
        # items = []
        cnt = 0
        psnr_val = 0
        for cid, client_model_server in zip(range(self.num_clients), self.client_models):
            client_model = self.clients[cid].model
            client_model.eval()
            origin_grad = []
            for gp, pp in zip(client_model_server.parameters(), client_model.parameters()):
                origin_grad.append(gp.data - pp.data)

            target_inputs = []
            trainloader, _ = self.clients[cid].load_train_data()
            with torch.no_grad():
                for i, (x, y) in enumerate(trainloader):
                    if i >= self.batch_num_per_client:
                        break

                    if type(x) == type([]):
                        x[0] = x[0].to(self.device)
                    else:
                        x = x.to(self.device)
                    y = y.to(self.device)
                    output = client_model(x)
                    target_inputs.append((x, output))

            d = DLG(client_model, origin_grad, target_inputs)
            if d is not None:
                psnr_val += d
                cnt += 1

            # items.append((client_model, origin_grad, target_inputs))

        if cnt > 0:
            print('PSNR value is {:.2f} dB'.format(psnr_val / cnt))
        else:
            print('PSNR error')

        # self.save_item(items, f'DLG_{R}')

---CLIENT---
import torch
import numpy as np
import time
import copy
from flcore.clients.clientbase import Client
from torch.utils.data import DataLoader
from utils.data_utils import read_client_data

class clientFomo(Client):
    def __init__(self, args, id, train_samples, test_samples, **kwargs):
        super().__init__(args, id, train_samples, test_samples, **kwargs)

        self.num_clients = args.num_clients
        self.old_model = copy.deepcopy(self.model)
        self.received_ids = []
        self.received_models = []
        self.weight_vector = torch.zeros(self.num_clients, device=self.device)

        self.val_ratio = 0.2
        self.train_samples = self.train_samples * (1-self.val_ratio)

    def train(self):
        trainloader, val_loader = self.load_train_data()
        start_time = time.time()

        self.aggregate_parameters(val_loader)
        self.clone_model(self.model, self.old_model)

        # self.model.to(self.device)
        self.model.train()

        max_local_epochs = self.local_epochs
        if self.train_slow:
            max_local_epochs = np.random.randint(1, max_local_epochs // 2)

        for epoch in range(max_local_epochs):
            for x, y in trainloader:
                if type(x) == type([]):
                    x[0] = x[0].to(self.device)
                else:
                    x = x.to(self.device)
                y = y.to(self.device)
                if self.train_slow:
                    time.sleep(0.1 * np.abs(np.random.rand()))
                output = self.model(x)
                loss = self.loss(output, y)
                self.optimizer.zero_grad()
                loss.backward()
                self.optimizer.step()

        # self.model.cpu()

        if self.learning_rate_decay:
            self.learning_rate_scheduler.step()

        self.train_time_cost['num_rounds'] += 1
        self.train_time_cost['total_cost'] += time.time() - start_time

    def load_train_data(self, batch_size=None):
        if batch_size == None:
            batch_size = self.batch_size
        train_data = read_client_data(self.dataset, self.id, is_train=True, few_shot=self.few_shot)
        val_idx = -int(self.val_ratio*len(train_data))
        val_data = train_data[val_idx:]
        train_data = train_data[:val_idx]

        trainloader = DataLoader(train_data, self.batch_size, drop_last=True, shuffle=False)
        val_loader = DataLoader(val_data, self.batch_size, drop_last=self.has_BatchNorm, shuffle=False)

        return trainloader, val_loader

    def train_metrics(self):
        trainloader, val_loader = self.load_train_data()
        # self.model = self.load_model('model')
        # self.model.to(self.device)
        self.model.eval()

        train_num = 0
        loss = 0
        for x, y in trainloader:
            if type(x) == type([]):
                x[0] = x[0].to(self.device)
            else:
                x = x.to(self.device)
            y = y.to(self.device)
            output = self.model(x)
            train_num += y.shape[0]
            loss += self.loss(output, y).item() * y.shape[0]

        # self.model.cpu()
        # self.save_model(self.model, 'model')

        return loss, train_num

    def receive_models(self, ids, models):
        self.received_ids = ids
        self.received_models = models

    def weight_cal(self, val_loader):
        weight_list = []
        L = self.recalculate_loss(self.old_model, val_loader)
        for received_model in self.received_models:
            params_dif = []
            for param_n, param_i in zip(received_model.parameters(), self.old_model.parameters()):
                params_dif.append((param_n - param_i).view(-1))
            params_dif = torch.cat(params_dif)

            weight_list.append((L - self.recalculate_loss(received_model, val_loader)) / (torch.norm(params_dif) + 1e-5))

        # import torch.autograd.profiler as profiler
        # with profiler.profile(profile_memory=True, record_shapes=True) as prof:
        #     self.weight_vector_update(weight_list)
        # print(prof.key_averages().table(sort_by="cuda_memory_usage", row_limit=10))

        # from pytorch_memlab import LineProfiler
        # with LineProfiler(self.weight_vector_update(weight_list)) as prof:
        #     self.weight_vector_update(weight_list)
        # prof.display()

        self.weight_vector_update(weight_list)

        return torch.tensor(weight_list)

    # from pytorch_memlab import profile
    # @profile
    def weight_vector_update(self, weight_list):
        # self.weight_vector = torch.zeros(self.num_clients, device=self.device)
        # for w, id in zip(weight_list, self.received_ids):
        #     self.weight_vector[id] += w.clone()

        self.weight_vector = np.zeros(self.num_clients)
        for w, id in zip(weight_list, self.received_ids):
            self.weight_vector[id] += w.item()
        self.weight_vector = torch.tensor(self.weight_vector).to(self.device)

    def recalculate_loss(self, new_model, val_loader):
        L = 0
        for x, y in val_loader:
            if type(x) == type([]):
                x[0] = x[0].to(self.device)
            else:
                x = x.to(self.device)
            y = y.to(self.device)
            output = new_model(x)
            loss = self.loss(output, y)
            L += loss.item()

        return L / len(val_loader)

    def add_parameters(self, w, received_model):
        for param, received_param in zip(self.model.parameters(), received_model.parameters()):
            param.data += received_param.data.clone() * w

    def aggregate_parameters(self, val_loader):
        weights = self.weight_scale(self.weight_cal(val_loader))

        if len(weights) > 0:
            for param in self.model.parameters():
                param.data.zero_()

            for w, received_model in zip(weights, self.received_models):
                self.add_parameters(w, received_model)

    def weight_scale(self, weights):
        weights = torch.maximum(weights, torch.tensor(0))
        w_sum = torch.sum(weights)
        if w_sum > 0:
            weights = [w/w_sum for w in weights]
            return torch.tensor(weights)
        else:
            return torch.tensor([])


Explanation:
**Inspecting FL module classes**

I’m thinking about the FL module and whether it should be based on the existing FLBase, Server, and Client classes. I need to inspect the serverbase and clientbase directly from the terminal. I could either read the file or use the terminal command `sed`. So, I’m aiming to check serverbase, clientbase, and possibly the arguments related to model building. Let's dig into that a bit further!

sshpass -p 'lbhzj' ssh -o StrictHostKeyChecking=no user-lbhzj@100.78.7.50 "cd /home/user-lbhzj/pw/PFLlib-master/system && sed -n '1,260p' flcore/servers/serverbase.py"

sshpass -p 'lbhzj' ssh -o StrictHostKeyChecking=no user-lbhzj@100.78.7.50 "cd /home/user-lbhzj/pw/PFLlib-master/system && sed -n '1,280p' flcore/clients/clientbase.py"

sshpass -p 'lbhzj' ssh -o StrictHostKeyChecking=no user-lbhzj@100.78.7.50 "cd /home/user-lbhzj/pw/PFLlib-master/system && grep -n 'parser.add_argument' -n main_v2.py | tail -80"


Output:
import torch
import os
import numpy as np
import h5py
import copy
import time
import random
from utils.data_utils import read_client_data
from utils.dlg import DLG

class Server(object):
    def __init__(self, args, times):
        # Set up the main attributes
        self.args = args
        self.device = args.device
        self.dataset = args.dataset
        self.num_classes = args.num_classes
        self.global_rounds = args.global_rounds
        self.local_epochs = args.local_epochs
        self.batch_size = args.batch_size
        self.learning_rate = args.local_learning_rate
        self.global_model = copy.deepcopy(args.model)
        self.num_clients = args.num_clients
        self.join_ratio = args.join_ratio
        self.random_join_ratio = args.random_join_ratio
        self.num_join_clients = int(self.num_clients * self.join_ratio)
        self.current_num_join_clients = self.num_join_clients
        self.few_shot = args.few_shot
        self.algorithm = args.algorithm
        self.time_select = args.time_select
        self.goal = args.goal
        self.time_threthold = args.time_threthold
        self.save_folder_name = args.save_folder_name
        self.top_cnt = args.top_cnt
        self.auto_break = args.auto_break

        self.clients = []
        self.selected_clients = []
        self.train_slow_clients = []
        self.send_slow_clients = []

        self.uploaded_weights = []
        self.uploaded_ids = []
        self.uploaded_models = []

        self.rs_test_acc = []
        self.rs_test_auc = []
        self.rs_train_loss = []

        self.times = times
        self.eval_gap = args.eval_gap
        self.client_drop_rate = args.client_drop_rate
        self.train_slow_rate = args.train_slow_rate
        self.send_slow_rate = args.send_slow_rate

        self.dlg_eval = args.dlg_eval
        self.dlg_gap = args.dlg_gap
        self.batch_num_per_client = args.batch_num_per_client

        self.num_new_clients = args.num_new_clients
        self.new_clients = []
        self.eval_new_clients = False
        self.fine_tuning_epoch_new = args.fine_tuning_epoch_new

        """
        # --- 针对 PFLlib 结构的智能自适应加载逻辑 ---
        import torch
        import os

        model_path = os.path.join("models", self.dataset, f"{self.algorithm}_{self.dataset}.pt")

        if os.path.exists(model_path):
            print("\n" + "="*40)
            print(f"发现已有模型权重: {model_path}")

            try:
                loaded_data = torch.load(model_path, map_location=self.device)

                # 情况 A: 如果加载的是整个模型实例
                if isinstance(loaded_data, torch.nn.Module):
                    state_dict = loaded_data.state_dict()
                else:
                    state_dict = loaded_data

                # 情况 B: 自动处理 module. 前缀冲突
                # 获取当前模型的所有 key
                current_model_keys = self.global_model.state_dict().keys()
                # 检查当前模型是否带 module.
                model_has_module = any(k.startswith('module.') for k in current_model_keys)
                # 检查保存的权重是否带 module.
                data_has_module = any(k.startswith('module.') for k in state_dict.keys())

                new_state_dict = {}
                if model_has_module and not data_has_module:
                    # 模型要前缀，数据没有 -> 给数据加上
                    for k, v in state_dict.items():
                        new_state_dict["module." + k] = v
                elif not model_has_module and data_has_module:
                    # 模型不要前缀，数据有 -> 把数据去掉
                    for k, v in state_dict.items():
                        new_state_dict[k.replace("module.", "")] = v
                else:
                    # 两者一致，直接用
                    new_state_dict = state_dict

                # 正式加载，设置 strict=False 容错
                self.global_model.load_state_dict(new_state_dict, strict=True)
                print("权重自适应加载成功！")

            except Exception as e:
                print(f"加载出错: {e}")
                print("程序将尝试继续运行...")
            print("="*40 + "\n")
        """

    def set_clients(self, clientObj):
        for i, train_slow, send_slow in zip(range(self.num_clients), self.train_slow_clients, self.send_slow_clients):
            train_data = read_client_data(self.dataset, i, is_train=True, few_shot=self.few_shot)
            test_data = read_client_data(self.dataset, i, is_train=False, few_shot=self.few_shot)
            client = clientObj(self.args,
                            id=i,
                            train_samples=len(train_data),
                            test_samples=len(test_data),
                            train_slow=train_slow,
                            send_slow=send_slow)
            self.clients.append(client)

    # random select slow clients
    def select_slow_clients(self, slow_rate):
        slow_clients = [False for i in range(self.num_clients)]
        idx = [i for i in range(self.num_clients)]
        idx_ = np.random.choice(idx, int(slow_rate * self.num_clients))
        for i in idx_:
            slow_clients[i] = True

        return slow_clients

    def set_slow_clients(self):
        self.train_slow_clients = self.select_slow_clients(
            self.train_slow_rate)
        self.send_slow_clients = self.select_slow_clients(
            self.send_slow_rate)

    def select_clients(self):
        if self.random_join_ratio:
            self.current_num_join_clients = np.random.choice(range(self.num_join_clients, self.num_clients+1), 1, replace=False)[0]
        else:
            self.current_num_join_clients = self.num_join_clients
        selected_clients = list(np.random.choice(self.clients, self.current_num_join_clients, replace=False))

        return selected_clients

    def send_models(self):
        assert (len(self.clients) > 0)

        for client in self.clients:
            start_time = time.time()

            client.set_parameters(self.global_model)

            client.send_time_cost['num_rounds'] += 1
            client.send_time_cost['total_cost'] += 2 * (time.time() - start_time)

    def receive_models(self):
        assert (len(self.selected_clients) > 0)

        active_clients = random.sample(
            self.selected_clients, int((1-self.client_drop_rate) * self.current_num_join_clients))

        self.uploaded_ids = []
        self.uploaded_weights = []
        self.uploaded_models = []
        tot_samples = 0
        for client in active_clients:
            try:
                client_time_cost = client.train_time_cost['total_cost'] / client.train_time_cost['num_rounds'] + \
                        client.send_time_cost['total_cost'] / client.send_time_cost['num_rounds']
            except ZeroDivisionError:
                client_time_cost = 0
            #if client_time_cost <= self.time_threthold:
            if self.time_threthold <= 0 or client_time_cost <= self.time_threthold:
                tot_samples += client.train_samples
                self.uploaded_ids.append(client.id)
                self.uploaded_weights.append(client.train_samples)
                self.uploaded_models.append(client.model)
        for i, w in enumerate(self.uploaded_weights):
            self.uploaded_weights[i] = w / tot_samples

    def aggregate_parameters(self):
        assert (len(self.uploaded_models) > 0)

        self.global_model = copy.deepcopy(self.uploaded_models[0])
        for param in self.global_model.parameters():
            param.data.zero_()

        for w, client_model in zip(self.uploaded_weights, self.uploaded_models):
            self.add_parameters(w, client_model)

    def add_parameters(self, w, client_model):
        for server_param, client_param in zip(self.global_model.parameters(), client_model.parameters()):
            server_param.data += client_param.data.clone() * w

    def save_global_model1(self):
        model_path = os.path.join("models", self.dataset)
        if not os.path.exists(model_path):
            os.makedirs(model_path)
        model_path = os.path.join(model_path, self.algorithm + "_" + self.dataset + ".pt")
        torch.save(self.global_model, model_path)

    def load_model(self):
        model_path = os.path.join("models", self.dataset)
        model_path = os.path.join(model_path, self.algorithm + "_" + self.dataset + ".pt")
        assert (os.path.exists(model_path))
        self.global_model = torch.load(model_path)

    def model_exists(self):
        model_path = os.path.join("models", self.dataset)
        model_path = os.path.join(model_path, self.algorithm + "_" + self.dataset + ".pt")
        return os.path.exists(model_path)

    def save_results1(self):
        algo = self.dataset + "_" + self.algorithm
        result_path = "../results/"
        if not os.path.exists(result_path):
            os.makedirs(result_path)

        if (len(self.rs_test_acc)):
            algo = algo + "_" + self.goal + "_" + str(self.times)
            file_path = result_path + "{}.h5".format(algo)
            print("File path: " + file_path)

            with h5py.File(file_path, 'w') as hf:
                hf.create_dataset('rs_test_acc', data=self.rs_test_acc)
                hf.create_dataset('rs_test_auc', data=self.rs_test_auc)
                hf.create_dataset('rs_train_loss', data=self.rs_train_loss)

    def save_global_model(self):
        # 1. 路径处理
        model_path = os.path.join("models", self.dataset)
        if not os.path.exists(model_path):
            os.makedirs(model_path)

        # 2. 获取当前测试集最高准确率
        max_acc = max(self.rs_test_acc) if len(self.rs_test_acc) > 0 else 0
        acc_str = f"_acc{max_acc:.4f}"

        # 3. 动态获取模型名称 (从 args 中提取模型类名或指定名称)
        # 如果 args 里没有 model_name，则尝试从模型对象中获取类名
        m_name = getattr(self.args, 'model_name', self.global_model.__class__.__name__)

        # 4. 组合文件名：模型名_算法名_数据集_准确率.pt
        file_name = f"{m_name}_{self.algorithm}_{self.dataset}{acc_str}.pt"
        model_full_path = os.path.join(model_path, file_name)

        # 5. 保存模型实例（直接保存整个模型，加载时更方便）
        self.global_model.eval()
        torch.save(self.global_model, model_full_path)


Output:
import copy
import torch
import torch.nn as nn
import numpy as np
import os
from torch.utils.data import DataLoader
from sklearn.preprocessing import label_binarize
from sklearn import metrics
from utils.data_utils import read_client_data, read_full_test_data
from torch.cuda.amp import autocast, GradScaler
from flcore.optimizers.fedoptimizer import PerturbedGradientDescent

class Client(object):
    """
    Base class for clients in federated learning.
    """

    def __init__(self, args, id, train_samples, test_samples, **kwargs):
        torch.manual_seed(0)
        self.model = copy.deepcopy(args.model)
        self.algorithm = args.algorithm
        self.dataset = args.dataset

        self.id = id
        # self.device = args.device
        self.device = args.client_device_map[self.id]
        self.model.to(self.device)

        self.save_folder_name = args.save_folder_name
        self.mu = args.mu
        self.num_classes = args.num_classes
        self.num_clients = args.num_clients
        self.train_samples = train_samples
        self.test_samples = test_samples
        self.batch_size = args.batch_size
        self.num_workers = getattr(args, 'num_workers', 4)
        self.pin_memory = getattr(args, 'pin_memory', True)
        self.learning_rate = args.local_learning_rate
        self.local_epochs = args.local_epochs
        self.few_shot = args.few_shot
        self.learning_rate_decay = args.learning_rate_decay
        # check BatchNorm
        self.has_BatchNorm = False
        for layer in self.model.children():
            if isinstance(layer, nn.BatchNorm2d):
                self.has_BatchNorm = True
                break

        self.train_slow = kwargs['train_slow']
        self.send_slow = kwargs['send_slow']
        self.train_time_cost = {'num_rounds': 0, 'total_cost': 0.0}
        self.send_time_cost = {'num_rounds': 0, 'total_cost': 0.0}

        self.loss = nn.CrossEntropyLoss()
        self.optimizer = torch.optim.SGD(self.model.parameters(), lr=self.learning_rate)

        """
        self.learning_rate_scheduler = torch.optim.lr_scheduler.ExponentialLR(
            optimizer=self.optimizer,
            gamma=args.learning_rate_decay_gamma
        )

        """

        # 引入 MultiStepLR
        self.learning_rate_scheduler = torch.optim.lr_scheduler.MultiStepLR(
            optimizer=self.optimizer,
            milestones=args.lr_decay_milestones, # 传入 [60, 120]
            gamma=args.learning_rate_decay_gamma # 传入 0.1 或 0.5
        )

        # 混合精度训练支持
        self.use_amp = getattr(args, 'use_amp', False)
        self.scaler = GradScaler(enabled=self.use_amp)

    def load_train_data(self, batch_size=None):
        if batch_size == None:
            batch_size = self.batch_size
        train_data = read_client_data(self.dataset, self.id, is_train=True, few_shot=self.few_shot)
        return DataLoader(train_data, batch_size, drop_last=True, shuffle=True,
                          num_workers=self.num_workers, pin_memory=self.pin_memory, persistent_workers=False)

    def load_test_data(self, batch_size=None):
        if batch_size == None:
            batch_size = self.batch_size
        test_data = read_client_data(self.dataset, self.id, is_train=False, few_shot=self.few_shot)
        return DataLoader(test_data, batch_size, drop_last=False, shuffle=True,
                          num_workers=self.num_workers, pin_memory=self.pin_memory)

    def load_full_test_data(self, batch_size=None):
        if batch_size == None:
            batch_size = self.batch_size
        test_data = read_full_test_data(self.dataset, self.num_clients)
        return DataLoader(test_data, batch_size, drop_last=False, shuffle=True,
                          num_workers=self.num_workers, pin_memory=self.pin_memory)

    def set_parameters(self, model):
        for new_param, old_param in zip(model.parameters(), self.model.parameters()):
            old_param.data = new_param.data.clone()

    def clone_model(self, model, target):
        for param, target_param in zip(model.parameters(), target.parameters()):
            target_param.data = param.data.clone()
            # target_param.grad = param.grad.clone()

    def update_parameters(self, model, new_params):
        for param, new_param in zip(model.parameters(), new_params):
            param.data = new_param.data.clone()

    def test_metrics(self):
        testloaderfull = self.load_test_data()
        # self.model = self.load_model('model')
        # self.model.to(self.device)
        self.model.eval()

        test_acc = 0
        test_num = 0
        y_prob = []
        y_true = []

        with torch.no_grad():
            for x, y in testloaderfull:
                if type(x) == type([]):
                    x[0] = x[0].to(self.device)
                else:
                    x = x.to(self.device)
                y = y.to(self.device)
                with autocast(enabled=self.use_amp):
                    output = self.model(x)

                test_acc += (torch.sum(torch.argmax(output, dim=1) == y)).item()
                test_num += y.shape[0]

                y_prob.append(output.detach().cpu().numpy())
                nc = self.num_classes
                if self.num_classes == 2:
                    nc += 1
                lb = label_binarize(y.detach().cpu().numpy(), classes=np.arange(nc))
                if self.num_classes == 2:
                    lb = lb[:, :2]
                y_true.append(lb)

        # self.model.cpu()
        # self.save_model(self.model, 'model')

        y_prob = np.concatenate(y_prob, axis=0)
        y_true = np.concatenate(y_true, axis=0)

        try:
            auc = metrics.roc_auc_score(y_true, y_prob, average='micro')
        except ValueError:
            auc = 0.0

        return test_acc, test_num, auc

    def test_metrics_full(self):
        """Evaluate client model on full test dataset (all clients' test data combined)."""
        testloaderfull = self.load_full_test_data()
        self.model.eval()

        test_acc = 0
        test_num = 0
        y_prob = []
        y_true = []

        with torch.no_grad():
            for x, y in testloaderfull:
                if type(x) == type([]):
                    x[0] = x[0].to(self.device)
                else:
                    x = x.to(self.device)
                y = y.to(self.device)
                with autocast(enabled=self.use_amp):
                    output = self.model(x)

                test_acc += (torch.sum(torch.argmax(output, dim=1) == y)).item()
                test_num += y.shape[0]

                y_prob.append(output.detach().cpu().numpy())
                nc = self.num_classes
                if self.num_classes == 2:
                    nc += 1
                lb = label_binarize(y.detach().cpu().numpy(), classes=np.arange(nc))
                if self.num_classes == 2:
                    lb = lb[:, :2]
                y_true.append(lb)

        y_prob = np.concatenate(y_prob, axis=0)
        y_true = np.concatenate(y_true, axis=0)

        try:
            auc = metrics.roc_auc_score(y_true, y_prob, average='micro')
        except ValueError:
            auc = 0.0

        return test_acc, test_num, auc

    def train_metrics(self):
        trainloader = self.load_train_data()
        # self.model = self.load_model('model')
        # self.model.to(self.device)
        self.model.eval()

        train_num = 0
        losses = 0
        with torch.no_grad():
            for x, y in trainloader:
                if type(x) == type([]):
                    x[0] = x[0].to(self.device)
                else:
                    x = x.to(self.device)
                y = y.to(self.device)
                output = self.model(x)
                loss = self.loss(output, y)
                train_num += y.shape[0]
                losses += loss.item() * y.shape[0]

        # self.model.cpu()
        # self.save_model(self.model, 'model')

        return losses, train_num

    # def get_next_train_batch(self):
    #     try:
    #         # Samples a new batch for persionalizing
    #         (x, y) = next(self.iter_trainloader)
    #     except StopIteration:
    #         # restart the generator if the previous generator is exhausted.
    #         self.iter_trainloader = iter(self.trainloader)
    #         (x, y) = next(self.iter_trainloader)

    #     if type(x) == type([]):
    #         x = x[0]
    #     x = x.to(self.device)
    #     y = y.to(self.device)

    #     return x, y

    def save_item(self, item, item_name, item_path=None):
        if item_path == None:
            item_path = self.save_folder_name
        if not os.path.exists(item_path):
            os.makedirs(item_path)
        torch.save(item, os.path.join(item_path, "client_" + str(self.id) + "_" + item_name + ".pt"))

    def load_item(self, item_name, item_path=None):
        if item_path == None:
            item_path = self.save_folder_name
        return torch.load(os.path.join(item_path, "client_" + str(self.id) + "_" + item_name + ".pt"))

    # @staticmethod
    # def model_exists():
    #     return os.path.exists(os.path.join("models", "server" + ".pt"))


Output:
936:    parser.add_argument('-go', "--goal", type=str, default="test",
938:    parser.add_argument('-dev', "--device", type=str, default="cuda",
940:    parser.add_argument('-did', "--device_id", type=str, default="0")
941:    parser.add_argument('-data', "--dataset", type=str, default="Cifar10")
942:    parser.add_argument('-ncl', "--num_classes", type=int, default=10)
943:    parser.add_argument('-m', "--model", type=str, default="MobileNet")
944:    parser.add_argument('-lbs', "--batch_size", type=int, default=64)
945:    parser.add_argument('-lr', "--local_learning_rate", type=float, default=0.05,
947:    parser.add_argument('-ld', "--learning_rate_decay", type=bool, default=True)
948:    parser.add_argument('-ldg', "--learning_rate_decay_gamma", type=float, default=0.2)
949:    parser.add_argument('-ldm', "--lr_decay_milestones", type=int, nargs='+', default=[40, 60],
951:    parser.add_argument('-gr', "--global_rounds", type=int, default=70)
952:    parser.add_argument('-tc', "--top_cnt", type=int, default=10,
954:    parser.add_argument('-ls', "--local_epochs", type=int, default=3,
956:    parser.add_argument('-algo', "--algorithm", type=str, default="FedProxV2")
958:    parser.add_argument('--weight-decay', '--wd', dest='weight_decay', default=1e-4, type=float,
960:    parser.add_argument('-jr', "--join_ratio", type=float, default=1,
962:    parser.add_argument('-rjr', "--random_join_ratio", type=bool, default=False,
964:    parser.add_argument('-nc', "--num_clients", type=int, default=20,
966:    parser.add_argument('-pv', "--prev", type=int, default=0,
968:    parser.add_argument('-t', "--times", type=int, default=1,
970:    parser.add_argument('-eg', "--eval_gap", type=int, default=1,
972:    parser.add_argument('-sfn', "--save_folder_name", type=str, default='items')
973:    parser.add_argument('-ab', "--auto_break", type=bool, default=True)
974:    parser.add_argument('-dlg', "--dlg_eval", type=bool, default=False)
975:    parser.add_argument('-dlgg', "--dlg_gap", type=int, default=100)
976:    parser.add_argument('-bnpc', "--batch_num_per_client", type=int, default=10)
977:    parser.add_argument('-nnc', "--num_new_clients", type=int, default=0)
978:    parser.add_argument('-ften', "--fine_tuning_epoch_new", type=int, default=0)
979:    parser.add_argument('-fd', "--feature_dim", type=int, default=512)
980:    parser.add_argument('-vs', "--vocab_size", type=int, default=80,
982:    parser.add_argument('-ml', "--max_len", type=int, default=200)
983:    parser.add_argument('-fs', "--few_shot", type=int, default=0)
985:    parser.add_argument('-cdr', "--client_drop_rate", type=float, default=0.0,
987:    parser.add_argument('-tsr', "--train_slow_rate", type=float, default=0.0,
989:    parser.add_argument('-ssr', "--send_slow_rate", type=float, default=0.0,
991:    parser.add_argument('-ts', "--time_select", type=bool, default=False,
993:    parser.add_argument('-tth', "--time_threthold", type=float, default=10000,
996:    parser.add_argument('-bt', "--beta", type=float, default=0.0)
997:    parser.add_argument('-lam', "--lamda", type=float, default=1.0,
999:    parser.add_argument('-mu', "--mu", type=float, default=0.1)
1000:    parser.add_argument('-K', "--K", type=int, default=5,
1002:    parser.add_argument('-lrp', "--p_learning_rate", type=float, default=0.01,
1005:    parser.add_argument('-M', "--M", type=int, default=5,
1008:    parser.add_argument('-itk', "--itk", type=int, default=4000,
1011:    parser.add_argument('-alk', "--alphaK", type=float, default=1.0,
1013:    parser.add_argument('-sg', "--sigma", type=float, default=1.0)
1015:    parser.add_argument('-al', "--alpha", type=float, default=1.0)
1017:    parser.add_argument('-pls', "--plocal_epochs", type=int, default=1)
1019:    parser.add_argument('-tau', "--tau", type=float, default=1.0)
1021:    parser.add_argument('-fte', "--fine_tuning_epochs", type=int, default=10)
1023:    parser.add_argument('-dlr', "--dr_learning_rate", type=float, default=0.0)
1024:    parser.add_argument('-L', "--L", type=float, default=1.0)
1026:    parser.add_argument('-nd', "--noise_dim", type=int, default=512)
1027:    parser.add_argument('-glr', "--generator_learning_rate", type=float, default=0.005)
1028:    parser.add_argument('-hd', "--hidden_dim", type=int, default=512)
1029:    parser.add_argument('-se', "--server_epochs", type=int, default=1000)
1030:    parser.add_argument('-lf', "--localize_feature_extractor", type=bool, default=False)
1032:    parser.add_argument('-slr', "--server_learning_rate", type=float, default=1.0)
1034:    parser.add_argument('-et', "--eta", type=float, default=1.0)
1035:    parser.add_argument('-s', "--rand_percent", type=int, default=80)
1036:    parser.add_argument('-p', "--layer_idx", type=int, default=2,
1039:    parser.add_argument('-mlr', "--mentee_learning_rate", type=float, default=0.005)
1040:    parser.add_argument('-Ts', "--T_start", type=float, default=0.95)
1041:    parser.add_argument('-Te', "--T_end", type=float, default=0.98)
1043:    parser.add_argument('-mo', "--momentum", type=float, default=0.9)
1044:    parser.add_argument('-klw', "--kl_weight", type=float, default=0.0)
1047:    parser.add_argument('-fsb', "--first_stage_bound", type=int, default=0)
1048:    parser.add_argument('-ca', "--fedcross_alpha", type=float, default=0.99)
1049:    parser.add_argument('-cmss', "--collaberative_model_select_strategy", type=int, default=1)
1052:    parser.add_argument('-wr', "--warmup_rounds", type=int, default=0,
1054:    parser.add_argument('-pp', "--pretrained_path", type=str, default="",
1056:    parser.add_argument('-mn', "--model_name", type=str, default="",
1058:    parser.add_argument('-ma', "--mixup_alpha", type=float, default=0.2,
1060:    parser.add_argument('-aw', "--aux_weight", type=float, default=0,
1062:    parser.add_argument('-at', "--attention", type=str, default='se',


可以了，我已经把方案收敛好了，**不会去改你原来的算法逻辑**，只会**新增一个 DeComFL 模块**，再加一个最小入口分支。

```bash
新增文件：
- flcore/clients/clientdecomfl.py
- flcore/servers/serverdecomfl.py

最小改动：
- main_v2.py 只新增 1 个 import
- main_v2.py 只新增 1 个 elif 分支：
  elif args.algorithm == "DeComFL": server = DeComFL(args, i)

原有 FedAvg / FedProx / FedNova / FedProxV2 等逻辑不动
```

```bash
我选 DeComFL 的原因：
- 2025 论文
- 比 FedBiP 更容易接进现有 CIFAR10/ResNet18 框架
- 属于优化型方法，最适合“只新增模块”的接法
- 不需要把整套代码重构成扩散/one-shot 框架
```

```bash
完整流程我会这样保证：
1. 新增独立模块，不碰旧模块
2. 新增入口分支，不改旧分支
3. 跑一个最小可用流程验证：
   - 能启动
   - 能下发
   - 能本地训练
   - 能回传
   - 能评估
4. 再补训练脚本，保证你能直接复现
```

```bash
下一步我就按这个方式开始加
```