Skip to content

Commit af0ca58

Browse files
committed
feat: Add GraphNet Agent for automated sample extraction from HuggingFace models
1 parent 3782f55 commit af0ca58

31 files changed

Lines changed: 2088 additions & 0 deletions

graph_net/agent/README.md

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# GraphNet Agent
2+
3+
自动样本抽取 Agent,实现从 HuggingFace ModelID 到 GraphNet Sample 的自动化转换。
4+
5+
## 安装
6+
7+
### 基础依赖
8+
```bash
9+
# 已包含在 GraphNet 主依赖中
10+
pip install torch torchvision
11+
```
12+
13+
### Agent 可选依赖
14+
```bash
15+
# 安装 Agent 相关依赖(包括 huggingface_hub)
16+
pip install -e ".[agent]"
17+
18+
# 或单独安装
19+
pip install huggingface_hub>=0.20.0
20+
```
21+
22+
## 环境配置
23+
24+
### 设置工作空间
25+
```bash
26+
export GRAPH_NET_EXTRACT_WORKSPACE=/path/to/your/workspace
27+
```
28+
29+
或在代码中指定:
30+
```python
31+
from graph_net.agent import GraphNetAgent
32+
agent = GraphNetAgent(workspace="/path/to/workspace")
33+
```
34+
35+
## 使用示例
36+
37+
```python
38+
from graph_net.agent import GraphNetAgent
39+
40+
# 初始化 Agent
41+
agent = GraphNetAgent(
42+
workspace="./agent_workspace",
43+
hf_token=None # 可选,用于访问私有模型
44+
)
45+
46+
# 运行提取
47+
success = agent.extract_sample("bert-base-uncased")
48+
49+
if success:
50+
print("✅ Sample extracted successfully")
51+
else:
52+
print("❌ Extraction failed")
53+
```
54+
55+
## 工作流程
56+
57+
1. **Fetch**: 从 HuggingFace 下载模型
58+
2. **Analyze**: 解析 config.json 提取元数据
59+
3. **CodeGen**: 生成 run_model.py 脚本
60+
4. **Extract**: 执行脚本提取计算图
61+
5. **Deduplicate**: 检查是否与已有样本重复
62+
6. **Verify**: 验证样本完整性
63+
7. **Archive**: 保存 run_model.py 到样本目录
64+
65+
## 测试
66+
67+
```bash
68+
# 运行所有测试
69+
pytest graph_net/agent/tests/ -v
70+
71+
# 运行实际模型测试(需要设置环境变量)
72+
TEST_REAL_RUN=1 pytest graph_net/agent/tests/test_real_run.py -v
73+
```

graph_net/agent/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
GraphNet Auto Sample Extraction Agent
3+
4+
This module provides an automated agent for extracting GraphNet samples
5+
from Hugging Face models.
6+
"""
7+
8+
from graph_net.agent.graph_net_agent import GraphNetAgent
9+
10+
__all__ = ["GraphNetAgent"]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Code generation modules"""
2+
3+
from graph_net.agent.code_generator.base import BaseCodeGenerator
4+
from graph_net.agent.code_generator.template_generator import TemplateCodeGenerator
5+
6+
__all__ = ["BaseCodeGenerator", "TemplateCodeGenerator"]
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""Base code generator interface"""
2+
3+
from abc import ABC, abstractmethod
4+
from pathlib import Path
5+
6+
from graph_net.agent.metadata_analyzer.model_metadata import ModelMetadata
7+
8+
9+
class BaseCodeGenerator(ABC):
10+
"""Base class for code generators"""
11+
12+
@abstractmethod
13+
def generate(
14+
self,
15+
model_dir: Path,
16+
model_metadata: ModelMetadata,
17+
output_dir: Path,
18+
) -> Path:
19+
"""
20+
Generate run_model.py extraction script
21+
22+
Args:
23+
model_dir: Path to model directory
24+
model_metadata: Model metadata extracted from configuration
25+
output_dir: Output directory for generated script
26+
27+
Returns:
28+
Path to generated script file
29+
30+
Raises:
31+
CodeGenError: If code generation fails
32+
"""
33+
pass
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Template-based code generator implementation"""
2+
3+
from pathlib import Path
4+
from typing import Optional
5+
6+
from graph_net.agent.metadata_analyzer.model_metadata import ModelMetadata
7+
from graph_net.agent.code_generator.base import BaseCodeGenerator
8+
from graph_net.agent.utils.exceptions import CodeGenError
9+
10+
# Constants for safe vocab size calculation
11+
DEFAULT_VOCAB_SIZE = 30522
12+
MIN_SAFE_VOCAB_SIZE = 100
13+
FIXED_SAFE_LIMIT = 25000 # For very large vocabularies or specific model types
14+
LARGE_VOCAB_THRESHOLD = 100000
15+
MEDIUM_VOCAB_THRESHOLD = 50000
16+
SMALL_VOCAB_THRESHOLD = 10000
17+
LARGE_VOCAB_RATIO = 0.7
18+
MEDIUM_VOCAB_RATIO = 0.8
19+
SMALL_VOCAB_RATIO = 0.9
20+
21+
22+
class TemplateCodeGenerator(BaseCodeGenerator):
23+
"""Code generator using Jinja2 template"""
24+
25+
def __init__(self, template_path: Optional[str] = None):
26+
"""
27+
Args:
28+
template_path: Path to Jinja2 template file (optional)
29+
"""
30+
self.template_path = template_path
31+
self._template = None
32+
33+
def generate(
34+
self,
35+
model_dir: Path,
36+
model_metadata: ModelMetadata,
37+
output_dir: Path,
38+
) -> Path:
39+
"""
40+
Generate run_model.py extraction script using template
41+
42+
Args:
43+
model_dir: Path to model directory
44+
model_metadata: Model metadata extracted from configuration
45+
output_dir: Output directory for generated script
46+
47+
Returns:
48+
Path to generated script file
49+
"""
50+
try:
51+
output_dir.mkdir(parents=True, exist_ok=True)
52+
code = self._generate_code(model_dir, model_metadata)
53+
54+
script_path = output_dir / "run_model.py"
55+
with open(script_path, 'w', encoding='utf-8') as f:
56+
f.write(code)
57+
58+
return script_path
59+
except Exception as e:
60+
raise CodeGenError(f"Failed to generate code: {e}") from e
61+
62+
def _generate_code(self, model_dir: Path, model_metadata: ModelMetadata) -> str:
63+
"""Generate complete extraction script code string"""
64+
# Generate model loading code
65+
load_code = self._generate_model_loader(model_dir, model_metadata)
66+
67+
# Generate input construction code
68+
input_code = self._generate_input_code(model_metadata)
69+
70+
# Generate main code
71+
code = f'''import torch
72+
try:
73+
from transformers import AutoModel
74+
except ImportError:
75+
raise ImportError("transformers is required. Install with: pip install transformers")
76+
77+
import graph_net
78+
79+
def main():
80+
# Load model
81+
{self._indent(load_code, 4)}
82+
83+
# Prepare inputs
84+
{self._indent(input_code, 4)}
85+
86+
# Extract graph
87+
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
88+
model = model.to(device).eval()
89+
90+
# Move inputs to same device as model
91+
inputs = {{k: v.to(device) for k, v in inputs.items()}}
92+
93+
wrapped = graph_net.torch.extract(name="{model_metadata.model_id}", dynamic=True)(model).eval()
94+
95+
with torch.no_grad():
96+
wrapped(**inputs)
97+
98+
if __name__ == "__main__":
99+
main()
100+
'''
101+
return code
102+
103+
def _generate_model_loader(self, model_dir: Path, model_metadata: ModelMetadata) -> str:
104+
"""Generate model loading code based on model type"""
105+
model_path = str(model_dir).replace("\\", "/")
106+
107+
if model_metadata.model_type in ['bert', 'gpt', 't5', 'roberta']:
108+
return f'model = AutoModel.from_pretrained("{model_path}")'
109+
elif model_metadata.model_type in ['resnet', 'vgg', 'densenet']:
110+
return f'model = torchvision.models.{model_metadata.model_type}(pretrained=True)'
111+
else:
112+
# Generic loading
113+
return f'model = AutoModel.from_pretrained("{model_path}")'
114+
115+
def _generate_input_code(self, model_metadata: ModelMetadata) -> str:
116+
"""Generate input tensor construction code based on model metadata"""
117+
lines = ['inputs = {}']
118+
119+
for name, shape in model_metadata.input_shapes.items():
120+
dtype = model_metadata.input_dtypes.get(name, "int64")
121+
torch_dtype = self._get_torch_dtype(dtype)
122+
shape_tuple = f"({', '.join(map(str, shape))})"
123+
124+
if dtype == "int64":
125+
if "input_ids" in name.lower():
126+
safe_vocab_size = self._calculate_safe_vocab_size(model_metadata)
127+
lines.append(f'inputs["{name}"] = torch.randint(0, {safe_vocab_size}, {shape_tuple}, dtype={torch_dtype})')
128+
else:
129+
lines.append(f'inputs["{name}"] = torch.ones({shape_tuple}, dtype={torch_dtype})')
130+
else:
131+
lines.append(f'inputs["{name}"] = torch.randn({shape_tuple}, dtype={torch_dtype})')
132+
133+
return "\n".join(lines)
134+
135+
def _get_torch_dtype(self, dtype: str) -> str:
136+
"""Convert dtype string to torch dtype"""
137+
if dtype == "int64":
138+
return "torch.int64"
139+
elif dtype == "float32":
140+
return "torch.float32"
141+
else:
142+
return f"torch.{dtype}"
143+
144+
def _calculate_safe_vocab_size(self, model_metadata: ModelMetadata) -> int:
145+
"""Calculate safe vocabulary size for input generation"""
146+
if model_metadata.embedding_size:
147+
return max(MIN_SAFE_VOCAB_SIZE, model_metadata.embedding_size - 1)
148+
149+
vocab_size = model_metadata.vocab_size or DEFAULT_VOCAB_SIZE
150+
model_type = (model_metadata.model_type or "").lower()
151+
152+
# Model-type-specific limits
153+
if self._is_large_vocab_model_type(model_type):
154+
return FIXED_SAFE_LIMIT
155+
156+
# Size-based strategy
157+
if vocab_size > LARGE_VOCAB_THRESHOLD:
158+
return FIXED_SAFE_LIMIT
159+
elif vocab_size > MEDIUM_VOCAB_THRESHOLD:
160+
return max(MIN_SAFE_VOCAB_SIZE, int(vocab_size * LARGE_VOCAB_RATIO))
161+
elif vocab_size > SMALL_VOCAB_THRESHOLD:
162+
return max(MIN_SAFE_VOCAB_SIZE, int(vocab_size * MEDIUM_VOCAB_RATIO))
163+
else:
164+
return max(MIN_SAFE_VOCAB_SIZE, int(vocab_size * SMALL_VOCAB_RATIO))
165+
166+
def _is_large_vocab_model_type(self, model_type: str) -> bool:
167+
"""Check if model type typically has large vocabulary but small embedding"""
168+
return "xlm-roberta" in model_type or "xlm_roberta" in model_type or "roberta" in model_type
169+
170+
def _indent(self, text: str, spaces: int) -> str:
171+
"""Indent text by specified spaces"""
172+
indent = " " * spaces
173+
return "\n".join(indent + line for line in text.split("\n"))
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Computation graph extraction modules"""
2+
3+
from graph_net.agent.graph_extractor.base import BaseGraphExtractor
4+
from graph_net.agent.graph_extractor.subprocess_graph_extractor import SubprocessGraphExtractor
5+
6+
__all__ = ["BaseGraphExtractor", "SubprocessGraphExtractor"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Base graph extractor interface"""
2+
3+
from abc import ABC, abstractmethod
4+
from pathlib import Path
5+
6+
7+
class BaseGraphExtractor(ABC):
8+
"""Base class for computation graph extractors"""
9+
10+
@abstractmethod
11+
def extract(self, code_path: Path, model_id: str) -> Path:
12+
"""
13+
Execute script and extract computation graph
14+
15+
Args:
16+
code_path: Path to run_model.py script
17+
model_id: Model ID for output directory naming
18+
19+
Returns:
20+
Path to extracted sample directory
21+
22+
Raises:
23+
ExtractionError: If extraction fails
24+
"""
25+
pass

0 commit comments

Comments
 (0)