1- # Copyright 2024-2025 , NVIDIA CORPORATION & AFFILIATES. All rights reserved.
1+ # Copyright 2024-2026 , NVIDIA CORPORATION & AFFILIATES. All rights reserved.
22#
33# Redistribution and use in source and binary forms, with or without
44# modification, are permitted provided that the following conditions
2424# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2525# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2626
27+ import collections
2728import io
2829import json
30+ import tempfile
2931import time
3032import psutil
3133import subprocess
3234import requests
3335from contextlib import redirect_stdout
3436from triton_cli .main import run
35- from subprocess import Popen
37+ from subprocess import Popen , STDOUT
3638
3739
3840class TritonCommands :
@@ -132,6 +134,7 @@ def __init__(self, repo=None, mode="local", timeout=60, frontend=None):
132134 self .frontend = frontend
133135 self .timeout = timeout
134136 self .proc = None
137+ self ._log_path = None
135138
136139 def __enter__ (self ):
137140 self .start ()
@@ -154,10 +157,30 @@ def run_server(self, repo=None, mode="local", frontend=None):
154157 args += ["--mode" , mode ]
155158 if frontend :
156159 args += ["--frontend" , frontend ]
157- # Use Popen to run the server in the background as a separate process.
158- p = Popen (args )
160+ # Redirect stdout/stderr to a temp file so the server's output can be
161+ # surfaced in error messages if startup fails.
162+ log_handle = tempfile .NamedTemporaryFile (
163+ mode = "wb" , suffix = ".log" , prefix = "triton_server_" , delete = False
164+ )
165+ self ._log_path = log_handle .name
166+ try :
167+ p = Popen (args , stdout = log_handle , stderr = STDOUT )
168+ finally :
169+ log_handle .close ()
159170 return p
160171
172+ def _format_server_output (self , max_lines : int = 50 ) -> str :
173+ if not self ._log_path :
174+ return "<server output not captured>"
175+ try :
176+ with open (self ._log_path , errors = "replace" ) as f :
177+ tail = collections .deque (f , maxlen = max_lines )
178+ except OSError as exc :
179+ return f"<failed to read server log { self ._log_path } : { exc } >"
180+ if not tail :
181+ return "<no server output produced>"
182+ return "" .join (tail )
183+
161184 def wait_for_server_ready (self , timeout : int = 60 ):
162185 if not self .proc :
163186 raise RuntimeError ("Server process wasn't started" )
@@ -166,33 +189,53 @@ def wait_for_server_ready(self, timeout: int = 60):
166189 while True :
167190 try :
168191 if self .check_server_ready ():
169- break
192+ return
170193 except Exception as err :
171- result = self .proc .poll ()
172- if result is not None and result != 0 :
173- raise RuntimeError ("Server exited unexpectedly." ) from err
194+ exit_code = self .proc .poll ()
195+ elapsed = time .time () - start
196+
197+ if exit_code is not None :
198+ state = (
199+ "exited cleanly with code 0"
200+ if exit_code == 0
201+ else f"exited with code { exit_code } "
202+ )
203+ raise RuntimeError (
204+ f"Triton server { state } after { elapsed :.1f} s without "
205+ f"becoming ready (pid={ self .proc .pid } , "
206+ f"timeout={ timeout } s).\n "
207+ f"--- last server output ---\n "
208+ f"{ self ._format_server_output ()} \n "
209+ f"--- end server output ---"
210+ ) from err
174211
175212 time .sleep (0.5 )
176213 if time .time () - start > timeout :
177- raise RuntimeError ("Server failed to start in time." ) from err
214+ raise RuntimeError (
215+ f"Triton server did not become ready within "
216+ f"{ timeout } s (pid={ self .proc .pid } , process still "
217+ f"running).\n "
218+ f"--- last server output ---\n "
219+ f"{ self ._format_server_output ()} \n "
220+ f"--- end server output ---"
221+ ) from err
178222
179223 def kill_server (self , timeout : int = 60 ):
180- if not self .proc :
224+ if self .proc :
225+ try :
226+ self .proc .terminate ()
227+ self .proc .wait (timeout = timeout ) # Wait for triton to clean up
228+ except subprocess .TimeoutExpired :
229+ self .proc .kill ()
230+ self .proc .wait () # Indefinetely wait until the process is cleaned up.
231+ except psutil .NoSuchProcess as e :
232+ print (e )
233+ except AttributeError as e :
234+ print (e )
235+ else :
181236 # If process wasn't started by this point, just print the error and
182237 # gracefully exit for now.
183238 print ("ERROR: Server process wasn't started" )
184- return
185-
186- try :
187- self .proc .terminate ()
188- self .proc .wait (timeout = timeout ) # Wait for triton to clean up
189- except subprocess .TimeoutExpired :
190- self .proc .kill ()
191- self .proc .wait () # Indefinetely wait until the process is cleaned up.
192- except psutil .NoSuchProcess as e :
193- print (e )
194- except AttributeError as e :
195- print (e )
196239
197240 def check_server_ready (self ):
198241 if self .frontend == "openai" :
0 commit comments