2424import getpass
2525import inspect
2626import argparse
27+ import json
28+ import shutil
2729import grp
2830import subprocess
2931from pathlib import Path
3032from typing import List , Dict , Any , Optional
3133
3234REPO_ROOT = Path (__file__ ).resolve ().parent .parent .parent
35+ SCRIPT_DIR = REPO_ROOT / ".ci-py-scripts"
3336NPROC = 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+
5461def 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
5970def 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
143253def 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