Skip to content

Commit a375d7f

Browse files
dengwx2026dengwx2009claude
authored
feat(datasets): add datasets SDK and CLI commands for OSS registry (#859)
* feat: add docs * feat: update docs * docs(datasets): update design doc to reuse OssRegistryInfo from bench Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(datasets): add models and package skeleton Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(datasets): add BaseDatasetRegistry ABC Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(datasets): add OssDatasetRegistry with list_datasets and upload_dataset Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(datasets): add DatasetClient Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(datasets): extend ConfigManager with [dataset] section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(datasets): add DatasetsCommand (list + upload) * feat: refine DatasetSpec * fix: test fail --------- Co-authored-by: dengwx <wanxi.dengwx@alibaba-inc.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 15a1799 commit a375d7f

15 files changed

Lines changed: 1074 additions & 0 deletions

File tree

docs/dev/envhub/README.md

Lines changed: 395 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,395 @@
1+
# EnvHub & Dataset 开发设计文档
2+
3+
## 目录
4+
5+
- [EnvHub(现有)](#envhub现有)
6+
- [Dataset 功能设计](#dataset-功能设计)
7+
- [背景与目标](#背景与目标)
8+
- [OSS 路径约定](#oss-路径约定)
9+
- [模块结构](#模块结构)
10+
- [核心模型](#核心模型)
11+
- [Registry 抽象层](#registry-抽象层)
12+
- [OssDatasetRegistry](#ossdatasetregistry)
13+
- [DatasetClient](#datasetclient)
14+
- [CLI 命令](#cli-命令)
15+
- [配置文件扩展](#配置文件扩展)
16+
- [数据流](#数据流)
17+
- [错误处理](#错误处理)
18+
- [测试策略](#测试策略)
19+
20+
---
21+
22+
## EnvHub(现有)
23+
24+
EnvHub 是 ROCK 的环境管理服务,提供 Docker 环境的注册、查询、列举和删除功能。
25+
26+
| 入口 | 模块 | 说明 |
27+
|----------------|-----------------------------|------------------------------|
28+
| `envhub` 服务 | `rock.envhub.server` | FastAPI 服务,端口 8081 |
29+
| SDK Client | `rock.sdk.envhub.client` | `EnvHubClient`,HTTP 调用服务 |
30+
31+
REST 端点:`POST /env/register``POST /env/get``POST /env/list``POST /env/delete``GET /health`
32+
33+
---
34+
35+
## Dataset 功能设计
36+
37+
### 背景与目标
38+
39+
在 ROCK 中引入 dataset 管理能力,核心目标如下:
40+
41+
**1. 约束 datasets**
42+
43+
统一 dataset 的路径约定、命名规范和存储格式,避免各服务自行散落地写 OSS 路径。所有 dataset 必须遵循 `datasets/{organization}/{dataset_name}/{split}/{task_id}/` 结构,由本模块作为唯一入口强制执行。
44+
45+
**2. 提供 SDK 和 CLI 供其他服务集成**
46+
47+
- **SDK**`rock.sdk.envhub.datasets`):提供 `DatasetClient`,供 Python 代码直接调用 list / upload,适合 admin、job 等服务在流程中集成 dataset 操作。
48+
- **CLI**`rock datasets`):提供 `list``upload` 子命令,供运维、研究人员在终端操作 dataset,也适合脚本化批量处理。
49+
50+
**3. 为后续权限管理预留扩展点**
51+
52+
当前阶段 CLI 直接对接 OSS,不经过 envhub server。后续可在 envhub server 增加 `/datasets/*` 端点,在 SDK/CLI 与 OSS 之间插入权限校验、审计日志等能力,Registry 抽象层的设计为此预留了扩展空间。
53+
54+
---
55+
56+
### OSS 路径约定
57+
58+
路径层级设计对齐 **HuggingFace Datasets** 的命名惯例(`{organization}/{dataset_name}/{split}`),在此基础上增加了 ROCK 特有的 `{task_id}` 层来组织结构化的 benchmark task 目录。
59+
60+
```
61+
oss://{bucket}/datasets/{organization}/{dataset_name}/{split}/{task_id}/
62+
```
63+
64+
| 层级 | 说明 | 类比 HuggingFace |
65+
|------|------|-----------------|
66+
| `organization` | 数据集所属组织,如 `qwen``alibaba` | HF namespace(`qwen/`|
67+
| `dataset_name` | 数据集名称 | HF repo name(`my-bench`|
68+
| `split` | 分片标识,如 `train``test``v1.0` | HF split(`train`/`test`|
69+
| `task_id` | 单个 task 目录名(ROCK 特有) | HF 无此层,HF 直接存数据文件 |
70+
71+
示例:
72+
73+
```
74+
oss://my-bucket/
75+
└── datasets/
76+
└── qwen/ # organization
77+
└── my-bench/ # dataset_name
78+
└── train/ # split
79+
├── task-001/ # task_id(ROCK 特有)
80+
│ ├── task.toml
81+
│ └── tests/
82+
└── task-002/
83+
├── task.toml
84+
└── tests/
85+
```
86+
87+
task 目录内的文件结构原样保留(相对路径不变),上传和下载均以 `task_id/` 为单位。
88+
89+
---
90+
91+
### 模块结构
92+
93+
```
94+
rock/
95+
├── sdk/
96+
│ └── envhub/
97+
│ ├── client.py # 现有:EnvHubClient
98+
│ ├── config.py # 现有
99+
│ ├── schema.py # 现有
100+
│ └── datasets/ # 新增
101+
│ ├── __init__.py # 对外入口:暴露 DatasetClient、DatasetSpec 等
102+
│ ├── models.py # DatasetSpec, UploadResult(OssRegistryInfo 复用自 bench)
103+
│ ├── client.py # DatasetClient(对外统一入口)
104+
│ └── registry/
105+
│ ├── __init__.py
106+
│ ├── base.py # BaseDatasetRegistry ABC
107+
│ └── oss.py # OssDatasetRegistry
108+
└── cli/
109+
└── command/
110+
└── datasets.py # DatasetsCommand(继承 Command ABC)
111+
```
112+
113+
---
114+
115+
### 核心模型
116+
117+
**复用模型(来自 `rock.sdk.bench.models.job.config`,不新增、不移动)**
118+
119+
| 模型 | 关键字段 | 用途 |
120+
|------|----------|------|
121+
| `OssRegistryInfo` | `oss_bucket`, `oss_endpoint`, `oss_region`, `oss_access_key_id`, `oss_access_key_secret`, `oss_dataset_path` | OSS 连接凭证与路径前缀(`oss_dataset_path` 默认 `"datasets"`|
122+
| `LocalDatasetConfig` | `path: Path` | 本地 task 目录(upload 数据源) |
123+
| `RegistryDatasetConfig` | `name="org/dataset_name"`, `version=split`, `overwrite`, `registry=OssRegistryInfo(...)` | 远端数据集引用(upload 目标) |
124+
125+
`RegistryDatasetConfig.name` 遵循 HuggingFace 惯例,使用 `"{organization}/{dataset_name}"` 格式;`OssDatasetRegistry` 通过 `name.split("/", 1)` 拆分得到 org 和 name。`version` 对应 split(如 `"train"``"test"`)。`DatasetSpec.id` / `UploadResult.id` 同样使用此格式,对齐 HF `DatasetInfo.id`
126+
127+
**新增模型(`rock/sdk/envhub/datasets/models.py`**
128+
129+
```python
130+
@dataclass
131+
class DatasetSpec:
132+
id: str # "{organization}/{dataset_name}",对齐 HF DatasetInfo.id,如 "princeton-nlp/SWE-bench_Verified"
133+
split: str
134+
task_ids: list[str]
135+
136+
@dataclass
137+
class UploadResult:
138+
id: str # "{organization}/{dataset_name}"
139+
split: str
140+
uploaded: int # 成功上传的文件数
141+
skipped: int # 已存在跳过的 task 数(overwrite=False)
142+
failed: int # 失败数
143+
```
144+
145+
---
146+
147+
### Registry 抽象层
148+
149+
**`rock/sdk/envhub/datasets/registry/base.py`**
150+
151+
```python
152+
class BaseDatasetRegistry(ABC):
153+
154+
@abstractmethod
155+
def list_datasets(self, organization: str | None = None) -> list[DatasetSpec]:
156+
"""枚举 registry 中的所有 datasets。
157+
organization 不为 None 时只返回该 org 下的 datasets。
158+
"""
159+
...
160+
161+
@abstractmethod
162+
def upload_dataset(
163+
self,
164+
source: LocalDatasetConfig,
165+
target: RegistryDatasetConfig,
166+
concurrency: int = 4,
167+
) -> UploadResult:
168+
"""将 source.path/{task_id}/ 批量上传到 target 指定的远端路径。
169+
org/name/split/overwrite 均从 target 提取。
170+
"""
171+
...
172+
```
173+
174+
---
175+
176+
### OssDatasetRegistry
177+
178+
**`rock/sdk/envhub/datasets/registry/oss.py`**
179+
180+
```python
181+
class OssDatasetRegistry(BaseDatasetRegistry):
182+
def __init__(self, registry: OssRegistryInfo): ...
183+
```
184+
185+
**路径构建:**
186+
187+
```python
188+
def _build_prefix(self, org: str, name: str, split: str | None = None) -> str:
189+
base = self._registry.oss_dataset_path or "datasets"
190+
parts = [base, org, name]
191+
if split:
192+
parts.append(split)
193+
return "/".join(parts)
194+
# → "datasets/qwen/my-bench/train"
195+
```
196+
197+
**list_datasets 逻辑:**
198+
199+
1.`datasets/` 为前缀列出三级目录(org → name → split)
200+
2. 对每个 `datasets/{org}/{name}/{split}/`,列出直接子目录作为 `task_ids`
201+
3. 返回 `list[DatasetSpec]`
202+
203+
OSS 列举使用 `list_objects_v2` with `delimiter="/"` 逐层枚举目录,避免全量遍历。
204+
205+
**upload_dataset 逻辑:**
206+
207+
1.`target.name.split("/", 1)` 提取 `org``name``target.version``split``target.overwrite` 为覆盖标志
208+
2. 遍历 `source.path` 下的一级子目录,每个子目录视为一个 task(`task_id = subdir.name`
209+
3.`target.overwrite=False` 且 OSS 上已存在该 task 目录,跳过
210+
4. 并发上传(`ThreadPoolExecutor``concurrency` 控制并发数)
211+
5. 目标 key:`datasets/{org}/{name}/{split}/{task_id}/{relative_file_path}`
212+
6. 返回 `UploadResult`
213+
214+
---
215+
216+
### DatasetClient
217+
218+
**`rock/sdk/envhub/datasets/client.py`**
219+
220+
薄封装层,负责从配置创建 registry 并提供面向业务的方法。
221+
222+
```python
223+
class DatasetClient:
224+
def __init__(self, registry: OssRegistryInfo):
225+
self._registry = OssDatasetRegistry(registry)
226+
227+
def list_datasets(self, org: str | None = None) -> list[DatasetSpec]:
228+
return self._registry.list_datasets(org)
229+
230+
def upload_dataset(
231+
self,
232+
source: LocalDatasetConfig,
233+
target: RegistryDatasetConfig,
234+
concurrency: int = 4,
235+
) -> UploadResult:
236+
return self._registry.upload_dataset(source, target, concurrency)
237+
```
238+
239+
---
240+
241+
### CLI 命令
242+
243+
**`rock/cli/command/datasets.py`**,继承 `Command` ABC,`name = "datasets"`
244+
245+
#### rock datasets list
246+
247+
```
248+
rock datasets list [OPTIONS]
249+
250+
Options:
251+
--org TEXT 只列出指定 organization 的 datasets
252+
--bucket TEXT OSS bucket 名称(覆盖 config.ini)
253+
--endpoint TEXT OSS endpoint(覆盖 config.ini)
254+
--access-key-id TEXT OSS access key ID(覆盖 config.ini)
255+
--access-key-secret TEXT OSS access key secret(覆盖 config.ini)
256+
```
257+
258+
输出示例:
259+
260+
```
261+
Dataset Split Tasks
262+
qwen/my-bench train 42
263+
qwen/my-bench test 10
264+
alibaba/code-eval train 100
265+
```
266+
267+
#### rock datasets upload
268+
269+
```
270+
rock datasets upload [OPTIONS]
271+
272+
Required:
273+
--org TEXT Organization 名称
274+
--dataset TEXT Dataset 名称
275+
--split TEXT Split 名称(如 train、test、v1.0)
276+
--dir PATH 本地 task 目录(内含 {task_id}/ 子目录)
277+
278+
Options:
279+
--bucket TEXT OSS bucket(覆盖 config.ini)
280+
--endpoint TEXT OSS endpoint(覆盖 config.ini)
281+
--access-key-id TEXT OSS access key ID(覆盖 config.ini)
282+
--access-key-secret TEXT OSS access key secret(覆盖 config.ini)
283+
--concurrency INT 并发上传数(默认 4,范围 1-16)
284+
--overwrite 覆盖 OSS 上已存在的 task 目录(默认跳过)
285+
```
286+
287+
输出示例:
288+
289+
```
290+
Uploading to oss://my-bucket/datasets/qwen/my-bench/train/
291+
✓ task-001 (5 files)
292+
✓ task-002 (5 files)
293+
- task-003 skipped (already exists)
294+
295+
Done: 2 uploaded, 1 skipped, 0 failed
296+
```
297+
298+
---
299+
300+
### 配置文件扩展
301+
302+
`.rock/config.ini` 新增 `[dataset]` section,用于存储 OSS 凭证默认值:
303+
304+
```ini
305+
[rock]
306+
base_url = http://localhost:8080
307+
308+
[dataset]
309+
oss_bucket = my-bucket
310+
oss_endpoint = https://oss-cn-hangzhou.aliyuncs.com
311+
oss_access_key_id = LTAI5t...
312+
oss_access_key_secret = xxxxxxx
313+
```
314+
315+
**优先级(高→低)**:CLI 参数 > `config.ini [dataset]` section > 报错(必填项缺失)
316+
317+
`ConfigManager` 扩展:在 `CLIConfig` 新增 `dataset_config: DatasetConfig` 字段(内部结构体),读取 `[dataset]` section 中的 OSS 凭证。`DatasetsCommand` 在初始化时合并 `DatasetConfig` + CLI args 构建 `OssRegistryInfo`(来自 `rock.sdk.bench.models.job.config`)。
318+
319+
---
320+
321+
### 数据流
322+
323+
**list:**
324+
325+
```
326+
rock datasets list --org qwen
327+
└─ DatasetCommand.list()
328+
├─ ConfigManager.get_dataset_config() # 读 config.ini [dataset]
329+
├─ 合并 CLI 参数 → OssRegistryInfo
330+
├─ DatasetClient(config)
331+
└─ OssDatasetRegistry.list_datasets(org="qwen")
332+
└─ alibabacloud_oss_v2: list_objects_v2(prefix="datasets/qwen/", delimiter="/")
333+
→ 枚举 name/split 层
334+
→ 构建 DatasetSpec 列表
335+
→ 打印表格
336+
```
337+
338+
**upload:**
339+
340+
```
341+
rock datasets upload --org qwen --dataset my-bench --split train --dir ./tasks/
342+
└─ DatasetsCommand.upload()
343+
├─ ConfigManager.get_config().dataset_config # 读 config.ini [dataset]
344+
├─ 合并 CLI 参数 → OssRegistryInfo
345+
├─ source = LocalDatasetConfig(path=./tasks/)
346+
├─ target = RegistryDatasetConfig(
347+
│ name="qwen/my-bench", version="train",
348+
│ overwrite=False, registry=OssRegistryInfo)
349+
├─ DatasetClient(registry=OssRegistryInfo)
350+
└─ OssDatasetRegistry.upload_dataset(source, target, concurrency=4)
351+
├─ target.name.split("/", 1) → org="qwen", name="my-bench"
352+
├─ target.version → split="train"
353+
├─ 遍历 source.path 下子目录:task-001/, task-002/, ...
354+
├─ ThreadPoolExecutor(max_workers=concurrency)
355+
└─ 每个 task:
356+
├─ 若 target.overwrite=False 且 OSS 存在 → skip
357+
└─ 遍历文件 → PutObject(key="datasets/qwen/my-bench/train/task-001/{file}")
358+
```
359+
360+
---
361+
362+
### 错误处理
363+
364+
| 场景 | 行为 |
365+
|------|------|
366+
| OSS 凭证缺失 | 启动时立即报错,提示配置 `[dataset]` section 或传 CLI 参数 |
367+
| OSS 权限错误(401/403) | 立即抛出,打印明确错误信息,不重试 |
368+
| OSS 网络错误(5xx/timeout) | 指数退避重试(最多 3 次),超限后报错 |
369+
| `--dir` 不存在或为空 | 命令入口检查,立即报错 |
370+
| 单个 task 上传失败 | 记录到 `UploadResult.failed`,继续上传其他 tasks,命令结束后汇总报告 |
371+
| `--org`/`--dataset`/`--split` 缺失 | argparse required 校验,自动报错 |
372+
373+
---
374+
375+
### 测试策略
376+
377+
| 测试类型 | 覆盖范围 | 标记 |
378+
|----------|----------|------|
379+
| 单元测试 | `OssDatasetRegistry` 路径构建、`DatasetSpec` 模型、`ConfigManager` 解析 `[dataset]` | 无特殊标记 |
380+
| 集成测试(mock OSS) | `list_datasets``upload_dataset` 逻辑,使用 `unittest.mock` mock OSS SDK | `@pytest.mark.integration` |
381+
| 集成测试(真实 OSS) | 端到端 upload → list 验证 | `@pytest.mark.need_admin`(需要 OSS 凭证) |
382+
383+
测试文件位置:
384+
385+
```
386+
tests/
387+
├── unit/
388+
│ └── datasets/
389+
│ ├── test_models.py
390+
│ ├── test_oss_registry.py
391+
│ └── test_config.py
392+
└── integration/
393+
└── datasets/
394+
└── test_oss_e2e.py
395+
```

0 commit comments

Comments
 (0)