Skip to content

Commit ca80e3d

Browse files
author
Jeff Yang
authored
feat: single model, single optimizer template (#34)
* feat: add single model, single optimizer template * Update README.md * feat: archive as python package system * feat: add hubconf, tests for single, TODOs tip * tip improvement * readme improve, script for find and replace * kwargs to Checkpoint handler * mimic tree * feat: more utils functions and tests * feat: handlers usage in main.py, more comments * feat: add custom additional events, fix #30 * fix: all in 1 deps install, amp option fix #32 * fix: model, opt, loss, lr_s from initialize, fix #32 * chore: up README about distributed launching * feat: add resume_from command line argument to resume from checkpoint, fix #31 * fix: comments and docstring
1 parent c5e511e commit ca80e3d

31 files changed

+1776
-169
lines changed

.github/run_code_style.sh

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22

33
set -xeu
44

5-
if [ $1 = "lint" ]; then
5+
if [ $1 == "lint" ]; then
66
flake8 app templates tests --config .flake8
77
isort app templates tests --check --settings pyproject.toml
88
black app templates tests --check --config pyproject.toml
9-
elif [ $1 = "fmt" ]; then
9+
elif [ $1 == "fmt" ]; then
1010
isort app templates tests --color --settings pyproject.toml
1111
black app templates tests --config pyproject.toml
12-
elif [ $1 = "install" ]; then
12+
elif [ $1 == "install" ]; then
1313
pip install flake8 "black==20.8b1" "isort==5.7.0"
1414
fi

.github/run_test.sh

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
set -xeu
44

5-
if [ $1 = "generate" ]; then
5+
if [ $1 == "generate" ]; then
66
python ./tests/generate.py
7-
elif [ $1 = "unittest" ]; then
7+
elif [ $1 == "unittest" ]; then
88
pytest ./tests/unittest -vvv -ra --color=yes --durations=0
9-
elif [ $1 = "integration" ]; then
9+
elif [ $1 == "integration" ]; then
1010
for file in $(find ./tests/integration -iname "*.sh")
1111
do
1212
bash $file

app/codegen.py

+20-16
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def __init__(self, templates_dir: str = "./templates", dist_dir: str = "./dist")
1212
self.dist_dir = Path(dist_dir)
1313
self.template_list = [p.stem for p in self.templates_dir.iterdir() if p.is_dir() and not p.stem.startswith("_")]
1414
self.rendered_code = {t: {} for t in self.template_list}
15-
self.available_archive_formats = sorted(map(lambda x: x[0], shutil.get_archive_formats()), reverse=True)
15+
self.available_archive_formats = [x[0] for x in shutil.get_archive_formats()[::-1]]
1616

1717
def render_templates(self, template_name: str, config: dict):
1818
"""Renders all the templates files from template folder for the given config."""
@@ -31,27 +31,31 @@ def render_templates(self, template_name: str, config: dict):
3131
self.rendered_code[template_name][fname] = code
3232
yield fname, code
3333

34-
def mk_dist_template_dir(self, template_name: str):
35-
self.dist_template_dir = Path(f"{self.dist_dir}/{template_name}")
36-
self.dist_template_dir.mkdir(parents=True, exist_ok=True)
34+
def make_and_write(self, template_name: str):
35+
"""Make the directories first and write to the files"""
36+
for p in (self.templates_dir / template_name).rglob("*"):
37+
if not p.stem.startswith("_") and p.is_dir():
38+
# p is templates/template_name/...
39+
# remove "templates" from p.parts and join with "/", so we'll have
40+
# template_name/...
41+
p = "/".join(p.parts[1:])
42+
else:
43+
p = template_name
3744

38-
def write_file(self, fname: str, code: str) -> None:
39-
"""Creates `fname` with content `code` in `dist_dir/template_name`."""
40-
(self.dist_template_dir / fname).write_text(code)
45+
if not (self.dist_dir / p).is_dir():
46+
(self.dist_dir / p).mkdir(parents=True, exist_ok=True)
4147

42-
def write_files(self, template_name):
43-
"""Writes all rendered code for the specified template."""
44-
# Save files with rendered code to the disk
4548
for fname, code in self.rendered_code[template_name].items():
46-
self.write_file(fname, code)
49+
(self.dist_dir / template_name / fname).write_text(code)
4750

4851
def make_archive(self, template_name, archive_format):
4952
"""Creates dist dir with generated code, then makes the archive."""
50-
self.mk_dist_template_dir(template_name)
51-
self.write_files(template_name)
53+
54+
self.make_and_write(template_name)
5255
archive_fname = shutil.make_archive(
53-
base_name=str(self.dist_template_dir),
56+
base_name=template_name,
57+
root_dir=self.dist_dir,
5458
format=archive_format,
55-
base_dir=self.dist_template_dir,
59+
base_dir=template_name,
5660
)
57-
return archive_fname
61+
return shutil.move(archive_fname, self.dist_dir / archive_fname.split("/")[-1])

app/streamlit_app.py

+61-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import os
21
import shutil
32
from pathlib import Path
4-
from subprocess import check_output
53

64
import streamlit as st
75
from codegen import CodeGenerator
@@ -10,6 +8,22 @@
108
__version__ = "0.1.0"
119

1210

11+
FOLDER_TO_TEMPLATE_NAME = {
12+
"Single Model, Single Optimizer": "single",
13+
"Generative Adversarial Network": "gan",
14+
"Image Classification": "image_classification",
15+
}
16+
17+
TIP = """
18+
> **💡 TIP**
19+
>
20+
> To quickly adapt to the generated code structure, there are TODOs in the files that are needed to be edited.
21+
> [PyCharm TODO comments](https://www.jetbrains.com/help/pycharm/using-todo.html) or
22+
> [VSCode Todo Tree](https://marketplace.visualstudio.com/items?itemName=Gruntfuggly.todo-tree)
23+
> can help you find them easily.
24+
"""
25+
26+
1327
class App:
1428
page_title = "Code Generator"
1529
page_icon = "https://raw.githubusercontent.com/pytorch/ignite/master/assets/logo/ignite_logomark.svg"
@@ -39,18 +53,19 @@ def sidebar(self, template_list=None, config=None):
3953
template_list = template_list or []
4054
st.markdown("### Choose a Template")
4155
self.template_name = st.selectbox("Available Templates are:", options=template_list)
56+
self.template_name = FOLDER_TO_TEMPLATE_NAME[self.template_name]
4257
with st.sidebar:
4358
if self.template_name:
4459
config = config(self.template_name)
4560
self.config = config.get_configs()
4661
else:
4762
self.config = {}
4863

49-
def render_code(self, fname="", code=""):
64+
def render_code(self, fname: str = "", code: str = ""):
5065
"""Main content with the code."""
51-
with st.beta_expander(f"View rendered {fname}"):
66+
with st.beta_expander(f"View rendered {fname}", expanded=fname.endswith(".md")):
5267
if fname.endswith(".md"):
53-
st.markdown(code)
68+
st.markdown(code, unsafe_allow_html=True)
5469
else:
5570
col1, col2 = st.beta_columns([1, 20])
5671
with col1:
@@ -59,22 +74,57 @@ def render_code(self, fname="", code=""):
5974
st.code(code)
6075

6176
def render_directory(self, dir):
62-
output = check_output(["tree", dir], encoding="utf-8")
77+
"""tree command is not available in all systems."""
78+
output = f"{dir}\n"
79+
# https://stackoverflow.com/questions/9727673/list-directory-tree-structure-in-python
80+
# prefix components:
81+
space = " "
82+
branch = "│ "
83+
# pointers:
84+
tee = "├── "
85+
last = "└── "
86+
file_count = 0
87+
dir_count = 0
88+
89+
def tree(dir_path: Path, prefix: str = ""):
90+
"""A recursive generator, given a directory Path object
91+
will yield a visual tree structure line by line
92+
with each line prefixed by the same characters
93+
"""
94+
nonlocal file_count
95+
nonlocal dir_count
96+
contents = sorted(dir_path.iterdir())
97+
# contents each get pointers that are ├── with a final └── :
98+
pointers = [tee] * (len(contents) - 1) + [last]
99+
for pointer, path in zip(pointers, contents):
100+
if path.is_file():
101+
file_count += 1
102+
yield prefix + pointer + path.name
103+
if path.is_dir(): # extend the prefix and recurse:
104+
dir_count += 1
105+
extension = branch if pointer == tee else space
106+
# i.e. space because last, └── , above so no more |
107+
yield from tree(path, prefix=prefix + extension)
108+
109+
for line in tree(dir):
110+
output += line + "\n"
111+
output += f"\n{dir_count} directories, {file_count} files"
63112
st.markdown("Generated files and directory structure")
64113
st.code(output)
65114

66115
def add_sidebar(self):
67116
def config(template_name):
68117
return import_from_file("template_config", f"./templates/{template_name}/_sidebar.py")
69118

70-
self.sidebar(self.codegen.template_list, config)
119+
self.sidebar([*FOLDER_TO_TEMPLATE_NAME], config)
71120

72121
def add_content(self):
73122
"""Get generated/rendered code from the codegen."""
74123
content = [*self.codegen.render_templates(self.template_name, self.config)]
75-
if st.checkbox("View rendered code ?"):
124+
if st.checkbox("View rendered code ?", value=True):
76125
for fname, code in content:
77-
self.render_code(fname, code)
126+
if len(code): # don't show files which don't have content in them
127+
self.render_code(fname, code)
78128

79129
def add_download(self):
80130
st.markdown("")
@@ -94,12 +144,13 @@ def add_download(self):
94144
shutil.copy(archive_fname, dist_path)
95145
st.success(f"Download link : [{archive_fname}](./static/{archive_fname})")
96146
with col2:
97-
self.render_directory(os.path.join(self.codegen.dist_dir, self.template_name))
147+
self.render_directory(Path(self.codegen.dist_dir, self.template_name))
98148

99149
def run(self):
100150
self.add_sidebar()
101151
self.add_content()
102152
self.add_download()
153+
st.info(TIP)
103154

104155

105156
def main():

requirements-dev.txt

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
-r requirements.txt
2+
13
# dev
24
pytorch-ignite
35
torch

templates/_base/_argparse.pyi

+141
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
{% block imports %}
2+
from argparse import ArgumentParser
3+
{% endblock %}
4+
5+
{% block defaults %}
6+
DEFAULTS = {
7+
"use_amp": {
8+
"action": "store_true",
9+
"help": "use torch.cuda.amp for automatic mixed precision"
10+
},
11+
"resume_from": {
12+
"default": None,
13+
"type": str,
14+
"help": "path to the checkpoint file to resume, can also url starting with https (None)"
15+
},
16+
"seed": {
17+
"default": 666,
18+
"type": int,
19+
"help": "seed to use in ignite.utils.manual_seed() (666)"
20+
},
21+
"verbose": {
22+
"action": "store_true",
23+
"help": "use logging.INFO in ignite.utils.setup_logger",
24+
},
25+
26+
# distributed training options
27+
"backend": {
28+
"default": None,
29+
"type": str,
30+
"help": "backend to use for distributed training (None)",
31+
},
32+
"nproc_per_node": {
33+
"default": {{nproc_per_node}},
34+
"type": int,
35+
"help": """number of processes to launch on each node, for GPU training
36+
this is recommended to be set to the number of GPUs in your system
37+
so that each process can be bound to a single GPU ({{ nproc_per_node }})""",
38+
},
39+
"nnodes": {
40+
"default": {{nnodes}},
41+
"type": int,
42+
"help": "number of nodes to use for distributed training ({{ nnodes }})",
43+
},
44+
"node_rank": {
45+
"default": {{node_rank}},
46+
"type": int,
47+
"help": "rank of the node for multi-node distributed training ({{ node_rank }})",
48+
},
49+
"master_addr": {
50+
"default": {{master_addr}},
51+
"type": str,
52+
"help": "master node TCP/IP address for torch native backends ({{ master_addr }})",
53+
},
54+
"master_port": {
55+
"default": {{master_port}},
56+
"type": int,
57+
"help": "master node port for torch native backends ({{ master_port }})",
58+
},
59+
60+
# ignite handlers options
61+
"output_path": {
62+
"default": "{{output_path}}",
63+
"type": str,
64+
"help": "output path to indicate where to_save objects are stored ({{output_path}})",
65+
},
66+
"save_every_iters": {
67+
"default": {{save_every_iters}},
68+
"type": int,
69+
"help": "Saving iteration interval ({{save_every_iters}})",
70+
},
71+
"n_saved": {
72+
"default": {{n_saved}},
73+
"type": int,
74+
"help": "number of best models to store ({{ n_saved }})",
75+
},
76+
"log_every_iters": {
77+
"default": {{log_every_iters}},
78+
"type": int,
79+
"help": "logging interval for iteration progress bar ({{log_every_iters}})",
80+
},
81+
"with_pbars": {
82+
"default": {{with_pbars}},
83+
"type": bool,
84+
"help": "show epoch-wise and iteration-wise progress bars ({{with_pbars}})",
85+
},
86+
"with_pbar_on_iters": {
87+
"default": {{with_pbar_on_iters}},
88+
"type": bool,
89+
"help": "show iteration progress bar or not ({{with_pbar_on_iters}})",
90+
},
91+
"stop_on_nan": {
92+
"default": {{stop_on_nan}},
93+
"type": bool,
94+
"help": "stop the training if engine output contains NaN/inf values (stop_on_nan)",
95+
},
96+
"clear_cuda_cache": {
97+
"default": {{clear_cuda_cache}},
98+
"type": bool,
99+
"help": "clear cuda cache every end of epoch ({{clear_cuda_cache}})",
100+
},
101+
"with_gpu_stats": {
102+
"default": {{with_gpu_stats}},
103+
"type": bool,
104+
"help": "show gpu information, requires pynvml ({{with_gpu_stats}})",
105+
},
106+
"patience": {
107+
"default": {{patience}},
108+
"type": int,
109+
"help": "number of events to wait if no improvement and then stop the training ({{patience}})"
110+
},
111+
"limit_sec": {
112+
"default": {{limit_sec}},
113+
"type": int,
114+
"help": "maximum time before training terminates in seconds ({{limit_sec}})"
115+
},
116+
117+
# ignite logger options
118+
"filepath": {
119+
"default": "{{ filepath }}",
120+
"type": str,
121+
"help": "logging file path ({{ filepath }})",
122+
},
123+
"logger_log_every_iters": {
124+
"default": {{logger_log_every_iters}},
125+
"type": int,
126+
"help": "logging interval for experiment tracking system ({{logger_log_every_iters}})",
127+
},
128+
}
129+
{% endblock %}
130+
131+
132+
{% block get_default_parser %}
133+
def get_default_parser() -> ArgumentParser:
134+
"""Get the default configs for training."""
135+
parser = ArgumentParser(add_help=False)
136+
137+
for key, value in DEFAULTS.items():
138+
parser.add_argument(f"--{key}", **value)
139+
140+
return parser
141+
{% endblock %}

templates/_base/_events.pyi

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
Additional Events to inspect the training at custom events.
3+
"""
4+
5+
from ignite.engine.events import EventEnum
6+
7+
8+
class TrainEvents(EventEnum):
9+
"""Additional Training Events. includes
10+
11+
- BACKWARD_COMPLETED : trigger after calling loss.backward()
12+
- OPTIM_STEP_COMPLETED : trigger after calling optimizer.step()
13+
"""
14+
15+
BACKWARD_COMPLETED = "backward_completed"
16+
OPTIM_STEP_COMPLETED = "optim_step_completed"
17+
18+
19+
# define events and attribute mapping
20+
# so that we can trigger them with custom filter function
21+
train_events_to_attr = {
22+
TrainEvents.BACKWARD_COMPLETED: "backward_completed",
23+
TrainEvents.OPTIM_STEP_COMPLETED: "optim_step_completed",
24+
}
25+
26+
# Any custom events can go below
27+
# fire them in process_function of the respective engine and
28+
# register them with the respective engine in the `engines.py`

0 commit comments

Comments
 (0)