Skip to content

Commit a218725

Browse files
committed
[tools] Introduce opm (one package manger)
It introduces opm tools for building package from pytorch. ONE-DCO-1.0-Signed-off-by: Sanggyu Lee <sg5.lee@samsung.com>
1 parent cd4e7de commit a218725

File tree

10 files changed

+431
-0
lines changed

10 files changed

+431
-0
lines changed

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,9 @@ dev = [
2222
"pre-commit==4.4.0",
2323
]
2424

25+
[tool.uv.workspace]
26+
members = [
27+
"tools/opm",
28+
]
29+
2530
# This file does not describe packaging details for compiler and runtime

tools/opm/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# OPM (ONE or circle Package Manager)
2+
3+
`opm` is a set of utility scripts to simplify the process of creating GGMA packages for ONE runtime. It handles environment setup, model downloading, and the export pipeline.
4+
5+
## Usage
6+
7+
The tools are designed to be run via the `opm` wrapper script (or directly via python if preferred).
8+
9+
### 1. Init
10+
11+
Initialize the environment. This creates a virtual environment, installs dependencies (including a CPU-only version of torch to avoid large downloads), and fetches the necessary `o2o` tools from the ONE repository.
12+
13+
```bash
14+
$ opm init
15+
```
16+
17+
### 2. Import
18+
19+
Download a model.
20+
21+
```bash
22+
$ opm import <model_id|url> [-r <requirements_file>]
23+
```
24+
25+
- `<model_id|url>`: HuggingFace model ID (e.g., `Maykeye/TinyLLama-v0`) or direct URL.
26+
- `-r, --requirements`: (Optional) Path to a requirements file to install specific dependencies for the model.
27+
28+
Example:
29+
```bash
30+
$ opm import Maykeye/TinyLLama-v0 -r tinyllama/tinyllama.requirements
31+
```
32+
33+
### 3. Export
34+
35+
Export the downloaded model to a GGMA package (`.circle` file + tokenizer). This runs the specified export script and pipeline configuration.
36+
37+
```bash
38+
$ opm export -s <export_script> -p <pipeline_config>
39+
```
40+
41+
- `-s, --script`: Path to the python export script (e.g., `tinyllama/tinyllama.py`).
42+
- `-p, --pipeline`: Path to the pipeline configuration YAML file (e.g., `tinyllama/tinyllama.pipeline`).
43+
44+
Example:
45+
```bash
46+
$ opm export -s tinyllama/tinyllama.py -p tinyllama/tinyllama.pipeline
47+
```
48+
49+
### 4. Clean
50+
51+
Clean up build artifacts.
52+
53+
```bash
54+
$ opm clean [--all]
55+
```
56+
57+
- Default: Removes the `build/` directory.
58+
- `--all`: Removes `build/`, `venv/`, `o2o/`, and `TICO/` (full reset).

tools/opm/clean.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#!/usr/bin/env python3
2+
import shutil
3+
import os
4+
5+
import argparse
6+
from config import VENV_DIR, O2O_DIR, BUILD_DIR
7+
8+
9+
def remove_dir(path):
10+
"""Remove directory if it exists, print message only if removed."""
11+
try:
12+
shutil.rmtree(path)
13+
print(f"Removing {path} directory...")
14+
except FileNotFoundError:
15+
pass
16+
17+
18+
def main():
19+
parser = argparse.ArgumentParser(description="Clean build artifacts")
20+
parser.add_argument("--all",
21+
action="store_true",
22+
help="Remove all generated files including venv and o2o")
23+
args = parser.parse_args()
24+
25+
# Always remove build directory
26+
remove_dir(BUILD_DIR)
27+
28+
if args.all:
29+
remove_dir(O2O_DIR)
30+
remove_dir(VENV_DIR)
31+
print("Full clean complete.")
32+
else:
33+
print("Clean complete.")
34+
35+
36+
if __name__ == "__main__":
37+
main()

tools/opm/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# OPM Constants
2+
VENV_DIR = "venv"
3+
O2O_DIR = "o2o"
4+
BUILD_DIR = "build"

tools/opm/export.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#!/usr/bin/env python3
2+
import argparse
3+
import shutil
4+
import json
5+
import yaml
6+
import os
7+
import sys
8+
from config import O2O_DIR, BUILD_DIR
9+
from util import run_cmd
10+
11+
12+
def main():
13+
parser = argparse.ArgumentParser(description="Export model to GGMA package")
14+
parser.add_argument("-s",
15+
"--script",
16+
required=True,
17+
help="Export script to use (e.g., tinyllama.py)")
18+
parser.add_argument("-p",
19+
"--pipeline",
20+
default="pipeline.yaml",
21+
help="Pipeline configuration file (default: pipeline.yaml)")
22+
args = parser.parse_args()
23+
24+
export_script_name = args.script
25+
pipeline_file = args.pipeline
26+
27+
# Change to build directory
28+
if not os.path.exists(BUILD_DIR):
29+
print(f"Error: {BUILD_DIR} directory does not exist. Run 'opm import' first.")
30+
return
31+
32+
os.chdir(BUILD_DIR)
33+
34+
# Load pipeline configuration
35+
pipeline_path = os.path.abspath(pipeline_file)
36+
if not os.path.exists(pipeline_path):
37+
print(f"Error: Pipeline file {pipeline_path} not found.")
38+
return
39+
40+
with open(pipeline_path, "r") as f:
41+
pipeline_config = yaml.safe_load(f)
42+
43+
# Find model directory by config.json
44+
model_dir = None
45+
for d in os.listdir("."):
46+
config_path = os.path.join(d, "config.json")
47+
if os.path.isdir(d) and os.path.exists(config_path):
48+
model_dir = d
49+
print(f"Using local model directory: {model_dir}")
50+
break
51+
52+
if not model_dir:
53+
raise ValueError("No local model directory found (directory with config.json)")
54+
55+
# Add o2o tools and the current directory (where pipeline scripts reside) to PATH
56+
env = os.environ.copy()
57+
cwd_path = os.path.abspath(os.getcwd())
58+
o2o_path = os.path.abspath(os.path.join(cwd_path, "..", O2O_DIR))
59+
env["PATH"] = f"{o2o_path}:{cwd_path}:{env['PATH']}"
60+
os.environ.update(env)
61+
62+
# Use current python executable instead of finding venv python
63+
python_bin = sys.executable
64+
export_script = os.path.join("..", export_script_name)
65+
66+
# 1. Generate prefill and decode circles
67+
print(f"Running {export_script_name} (prefill)...")
68+
run_cmd(f"{python_bin} {export_script} --mode prefill", env=env)
69+
70+
print(f"Running {export_script_name} (decode)...")
71+
run_cmd(f"{python_bin} {export_script} --mode decode", env=env)
72+
73+
# Helper to run pipeline command
74+
def run_pipeline_step(step_name):
75+
if step_name in pipeline_config:
76+
print(f"Running {step_name} pipeline...")
77+
cmd = pipeline_config[step_name]
78+
# If cmd is a multiline string (from YAML |), it might contain newlines.
79+
# We can replace newlines with spaces or let the shell handle it if it's a single command string.
80+
# For safety with pipes, we replace newlines with spaces if they are meant to be a single line command.
81+
# But YAML block scalar preserves newlines.
82+
# If the user wrote it with pipes at the start of lines, we should join them.
83+
cmd = cmd.replace("\n", " ")
84+
run_cmd(cmd, env=env)
85+
86+
# 2. Pipeline (decode)
87+
run_pipeline_step("decode")
88+
89+
# 3. Merge
90+
run_pipeline_step("merge")
91+
92+
# 4. Create package directory and copy files
93+
# Find source directory with tokenizer.json
94+
source_dir = None
95+
for d in os.listdir("."):
96+
if os.path.isdir(d) and os.path.exists(os.path.join(d, "tokenizer.json")):
97+
source_dir = d
98+
break
99+
100+
if source_dir:
101+
package_dir = "out"
102+
print(f"Creating package directory {package_dir}...")
103+
os.makedirs(package_dir, exist_ok=True)
104+
105+
# Copy tokenizer and config files
106+
for filename in ["tokenizer.json", "tokenizer.model", "config.json"]:
107+
src = os.path.join(source_dir, filename)
108+
if os.path.exists(src):
109+
shutil.copy2(src, package_dir)
110+
111+
# Move model.circle
112+
print(f"Moving model.circle to {package_dir}...")
113+
shutil.move("model.circle", os.path.join(package_dir, "model.circle"))
114+
else:
115+
print(
116+
"Warning: Could not find source directory (directory with tokenizer.json). Leaving model.circle in current dir."
117+
)
118+
119+
120+
if __name__ == "__main__":
121+
main()

tools/opm/import.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env python3
2+
import sys
3+
import os
4+
import argparse
5+
import subprocess
6+
import httpx
7+
import urllib.request
8+
from huggingface_hub import snapshot_download
9+
from config import BUILD_DIR
10+
11+
12+
def main():
13+
parser = argparse.ArgumentParser(description="Import model from HuggingFace")
14+
parser.add_argument("model_id", help="HuggingFace model ID")
15+
parser.add_argument(
16+
"-r",
17+
"--requirements",
18+
help="Path to requirements file to install (default: requirements.txt)")
19+
args = parser.parse_args()
20+
21+
model_id = args.model_id
22+
model_basename = model_id.split("/")[-1].lower()
23+
24+
# Create build directory
25+
os.makedirs(BUILD_DIR, exist_ok=True)
26+
27+
# Download into build directory
28+
target_dir = os.path.join(BUILD_DIR, model_basename)
29+
print(f"Downloading model files for {model_id} into {target_dir}...")
30+
31+
# Patch httpx to disable SSL verification and forward proxy if set
32+
original_client = httpx.Client
33+
proxy = urllib.request.getproxies()
34+
35+
def patched_client(*args, **kwargs):
36+
# Always disable SSL verification (needed for self‑signed certs)
37+
kwargs.setdefault('verify', False)
38+
# If a proxy is defined, pass it to the client
39+
if proxy:
40+
kwargs.setdefault('proxies', proxy)
41+
return original_client(*args, **kwargs)
42+
43+
httpx.Client = patched_client
44+
45+
try:
46+
snapshot_download(repo_id=model_id, local_dir=target_dir)
47+
finally:
48+
# Restore original httpx.Client
49+
httpx.Client = original_client
50+
51+
# Install requirements
52+
requirements_file = args.requirements if args.requirements else "requirements.txt"
53+
54+
requirements_path = os.path.abspath(requirements_file)
55+
if not os.path.exists(requirements_path):
56+
print(f"Error: {requirements_path} not found.")
57+
return
58+
59+
60+
if __name__ == "__main__":
61+
main()

tools/opm/init.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
import os
3+
import shutil
4+
import subprocess
5+
import venv
6+
from config import VENV_DIR, O2O_DIR, BUILD_DIR
7+
from util import run_cmd
8+
9+
# PR-related constants (used only in init.py)
10+
PR_WORKTREE = "_pr_16233"
11+
PR_BRANCH = "pr-16233"
12+
PR_REF = "refs/pull/16233/head"
13+
14+
15+
def main():
16+
# 1. Create virtual environment
17+
if not os.path.exists(VENV_DIR):
18+
print(f"Creating virtual environment at {VENV_DIR}...")
19+
venv.create(VENV_DIR, with_pip=True)
20+
else:
21+
print(f"Virtual environment already exists at {VENV_DIR}.")
22+
23+
# 2. Install opm requirements
24+
pip_cmd = os.path.join(VENV_DIR, "bin", "pip")
25+
# By installing Torch (cpu), it prevents TICO from pulling the large CUDA-enabled PyTorch package
26+
run_cmd(f"{pip_cmd} install torch --index-url https://download.pytorch.org/whl/cpu")
27+
run_cmd(f"{pip_cmd} install tico==0.1.0.dev251125")
28+
29+
# 3. Prepare build directory for temporary worktree
30+
os.makedirs(BUILD_DIR, exist_ok=True)
31+
worktree_path = os.path.join(BUILD_DIR, PR_WORKTREE)
32+
33+
# 4. Git worktree for PR and o2o extraction
34+
if not os.path.exists(worktree_path):
35+
# Fetch PR only if worktree doesn't exist
36+
try:
37+
run_cmd(f"git fetch https://github.com/Samsung/ONE.git {PR_REF}:{PR_BRANCH}")
38+
except subprocess.CalledProcessError:
39+
print("Fetch failed, possibly branch already exists. Continuing...")
40+
41+
# Create worktree with no checkout
42+
run_cmd(f"git worktree add --no-checkout -f {worktree_path} {PR_BRANCH}")
43+
44+
# Configure sparse checkout
45+
cwd = os.getcwd()
46+
try:
47+
os.chdir(worktree_path)
48+
run_cmd("git sparse-checkout init --cone")
49+
run_cmd(f"git sparse-checkout set tools/{O2O_DIR}")
50+
# Populate files
51+
run_cmd(f"git checkout {PR_BRANCH}")
52+
finally:
53+
os.chdir(cwd)
54+
55+
# 5. Move o2o to current directory
56+
if not os.path.exists(O2O_DIR):
57+
src_o2o = os.path.join(worktree_path, "tools", O2O_DIR)
58+
if os.path.exists(src_o2o):
59+
print(f"Moving o2o tools to {O2O_DIR}...")
60+
shutil.move(src_o2o, O2O_DIR)
61+
else:
62+
print("o2o tools not found in worktree.")
63+
64+
# 6. Remove temporary worktree
65+
if os.path.exists(worktree_path):
66+
print("Removing temporary worktree...")
67+
run_cmd(f"git worktree remove --force {worktree_path}")
68+
69+
# 7. Make tools executable
70+
if os.path.exists(O2O_DIR):
71+
run_cmd(f"chmod +x {O2O_DIR}/*.py")
72+
73+
print("opm init completed.")
74+
75+
76+
if __name__ == "__main__":
77+
main()

0 commit comments

Comments
 (0)