Skip to content

Commit 62ad795

Browse files
driazatiylc
authored andcommitted
Usability fixes to CI runner script (apache#9752)
* Usability fixes to CI runner script * address comments Co-authored-by: driazati <driazati@users.noreply.github.com>
1 parent e9ce1ba commit 62ad795

File tree

2 files changed

+133
-18
lines changed

2 files changed

+133
-18
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,3 +247,4 @@ conda/pkg
247247
_docs/
248248
jvm/target
249249
.config/configstore/
250+
.ci-py-scripts/

tests/scripts/ci.py

Lines changed: 132 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@
2424
import getpass
2525
import inspect
2626
import argparse
27+
import json
28+
import shutil
2729
import grp
2830
import subprocess
2931
from pathlib import Path
3032
from typing import List, Dict, Any, Optional
3133

3234
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
35+
SCRIPT_DIR = REPO_ROOT / ".ci-py-scripts"
3336
NPROC = multiprocessing.cpu_count()
3437

3538

@@ -44,48 +47,121 @@ class col:
4447
UNDERLINE = "\033[4m"
4548

4649

47-
def print_color(color: str, msg: str, **kwargs: Any) -> None:
50+
def print_color(color: str, msg: str, bold: bool, **kwargs: Any) -> None:
4851
if hasattr(sys.stdout, "isatty") and sys.stdout.isatty():
49-
print(col.BOLD + color + msg + col.RESET, **kwargs)
52+
bold_code = col.BOLD if bold else ""
53+
print(bold_code + color + msg + col.RESET, **kwargs)
5054
else:
5155
print(msg, **kwargs)
5256

5357

58+
warnings = []
59+
60+
5461
def clean_exit(msg: str) -> None:
55-
print_color(col.RED, msg, file=sys.stderr)
62+
print_color(col.RED, msg, bold=True, file=sys.stderr)
63+
64+
for warning in warnings:
65+
print_color(col.YELLOW, warning, bold=False, file=sys.stderr)
66+
5667
exit(1)
5768

5869

5970
def cmd(commands: List[Any], **kwargs: Any):
6071
commands = [str(s) for s in commands]
6172
command_str = " ".join(commands)
62-
print_color(col.BLUE, command_str)
73+
print_color(col.BLUE, command_str, bold=True)
6374
proc = subprocess.run(commands, **kwargs)
6475
if proc.returncode != 0:
6576
raise RuntimeError(f"Command failed: '{command_str}'")
77+
return proc
6678

6779

68-
def docker(name: str, image: str, scripts: List[str], env: Dict[str, str]):
69-
"""
70-
Invoke a set of bash scripts through docker/bash.sh
71-
"""
80+
def check_docker():
81+
executable = shutil.which("docker")
82+
if executable is None:
83+
clean_exit("'docker' executable not found, install it first (e.g. 'apt install docker.io')")
84+
7285
if sys.platform == "linux":
7386
# Check that the user is in the docker group before running
7487
try:
7588
group = grp.getgrnam("docker")
7689
if getpass.getuser() not in group.gr_mem:
77-
print_color(
78-
col.YELLOW, f"Note: User '{getpass.getuser()}' is not in the 'docker' group"
90+
warnings.append(
91+
f"Note: User '{getpass.getuser()}' is not in the 'docker' group, either:\n"
92+
" * run with 'sudo'\n"
93+
" * add user to 'docker': sudo usermod -aG docker $(whoami), then log out and back in",
7994
)
80-
except KeyError:
81-
print_color(col.YELLOW, f"Note: 'docker' group does not exist")
95+
except KeyError as e:
96+
warnings.append(f"Note: 'docker' group does not exist")
97+
98+
99+
def check_gpu():
100+
if not (sys.platform == "linux" and shutil.which("lshw")):
101+
# Can't check GPU on non-Linux platforms
102+
return
103+
104+
# See if we can check if a GPU is present in case of later failures,
105+
# but don't block on execution since this isn't critical
106+
try:
107+
proc = cmd(
108+
["lshw", "-json", "-C", "display"],
109+
stdout=subprocess.PIPE,
110+
stderr=subprocess.PIPE,
111+
encoding="utf-8",
112+
)
113+
stdout = proc.stdout.strip().strip(",")
114+
stdout = json.loads(stdout)
115+
except (subprocess.CalledProcessError, json.decoder.JSONDecodeError) as e:
116+
# Do nothing if any step failed
117+
return
118+
119+
if isinstance(stdout, dict):
120+
# Sometimes lshw outputs a single item as a dict instead of a list of
121+
# dicts, so wrap it up if necessary
122+
stdout = [stdout]
123+
if not isinstance(stdout, list):
124+
return
125+
126+
products = [s.get("product", "").lower() for s in stdout]
127+
if not any("nvidia" in product for product in products):
128+
warnings.append("nvidia GPU not found in 'lshw', maybe use --cpu flag?")
129+
130+
131+
def check_build():
132+
if (REPO_ROOT / "build").exists():
133+
warnings.append(
134+
"Existing build dir found may be interfering with the Docker "
135+
"build (you may need to remove it)"
136+
)
137+
138+
139+
def docker(name: str, image: str, scripts: List[str], env: Dict[str, str]):
140+
"""
141+
Invoke a set of bash scripts through docker/bash.sh
142+
143+
name: container name
144+
image: docker image name
145+
scripts: list of bash commands to run
146+
env: environment to set
147+
"""
148+
check_docker()
82149

83150
docker_bash = REPO_ROOT / "docker" / "bash.sh"
84151
command = [docker_bash, "--name", name]
85152
for key, value in env.items():
86153
command.append("--env")
87154
command.append(f"{key}={value}")
88-
command += [image, "bash", "-c", " && ".join(scripts)]
155+
156+
SCRIPT_DIR.mkdir(exist_ok=True)
157+
158+
script_file = SCRIPT_DIR / f"{name}.sh"
159+
with open(script_file, "w") as f:
160+
f.write("set -eux\n\n")
161+
f.write("\n".join(scripts))
162+
f.write("\n")
163+
164+
command += [image, "bash", str(script_file.relative_to(REPO_ROOT))]
89165

90166
try:
91167
cmd(command)
@@ -110,17 +186,50 @@ def docs(
110186
full -- Build all language docs, not just Python
111187
precheck -- Run Sphinx precheck script
112188
tutorial-pattern -- Regex for which tutorials to execute when building docs (can also be set via TVM_TUTORIAL_EXEC_PATTERN)
113-
cpu -- Use CMake defaults for building TVM (useful for building docs on a CPU machine.)
189+
cpu -- Run with the ci-cpu image and use CMake defaults for building TVM (if no GPUs are available)
114190
"""
115191
config = "./tests/scripts/task_config_build_gpu.sh"
116192
if cpu and full:
117193
clean_exit("--full cannot be used with --cpu")
118194

195+
extra_setup = []
196+
image = "ci_gpu"
119197
if cpu:
120-
# The docs import tvm.micro, so it has to be enabled in the build
121-
config = "cd build && cp ../cmake/config.cmake . && echo set\(USE_MICRO ON\) >> config.cmake && cd .."
198+
image = "ci_cpu"
199+
config = " && ".join(
200+
[
201+
"mkdir -p build",
202+
"pushd build",
203+
"cp ../cmake/config.cmake .",
204+
# The docs import tvm.micro, so it has to be enabled in the build
205+
"echo set\(USE_MICRO ON\) >> config.cmake",
206+
"popd",
207+
]
208+
)
209+
210+
# These are taken from the ci-gpu image via pip freeze, consult that
211+
# if there are any changes: https://github.com/apache/tvm/tree/main/docs#native
212+
requirements = [
213+
"Sphinx==4.2.0",
214+
"tlcpack-sphinx-addon==0.2.1",
215+
"synr==0.5.0",
216+
"image==1.5.33",
217+
"sphinx-gallery==0.4.0",
218+
"sphinx-rtd-theme==1.0.0",
219+
"matplotlib==3.3.4",
220+
"commonmark==0.9.1",
221+
"Pillow==8.3.2",
222+
"autodocsumm==0.2.7",
223+
"docutils==0.16",
224+
]
225+
226+
extra_setup = [
227+
"python3 -m pip install --user " + " ".join(requirements),
228+
]
229+
else:
230+
check_gpu()
122231

123-
scripts = [
232+
scripts = extra_setup + [
124233
config,
125234
f"./tests/scripts/task_build.sh build -j{NPROC}",
126235
"./tests/scripts/task_ci_setup.sh",
@@ -137,7 +246,8 @@ def docs(
137246
"PYTHON_DOCS_ONLY": "0" if full else "1",
138247
"IS_LOCAL": "1",
139248
}
140-
docker(name="ci-docs", image="ci_gpu", scripts=scripts, env=env)
249+
check_build()
250+
docker(name="ci-docs", image=image, scripts=scripts, env=env)
141251

142252

143253
def serve_docs(directory: str = "_docs") -> None:
@@ -221,6 +331,10 @@ def main():
221331
add_subparser(func, subparsers)
222332

223333
args = parser.parse_args()
334+
if args.command is None:
335+
parser.print_help()
336+
exit(1)
337+
224338
func = subparser_functions[args.command]
225339

226340
# Extract out the parsed args and invoke the relevant function

0 commit comments

Comments
 (0)