From 1f3d833af4812616ce8bc4efa6026a9c04014f21 Mon Sep 17 00:00:00 2001 From: Mansi Khamkar Date: Wed, 26 Mar 2025 23:05:22 +0530 Subject: [PATCH 1/6] multiple requests according to model type --- clarifai/runners/models/model_run_locally.py | 108 ++++++++++++++++--- static/example_image.jpg | Bin 0 -> 70911 bytes 2 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 static/example_image.jpg diff --git a/clarifai/runners/models/model_run_locally.py b/clarifai/runners/models/model_run_locally.py index 0e63c284..89489d6e 100644 --- a/clarifai/runners/models/model_run_locally.py +++ b/clarifai/runners/models/model_run_locally.py @@ -93,29 +93,99 @@ def _build_request(self): model_version_proto = self.builder.get_model_version_proto() model_version_proto.id = "model_version" + image_url = "https://samples.clarifai.com/metro-north.jpg" + image_path = "../../static/metro-north.jpg" - return service_pb2.PostModelOutputsRequest( + default_request = service_pb2.PostModelOutputsRequest( model=resources_pb2.Model(model_version=model_version_proto), inputs=[ resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="How many people live in new york?"), + text=resources_pb2.Text(raw="Describe the image"), image=resources_pb2.Image(url="https://samples.clarifai.com/metro-north.jpg"), - audio=resources_pb2.Audio(url="https://samples.clarifai.com/GoodMorning.wav"), - video=resources_pb2.Video(url="https://samples.clarifai.com/beer.mp4"), )) ], ) + def image_to_base64(image_path): + with open(image_path, "rb") as image_file: + base64_img = base64.b64encode(image_file.read()) + return base64_img + + def _build_text_to_text_request(): + requests = [ + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input(data=resources_pb2.Data( + text=resources_pb2.Text(raw="How many people live in new york?"), + )) + ], + ), + ] + return requests + + def _build_multimodal_to_text_request(): + requests = [ + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input(data=resources_pb2.Data( + text=resources_pb2.Text(raw="Describe the image"), + image=resources_pb2.Image(url=image_url), + )) + ], + ), + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input(data=resources_pb2.Data( + text=resources_pb2.Text(raw="Describe the image"), + image=resources_pb2.Image(base64=image_to_base64(image_path)), + )) + ], + ), + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input(data=resources_pb2.Data( + text=resources_pb2.Text(raw="How many people live in new york?"), + )) + ], + ), + ] + return requests + + def _build_text_to_image_request(): + requests = [ + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input(data=resources_pb2.Data( + text=resources_pb2.Text(raw="Generate an image of a dog playing in the park"), + )) + ], + ), + ] + return requests + + requests = [default_request] + if self.config.get("multimodal_to_text"): + requests.extend(_build_multimodal_to_text_request()) + if self.config.get("text_to_text"): + requests.extend(_build_text_to_text_request()) + if self.config.get("text_to_image"): + requests.extend(_build_text_to_image_request()) + + return requests + + def _build_stream_request(self): - request = self._build_request() - for i in range(1): + requests = self._build_request() + for i in range(len(requests)): yield request - def _run_model_inference(self, model): - """Perform inference using the model.""" - request = self._build_request() - stream_request = self._build_stream_request() - + def _run_model_on_single_request(self, model, request): + """Perform inference using the model on a single request.""" ensure_urls_downloaded(request) predict_response = None generate_response = None @@ -136,7 +206,7 @@ def _run_model_inference(self, model): if predict_response: if predict_response.outputs[0].status.code != status_code_pb2.SUCCESS: - logger.error(f"Moddel Prediction failed: {predict_response}") + logger.error(f"Model Prediction failed: {predict_response}") else: logger.info(f"Model Prediction succeeded: {predict_response}") @@ -157,7 +227,7 @@ def _run_model_inference(self, model): if generate_response: generate_first_res = next(generate_response) if generate_first_res.outputs[0].status.code != status_code_pb2.SUCCESS: - logger.error(f"Moddel Prediction failed: {generate_first_res}") + logger.error(f"Model Prediction failed: {generate_first_res}") else: logger.info( f"Model Prediction succeeded for generate and first response: {generate_first_res}") @@ -179,11 +249,21 @@ def _run_model_inference(self, model): if stream_response: stream_first_res = next(stream_response) if stream_first_res.outputs[0].status.code != status_code_pb2.SUCCESS: - logger.error(f"Moddel Prediction failed: {stream_first_res}") + logger.error(f"Model Prediction failed: {stream_first_res}") else: logger.info( f"Model Prediction succeeded for stream and first response: {stream_first_res}") + + def _run_model_inference(self, model): + """Perform inference using the model.""" + requests = self._build_request() + stream_requests = self._build_stream_request() + + for req_index in range(len(requests)): + self._run_model_on_single_request(model, requests[req_index]) + + def _run_test(self): """Test the model locally by making a prediction.""" # Create the model diff --git a/static/example_image.jpg b/static/example_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2eab56451d0f1e06c3e2c960e27b519185a76485 GIT binary patch literal 70911 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 Oh4)Ow^HP^)fB)Hr9l;6! literal 0 HcmV?d00001 From fcca21cda8eb57b4ce97c1d8011a310bc4b9b759 Mon Sep 17 00:00:00 2001 From: Mansi Khamkar Date: Thu, 27 Mar 2025 16:17:47 +0530 Subject: [PATCH 2/6] multiple requests according to model type --- clarifai/runners/models/model_run_locally.py | 24 ++++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/clarifai/runners/models/model_run_locally.py b/clarifai/runners/models/model_run_locally.py index 89489d6e..dbd5740e 100644 --- a/clarifai/runners/models/model_run_locally.py +++ b/clarifai/runners/models/model_run_locally.py @@ -100,8 +100,10 @@ def _build_request(self): model=resources_pb2.Model(model_version=model_version_proto), inputs=[ resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="Describe the image"), + text=resources_pb2.Text(raw="How many people live in new york?"), image=resources_pb2.Image(url="https://samples.clarifai.com/metro-north.jpg"), + audio=resources_pb2.Audio(url="https://samples.clarifai.com/GoodMorning.wav"), + video=resources_pb2.Video(url="https://samples.clarifai.com/beer.mp4"), )) ], ) @@ -111,7 +113,7 @@ def image_to_base64(image_path): base64_img = base64.b64encode(image_file.read()) return base64_img - def _build_text_to_text_request(): + def _build_text_to_text_requests(): requests = [ service_pb2.PostModelOutputsRequest( model=resources_pb2.Model(model_version=model_version_proto), @@ -124,7 +126,7 @@ def _build_text_to_text_request(): ] return requests - def _build_multimodal_to_text_request(): + def _build_multimodal_to_text_requests(): requests = [ service_pb2.PostModelOutputsRequest( model=resources_pb2.Model(model_version=model_version_proto), @@ -155,7 +157,7 @@ def _build_multimodal_to_text_request(): ] return requests - def _build_text_to_image_request(): + def _build_text_to_image_requests(): requests = [ service_pb2.PostModelOutputsRequest( model=resources_pb2.Model(model_version=model_version_proto), @@ -168,13 +170,15 @@ def _build_text_to_image_request(): ] return requests - requests = [default_request] + requests = [] if self.config.get("multimodal_to_text"): - requests.extend(_build_multimodal_to_text_request()) - if self.config.get("text_to_text"): - requests.extend(_build_text_to_text_request()) - if self.config.get("text_to_image"): - requests.extend(_build_text_to_image_request()) + requests.extend(_build_multimodal_to_text_requests()) + elif self.config.get("text_to_text"): + requests.extend(_build_text_to_text_requests()) + elif self.config.get("text_to_image"): + requests.extend(_build_text_to_image_requests()) + else: + requests.append(default_request) return requests From e6a9259d03d08f61ba5c240e452e4502e07ad4a6 Mon Sep 17 00:00:00 2001 From: Mansi Khamkar Date: Thu, 27 Mar 2025 16:32:42 +0530 Subject: [PATCH 3/6] multiple requests according to model type --- clarifai/runners/models/model_run_locally.py | 1265 ++++++++++-------- 1 file changed, 691 insertions(+), 574 deletions(-) diff --git a/clarifai/runners/models/model_run_locally.py b/clarifai/runners/models/model_run_locally.py index dbd5740e..30a9a4ac 100644 --- a/clarifai/runners/models/model_run_locally.py +++ b/clarifai/runners/models/model_run_locally.py @@ -9,6 +9,8 @@ import time import traceback import venv +import base64 + from clarifai_grpc.grpc.api import resources_pb2, service_pb2 from clarifai_grpc.grpc.api.status import status_code_pb2, status_pb2 @@ -19,587 +21,702 @@ class ModelRunLocally: + def __init__(self, model_path): + self.model_path = model_path + self.requirements_file = os.path.join(self.model_path, "requirements.txt") + + # ModelBuilder contains multiple useful methods to interact with the model + self.builder = ModelBuilder(self.model_path, download_validation_only=True) + self.config = self.builder.config + + def _requirements_hash(self): + """Generate a hash of the requirements file.""" + with open(self.requirements_file, "r") as f: + return hashlib.md5(f.read().encode("utf-8")).hexdigest() + + def _get_env_executable(self): + """Get the python executable from the virtual environment.""" + # Depending on the platform, venv scripts are placed in either "Scripts" (Windows) or "bin" (Linux/Mac) + if platform.system().lower().startswith("win"): + scripts_folder = "Scripts" + python_exe = "python.exe" + pip_exe = "pip.exe" + else: + scripts_folder = "bin" + python_exe = "python" + pip_exe = "pip" - def __init__(self, model_path): - self.model_path = model_path - self.requirements_file = os.path.join(self.model_path, "requirements.txt") - - # ModelBuilder contains multiple useful methods to interact with the model - self.builder = ModelBuilder(self.model_path, download_validation_only=True) - self.config = self.builder.config - - def _requirements_hash(self): - """Generate a hash of the requirements file.""" - with open(self.requirements_file, "r") as f: - return hashlib.md5(f.read().encode('utf-8')).hexdigest() - - def _get_env_executable(self): - """Get the python executable from the virtual environment.""" - # Depending on the platform, venv scripts are placed in either "Scripts" (Windows) or "bin" (Linux/Mac) - if platform.system().lower().startswith("win"): - scripts_folder = "Scripts" - python_exe = "python.exe" - pip_exe = "pip.exe" - else: - scripts_folder = "bin" - python_exe = "python" - pip_exe = "pip" + self.python_executable = os.path.join(self.venv_dir, scripts_folder, python_exe) + self.pip_executable = os.path.join(self.venv_dir, scripts_folder, pip_exe) + + return self.python_executable, self.pip_executable - self.python_executable = os.path.join(self.venv_dir, scripts_folder, python_exe) - self.pip_executable = os.path.join(self.venv_dir, scripts_folder, pip_exe) + def create_temp_venv(self): + """Create a temporary virtual environment.""" + requirements_hash = self._requirements_hash() - return self.python_executable, self.pip_executable + temp_dir = os.path.join(tempfile.gettempdir(), str(requirements_hash)) + venv_dir = os.path.join(temp_dir, "venv") - def create_temp_venv(self): - """Create a temporary virtual environment.""" - requirements_hash = self._requirements_hash() + if os.path.exists(temp_dir): + logger.info(f"Using previous virtual environment at {temp_dir}") + use_existing_venv = True + else: + logger.info("Creating temporary virtual environment...") + use_existing_venv = False + venv.create(venv_dir, with_pip=True) + logger.info(f"Created temporary virtual environment at {venv_dir}") - temp_dir = os.path.join(tempfile.gettempdir(), str(requirements_hash)) - venv_dir = os.path.join(temp_dir, "venv") + self.venv_dir = venv_dir + self.temp_dir = temp_dir + self.python_executable, self.pip_executable = self._get_env_executable() - if os.path.exists(temp_dir): - logger.info(f"Using previous virtual environment at {temp_dir}") - use_existing_venv = True - else: - logger.info("Creating temporary virtual environment...") - use_existing_venv = False - venv.create(venv_dir, with_pip=True) - logger.info(f"Created temporary virtual environment at {venv_dir}") - - self.venv_dir = venv_dir - self.temp_dir = temp_dir - self.python_executable, self.pip_executable = self._get_env_executable() - - return use_existing_venv - - def install_requirements(self): - """Install the dependencies from requirements.txt and Clarifai.""" - _, pip_executable = self._get_env_executable() - try: - logger.info( - f"Installing requirements from {self.requirements_file}... in the virtual environment {self.venv_dir}" - ) - subprocess.check_call([pip_executable, "install", "-r", self.requirements_file]) - logger.info("Installing Clarifai package...") - subprocess.check_call([pip_executable, "install", "clarifai"]) - logger.info("Requirements installed successfully!") - except subprocess.CalledProcessError as e: - logger.error(f"Error installing requirements: {e}") - self.clean_up() - sys.exit(1) - - def _build_request(self): - """Create a mock inference request for testing the model.""" - - model_version_proto = self.builder.get_model_version_proto() - model_version_proto.id = "model_version" - image_url = "https://samples.clarifai.com/metro-north.jpg" - image_path = "../../static/metro-north.jpg" - - default_request = service_pb2.PostModelOutputsRequest( - model=resources_pb2.Model(model_version=model_version_proto), - inputs=[ - resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="How many people live in new york?"), - image=resources_pb2.Image(url="https://samples.clarifai.com/metro-north.jpg"), - audio=resources_pb2.Audio(url="https://samples.clarifai.com/GoodMorning.wav"), - video=resources_pb2.Video(url="https://samples.clarifai.com/beer.mp4"), - )) - ], + return use_existing_venv + + def install_requirements(self): + """Install the dependencies from requirements.txt and Clarifai.""" + _, pip_executable = self._get_env_executable() + try: + logger.info( + f"Installing requirements from {self.requirements_file}... in the virtual environment {self.venv_dir}" + ) + subprocess.check_call( + [pip_executable, "install", "-r", self.requirements_file] + ) + logger.info("Installing Clarifai package...") + subprocess.check_call([pip_executable, "install", "clarifai"]) + logger.info("Requirements installed successfully!") + except subprocess.CalledProcessError as e: + logger.error(f"Error installing requirements: {e}") + self.clean_up() + sys.exit(1) + + def _build_request(self): + """Create a mock inference request for testing the model.""" + + model_version_proto = self.builder.get_model_version_proto() + model_version_proto.id = "model_version" + image_url = "https://samples.clarifai.com/metro-north.jpg" + image_path = "../../static/metro-north.jpg" + + default_request = service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input( + data=resources_pb2.Data( + text=resources_pb2.Text( + raw="How many people live in new york?" + ), + image=resources_pb2.Image( + url="https://samples.clarifai.com/metro-north.jpg" + ), + audio=resources_pb2.Audio( + url="https://samples.clarifai.com/GoodMorning.wav" + ), + video=resources_pb2.Video( + url="https://samples.clarifai.com/beer.mp4" + ), + ) + ) + ], + ) + + def image_to_base64(image_path): + with open(image_path, "rb") as image_file: + base64_img = base64.b64encode(image_file.read()) + return base64_img + + def _build_text_to_text_requests(): + requests = [ + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input( + data=resources_pb2.Data( + text=resources_pb2.Text( + raw="How many people live in new york?" + ), + ) + ) + ], + ), + ] + return requests + + def _build_multimodal_to_text_requests(): + requests = [ + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input( + data=resources_pb2.Data( + text=resources_pb2.Text(raw="Describe the image"), + image=resources_pb2.Image(url=image_url), + ) + ) + ], + ), + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input( + data=resources_pb2.Data( + text=resources_pb2.Text(raw="Describe the image"), + image=resources_pb2.Image( + base64=image_to_base64(image_path) + ), + ) + ) + ], + ), + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input( + data=resources_pb2.Data( + text=resources_pb2.Text( + raw="How many people live in new york?" + ), + ) + ) + ], + ), + ] + return requests + + def _build_text_to_image_requests(): + requests = [ + service_pb2.PostModelOutputsRequest( + model=resources_pb2.Model(model_version=model_version_proto), + inputs=[ + resources_pb2.Input( + data=resources_pb2.Data( + text=resources_pb2.Text( + raw="Generate an image of a dog playing in the park" + ), + ) + ) + ], + ), + ] + return requests + + requests = [] + if self.config.get("multimodal_to_text"): + requests.extend(_build_multimodal_to_text_requests()) + elif self.config.get("text_to_text"): + requests.extend(_build_text_to_text_requests()) + elif self.config.get("text_to_image"): + requests.extend(_build_text_to_image_requests()) + else: + requests.append(default_request) + + return requests + + def _build_stream_request(self): + requests = self._build_request() + for i in range(len(requests)): + yield requests[i] + + def _run_model_on_single_request(self, model, request, stream_request): + """Perform inference using the model on a single request.""" + ensure_urls_downloaded(request) + predict_response = None + generate_response = None + stream_response = None + try: + predict_response = model.predict(request) + except NotImplementedError: + logger.info("Model does not implement predict() method.") + except Exception as e: + logger.error(f"Model Prediction failed: {e}") + traceback.print_exc() + predict_response = service_pb2.MultiOutputResponse( + status=status_pb2.Status( + code=status_code_pb2.MODEL_PREDICTION_FAILED, + description="Prediction failed", + details="", + internal_details=str(e), + ) + ) + + if predict_response: + if predict_response.outputs[0].status.code != status_code_pb2.SUCCESS: + logger.error(f"Model Prediction failed: {predict_response}") + else: + logger.info(f"Model Prediction succeeded: {predict_response}") + + try: + generate_response = model.generate(request) + except NotImplementedError: + logger.info("Model does not implement generate() method.") + except Exception as e: + logger.error(f"Model Generation failed: {e}") + traceback.print_exc() + generate_response = service_pb2.MultiOutputResponse( + status=status_pb2.Status( + code=status_code_pb2.MODEL_GENERATION_FAILED, + description="Generation failed", + details="", + internal_details=str(e), + ) + ) + + if generate_response: + generate_first_res = next(generate_response) + if generate_first_res.outputs[0].status.code != status_code_pb2.SUCCESS: + logger.error(f"Model Prediction failed: {generate_first_res}") + else: + logger.info( + f"Model Prediction succeeded for generate and first response: {generate_first_res}" + ) + + try: + stream_response = model.stream(stream_request) + except NotImplementedError: + logger.info("Model does not implement stream() method.") + except Exception as e: + logger.error(f"Model Stream failed: {e}") + traceback.print_exc() + stream_response = service_pb2.MultiOutputResponse( + status=status_pb2.Status( + code=status_code_pb2.MODEL_STREAM_FAILED, + description="Stream failed", + details="", + internal_details=str(e), + ) + ) + + if stream_response: + stream_first_res = next(stream_response) + if stream_first_res.outputs[0].status.code != status_code_pb2.SUCCESS: + logger.error(f"Model Prediction failed: {stream_first_res}") + else: + logger.info( + f"Model Prediction succeeded for stream and first response: {stream_first_res}" + ) + + def _run_model_inference(self, model): + """Perform inference using the model.""" + requests = self._build_request() + stream_requests = self._build_stream_request() + + for req_index in range(len(requests)): + self._run_model_on_single_request( + model, requests[req_index], stream_requests[req_index] + ) + + def _run_test(self): + """Test the model locally by making a prediction.""" + # Create the model + model = self.builder.create_model_instance() + # send an inference. + self._run_model_inference(model) + + def test_model(self): + """Test the model by running it locally in the virtual environment.""" + + import_path = repr(os.path.dirname(os.path.abspath(__file__))) + model_path = repr(self.model_path) + + command_string = ( + f"import sys; " + f"sys.path.append({import_path}); " + f"from model_run_locally import ModelRunLocally; " + f"ModelRunLocally({model_path})._run_test()" + ) + + command = [self.python_executable, "-c", command_string] + process = None + try: + logger.info("Testing the model locally...") + process = subprocess.Popen(command) + # Wait for the process to complete + process.wait() + if process.returncode == 0: + logger.info("Model tested successfully!") + if process.returncode != 0: + raise subprocess.CalledProcessError(process.returncode, command) + except subprocess.CalledProcessError as e: + logger.error(f"Error testing the model: {e}") + sys.exit(1) + except Exception as e: + logger.error(f"Unexpected error: {e}") + sys.exit(1) + finally: + # After the function runs, check if the process is still running + if process and process.poll() is None: + logger.info("Process is still running. Terminating process.") + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + logger.info( + "Process did not terminate gracefully. Killing process." + ) + # Kill the process if it doesn't terminate after 5 seconds + process.kill() + + # run the model server + def run_model_server(self, port=8080): + """Run the Clarifai Runners's model server.""" + + command = [ + self.python_executable, + "-m", + "clarifai.runners.server", + "--model_path", + self.model_path, + "--grpc", + "--port", + str(port), + ] + try: + logger.info( + f"Starting model server at localhost:{port} with the model at {self.model_path}..." + ) + subprocess.check_call(command) + logger.info( + "Model server started successfully and running at localhost:{port}" + ) + except subprocess.CalledProcessError as e: + logger.error(f"Error running model server: {e}") + self.clean_up() + sys.exit(1) + + def _docker_hash(self): + """Generate a hash of the combined requirements file and Dockefile""" + with open(self.requirements_file, "r") as f: + requirements_hash = hashlib.md5(f.read().encode("utf-8")).hexdigest() + with open(os.path.join(self.model_path, "Dockerfile"), "r") as f: + dockerfile_hash = hashlib.md5(f.read().encode("utf-8")).hexdigest() + + return hashlib.md5( + f"{requirements_hash}{dockerfile_hash}".encode("utf-8") + ).hexdigest() + + def is_docker_installed(self): + """Checks if Docker is installed on the system.""" + try: + logger.info("Checking if Docker is installed...") + subprocess.run(["docker", "--version"], check=True) + return True + except subprocess.CalledProcessError: + logger.error( + "Docker is not installed! Please install Docker to run the model in a container." + ) + return False + + def build_docker_image( + self, + image_name="model_image", + ): + """Build the docker image using the Dockerfile in the model directory.""" + try: + logger.info( + f"Building docker image from Dockerfile in {self.model_path}..." + ) + + # since we don't want to copy the model directory into the container, we need to modify the Dockerfile and comment out the COPY instruction + dockerfile_path = os.path.join(self.model_path, "Dockerfile") + # Read the Dockerfile + with open(dockerfile_path, "r") as file: + lines = file.readlines() + + # Comment out the COPY instruction that copies the current folder + modified_lines = [] + for line in lines: + if "COPY" in line and "/home/nonroot/main" in line: + modified_lines.append(f"# {line}") + elif "download-checkpoints" in line and "/home/nonroot/main" in line: + modified_lines.append(f"# {line}") + else: + modified_lines.append(line) + + # Create a temporary directory to store the modified Dockerfile + with tempfile.TemporaryDirectory() as temp_dir: + temp_dockerfile_path = os.path.join(temp_dir, "Dockerfile.temp") + + # Write the modified Dockerfile to the temporary file + with open(temp_dockerfile_path, "w") as file: + file.writelines(modified_lines) + + # Build the Docker image using the temporary Dockerfile + subprocess.check_call( + [ + "docker", + "build", + "-t", + image_name, + "-f", + temp_dockerfile_path, + self.model_path, + ] + ) + logger.info(f"Docker image '{image_name}' built successfully!") + except subprocess.CalledProcessError as e: + logger.info(f"Error occurred while building the Docker image: {e}") + sys.exit(1) + + def docker_image_exists(self, image_name): + """Check if the Docker image exists.""" + try: + logger.info(f"Checking if Docker image '{image_name}' exists...") + subprocess.run(["docker", "inspect", image_name], check=True) + logger.info(f"Docker image '{image_name}' exists!") + return True + except subprocess.CalledProcessError: + logger.info(f"Docker image '{image_name}' does not exist!") + return False + + def _gpu_is_available(self): + """ + Checks if nvidia-smi is available, indicating a GPU is likely accessible. + """ + return shutil.which("nvidia-smi") is not None + + def run_docker_container( + self, + image_name, + container_name="clarifai-model-container", + port=8080, + env_vars=None, + ): + """Runs a Docker container from the specified image.""" + try: + logger.info( + f"Running Docker container '{container_name}' from image '{image_name}'..." + ) + # Base docker run command + cmd = [ + "docker", + "run", + "--name", + container_name, + "--rm", + "--network", + "host", + ] + if self._gpu_is_available(): + cmd.extend(["--gpus", "all"]) + # Add volume mappings + cmd.extend(["-v", f"{self.model_path}:/home/nonroot/main"]) + # Add environment variables + if env_vars: + for key, value in env_vars.items(): + cmd.extend(["-e", f"{key}={value}"]) + # Add the image name + cmd.append(image_name) + # update the CMD to run the server + cmd.extend( + ["--model_path", "/home/nonroot/main", "--grpc", "--port", str(port)] + ) + # Run the container + process = subprocess.Popen( + cmd, + ) + logger.info( + f"Docker container '{container_name}' is running successfully! access the model at http://localhost:{port}" + ) + + # Function to handle Ctrl+C (SIGINT) gracefully + def signal_handler(sig, frame): + logger.info(f"Stopping Docker container '{container_name}'...") + subprocess.run(["docker", "stop", container_name], check=True) + process.terminate() + logger.info( + f"Docker container '{container_name}' stopped successfully!" + ) + time.sleep(1) + sys.exit(0) + + # Register the signal handler for SIGINT (Ctrl+C) + signal.signal(signal.SIGINT, signal_handler) + # Wait for the process to finish (keeps the container running until it's stopped) + process.wait() + except subprocess.CalledProcessError as e: + logger.info(f"Error occurred while running the Docker container: {e}") + sys.exit(1) + except Exception as e: + logger.info(f"Error occurred while running the Docker container: {e}") + sys.exit(1) + + def test_model_container( + self, image_name, container_name="clarifai-model-container", env_vars=None + ): + """Test the model inside the Docker container.""" + try: + logger.info("Testing the model inside the Docker container...") + # Base docker run command + cmd = [ + "docker", + "run", + "--name", + container_name, + "--rm", + "--network", + "host", + ] + if self._gpu_is_available(): + cmd.extend(["--gpus", "all"]) + # update the entrypoint for testing the model + cmd.extend(["--entrypoint", "python"]) + # Add volume mappings + cmd.extend(["-v", f"{self.model_path}:/home/nonroot/main"]) + # Add environment variables + if env_vars: + for key, value in env_vars.items(): + cmd.extend(["-e", f"{key}={value}"]) + # Add the image name + cmd.append(image_name) + # update the CMD to test the model inside the container + cmd.extend( + [ + "-c", + "from clarifai.runners.models.model_run_locally import ModelRunLocally; ModelRunLocally('/home/nonroot/main')._run_test()", + ] + ) + # Run the container + subprocess.check_call(cmd) + logger.info("Model tested successfully!") + except subprocess.CalledProcessError as e: + logger.error(f"Error testing the model inside the Docker container: {e}") + sys.exit(1) + + def container_exists(self, container_name="clarifai-model-container"): + """Check if the Docker container exists.""" + try: + # Run docker ps -a to list all containers (running and stopped) + result = subprocess.run( + [ + "docker", + "ps", + "-a", + "--filter", + f"name={container_name}", + "--format", + "{{.Names}}", + ], + check=True, + capture_output=True, + text=True, + ) + # If the container name is returned, it exists + if result.stdout.strip() == container_name: + logger.info(f"Docker container '{container_name}' exists.") + return True + else: + return False + except subprocess.CalledProcessError as e: + logger.error(f"Error occurred while checking if container exists: {e}") + return False + + def stop_docker_container(self, container_name="clarifai-model-container"): + """Stop the Docker container if it's running.""" + try: + # Check if the container is running + result = subprocess.run( + [ + "docker", + "ps", + "--filter", + f"name={container_name}", + "--format", + "{{.Names}}", + ], + check=True, + capture_output=True, + text=True, + ) + if result.stdout.strip() == container_name: + logger.info( + f"Docker container '{container_name}' is running. Stopping it..." + ) + subprocess.run(["docker", "stop", container_name], check=True) + logger.info( + f"Docker container '{container_name}' stopped successfully!" + ) + except subprocess.CalledProcessError as e: + logger.error(f"Error occurred while stopping the Docker container: {e}") + + def remove_docker_container(self, container_name="clarifai-model-container"): + """Remove the Docker container.""" + try: + logger.info(f"Removing Docker container '{container_name}'...") + subprocess.run(["docker", "rm", container_name], check=True) + logger.info(f"Docker container '{container_name}' removed successfully!") + except subprocess.CalledProcessError as e: + logger.error(f"Error occurred while removing the Docker container: {e}") + + def remove_docker_image(self, image_name): + """Remove the Docker image.""" + try: + logger.info(f"Removing Docker image '{image_name}'...") + subprocess.run(["docker", "rmi", image_name], check=True) + logger.info(f"Docker image '{image_name}' removed successfully!") + except subprocess.CalledProcessError as e: + logger.error(f"Error occurred while removing the Docker image: {e}") + + def clean_up(self): + """Clean up the temporary virtual environment.""" + if os.path.exists(self.temp_dir): + logger.info("Cleaning up temporary virtual environment...") + shutil.rmtree(self.temp_dir) + + +def main( + model_path, + run_model_server=False, + inside_container=False, + port=8080, + keep_env=False, + keep_image=False, + skip_dockerfile: bool = False, +): + manager = ModelRunLocally(model_path) + # get whatever stage is in config.yaml to force download now + # also always write to where upload/build wants to, not the /tmp folder that runtime stage uses + _, _, _, when = manager.builder._validate_config_checkpoints() + manager.builder.download_checkpoints( + stage=when, checkpoint_path_override=manager.builder.checkpoint_path ) + if inside_container: + if not manager.is_docker_installed(): + sys.exit(1) + if not skip_dockerfile: + manager.builder.create_dockerfile() + image_tag = manager._docker_hash() + model_id = manager.config["model"]["id"].lower() + # must be in lowercase + image_name = f"{model_id}:{image_tag}" + container_name = model_id + if not manager.docker_image_exists(image_name): + manager.build_docker_image(image_name=image_name) + try: + if run_model_server: + manager.run_docker_container( + image_name=image_name, container_name=container_name, port=port + ) + else: + manager.test_model_container( + image_name=image_name, container_name=container_name + ) + finally: + if manager.container_exists(container_name): + manager.stop_docker_container(container_name) + manager.remove_docker_container(container_name=container_name) + if not keep_image: + manager.remove_docker_image(image_name) - def image_to_base64(image_path): - with open(image_path, "rb") as image_file: - base64_img = base64.b64encode(image_file.read()) - return base64_img - - def _build_text_to_text_requests(): - requests = [ - service_pb2.PostModelOutputsRequest( - model=resources_pb2.Model(model_version=model_version_proto), - inputs=[ - resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="How many people live in new york?"), - )) - ], - ), - ] - return requests - - def _build_multimodal_to_text_requests(): - requests = [ - service_pb2.PostModelOutputsRequest( - model=resources_pb2.Model(model_version=model_version_proto), - inputs=[ - resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="Describe the image"), - image=resources_pb2.Image(url=image_url), - )) - ], - ), - service_pb2.PostModelOutputsRequest( - model=resources_pb2.Model(model_version=model_version_proto), - inputs=[ - resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="Describe the image"), - image=resources_pb2.Image(base64=image_to_base64(image_path)), - )) - ], - ), - service_pb2.PostModelOutputsRequest( - model=resources_pb2.Model(model_version=model_version_proto), - inputs=[ - resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="How many people live in new york?"), - )) - ], - ), - ] - return requests - - def _build_text_to_image_requests(): - requests = [ - service_pb2.PostModelOutputsRequest( - model=resources_pb2.Model(model_version=model_version_proto), - inputs=[ - resources_pb2.Input(data=resources_pb2.Data( - text=resources_pb2.Text(raw="Generate an image of a dog playing in the park"), - )) - ], - ), - ] - return requests - - requests = [] - if self.config.get("multimodal_to_text"): - requests.extend(_build_multimodal_to_text_requests()) - elif self.config.get("text_to_text"): - requests.extend(_build_text_to_text_requests()) - elif self.config.get("text_to_image"): - requests.extend(_build_text_to_image_requests()) else: - requests.append(default_request) - - return requests - - - def _build_stream_request(self): - requests = self._build_request() - for i in range(len(requests)): - yield request - - def _run_model_on_single_request(self, model, request): - """Perform inference using the model on a single request.""" - ensure_urls_downloaded(request) - predict_response = None - generate_response = None - stream_response = None - try: - predict_response = model.predict(request) - except NotImplementedError: - logger.info("Model does not implement predict() method.") - except Exception as e: - logger.error(f"Model Prediction failed: {e}") - traceback.print_exc() - predict_response = service_pb2.MultiOutputResponse(status=status_pb2.Status( - code=status_code_pb2.MODEL_PREDICTION_FAILED, - description="Prediction failed", - details="", - internal_details=str(e), - )) - - if predict_response: - if predict_response.outputs[0].status.code != status_code_pb2.SUCCESS: - logger.error(f"Model Prediction failed: {predict_response}") - else: - logger.info(f"Model Prediction succeeded: {predict_response}") - - try: - generate_response = model.generate(request) - except NotImplementedError: - logger.info("Model does not implement generate() method.") - except Exception as e: - logger.error(f"Model Generation failed: {e}") - traceback.print_exc() - generate_response = service_pb2.MultiOutputResponse(status=status_pb2.Status( - code=status_code_pb2.MODEL_GENERATION_FAILED, - description="Generation failed", - details="", - internal_details=str(e), - )) - - if generate_response: - generate_first_res = next(generate_response) - if generate_first_res.outputs[0].status.code != status_code_pb2.SUCCESS: - logger.error(f"Model Prediction failed: {generate_first_res}") - else: - logger.info( - f"Model Prediction succeeded for generate and first response: {generate_first_res}") - - try: - stream_response = model.stream(stream_request) - except NotImplementedError: - logger.info("Model does not implement stream() method.") - except Exception as e: - logger.error(f"Model Stream failed: {e}") - traceback.print_exc() - stream_response = service_pb2.MultiOutputResponse(status=status_pb2.Status( - code=status_code_pb2.MODEL_STREAM_FAILED, - description="Stream failed", - details="", - internal_details=str(e), - )) - - if stream_response: - stream_first_res = next(stream_response) - if stream_first_res.outputs[0].status.code != status_code_pb2.SUCCESS: - logger.error(f"Model Prediction failed: {stream_first_res}") - else: - logger.info( - f"Model Prediction succeeded for stream and first response: {stream_first_res}") - - - def _run_model_inference(self, model): - """Perform inference using the model.""" - requests = self._build_request() - stream_requests = self._build_stream_request() - - for req_index in range(len(requests)): - self._run_model_on_single_request(model, requests[req_index]) - - - def _run_test(self): - """Test the model locally by making a prediction.""" - # Create the model - model = self.builder.create_model_instance() - # send an inference. - self._run_model_inference(model) - - def test_model(self): - """Test the model by running it locally in the virtual environment.""" - - import_path = repr(os.path.dirname(os.path.abspath(__file__))) - model_path = repr(self.model_path) - - command_string = (f"import sys; " - f"sys.path.append({import_path}); " - f"from model_run_locally import ModelRunLocally; " - f"ModelRunLocally({model_path})._run_test()") - - command = [self.python_executable, "-c", command_string] - process = None - try: - logger.info("Testing the model locally...") - process = subprocess.Popen(command) - # Wait for the process to complete - process.wait() - if process.returncode == 0: - logger.info("Model tested successfully!") - if process.returncode != 0: - raise subprocess.CalledProcessError(process.returncode, command) - except subprocess.CalledProcessError as e: - logger.error(f"Error testing the model: {e}") - sys.exit(1) - except Exception as e: - logger.error(f"Unexpected error: {e}") - sys.exit(1) - finally: - # After the function runs, check if the process is still running - if process and process.poll() is None: - logger.info("Process is still running. Terminating process.") - process.terminate() try: - process.wait(timeout=5) - except subprocess.TimeoutExpired: - logger.info("Process did not terminate gracefully. Killing process.") - # Kill the process if it doesn't terminate after 5 seconds - process.kill() - - # run the model server - def run_model_server(self, port=8080): - """Run the Clarifai Runners's model server.""" - - command = [ - self.python_executable, "-m", "clarifai.runners.server", "--model_path", self.model_path, - "--grpc", "--port", - str(port) - ] - try: - logger.info( - f"Starting model server at localhost:{port} with the model at {self.model_path}...") - subprocess.check_call(command) - logger.info("Model server started successfully and running at localhost:{port}") - except subprocess.CalledProcessError as e: - logger.error(f"Error running model server: {e}") - self.clean_up() - sys.exit(1) - - def _docker_hash(self): - """Generate a hash of the combined requirements file and Dockefile""" - with open(self.requirements_file, "r") as f: - requirements_hash = hashlib.md5(f.read().encode('utf-8')).hexdigest() - with open(os.path.join(self.model_path, "Dockerfile"), "r") as f: - dockerfile_hash = hashlib.md5(f.read().encode('utf-8')).hexdigest() - - return hashlib.md5(f"{requirements_hash}{dockerfile_hash}".encode('utf-8')).hexdigest() - - def is_docker_installed(self): - """Checks if Docker is installed on the system.""" - try: - logger.info("Checking if Docker is installed...") - subprocess.run(["docker", "--version"], check=True) - return True - except subprocess.CalledProcessError: - logger.error( - "Docker is not installed! Please install Docker to run the model in a container.") - return False - - def build_docker_image( - self, - image_name="model_image", - ): - """Build the docker image using the Dockerfile in the model directory.""" - try: - logger.info(f"Building docker image from Dockerfile in {self.model_path}...") - - # since we don't want to copy the model directory into the container, we need to modify the Dockerfile and comment out the COPY instruction - dockerfile_path = os.path.join(self.model_path, "Dockerfile") - # Read the Dockerfile - with open(dockerfile_path, 'r') as file: - lines = file.readlines() - - # Comment out the COPY instruction that copies the current folder - modified_lines = [] - for line in lines: - if 'COPY' in line and '/home/nonroot/main' in line: - modified_lines.append(f'# {line}') - elif 'download-checkpoints' in line and '/home/nonroot/main' in line: - modified_lines.append(f'# {line}') - else: - modified_lines.append(line) - - # Create a temporary directory to store the modified Dockerfile - with tempfile.TemporaryDirectory() as temp_dir: - temp_dockerfile_path = os.path.join(temp_dir, "Dockerfile.temp") - - # Write the modified Dockerfile to the temporary file - with open(temp_dockerfile_path, 'w') as file: - file.writelines(modified_lines) - - # Build the Docker image using the temporary Dockerfile - subprocess.check_call( - ['docker', 'build', '-t', image_name, '-f', temp_dockerfile_path, self.model_path]) - logger.info(f"Docker image '{image_name}' built successfully!") - except subprocess.CalledProcessError as e: - logger.info(f"Error occurred while building the Docker image: {e}") - sys.exit(1) - - def docker_image_exists(self, image_name): - """Check if the Docker image exists.""" - try: - logger.info(f"Checking if Docker image '{image_name}' exists...") - subprocess.run(["docker", "inspect", image_name], check=True) - logger.info(f"Docker image '{image_name}' exists!") - return True - except subprocess.CalledProcessError: - logger.info(f"Docker image '{image_name}' does not exist!") - return False - - def _gpu_is_available(self): - """ - Checks if nvidia-smi is available, indicating a GPU is likely accessible. - """ - return shutil.which("nvidia-smi") is not None - - def run_docker_container(self, - image_name, - container_name="clarifai-model-container", - port=8080, - env_vars=None): - """Runs a Docker container from the specified image.""" - try: - logger.info(f"Running Docker container '{container_name}' from image '{image_name}'...") - # Base docker run command - cmd = ["docker", "run", "--name", container_name, '--rm', "--network", "host"] - if self._gpu_is_available(): - cmd.extend(["--gpus", "all"]) - # Add volume mappings - cmd.extend(["-v", f"{self.model_path}:/home/nonroot/main"]) - # Add environment variables - if env_vars: - for key, value in env_vars.items(): - cmd.extend(["-e", f"{key}={value}"]) - # Add the image name - cmd.append(image_name) - # update the CMD to run the server - cmd.extend(["--model_path", "/home/nonroot/main", "--grpc", "--port", str(port)]) - # Run the container - process = subprocess.Popen(cmd,) - logger.info( - f"Docker container '{container_name}' is running successfully! access the model at http://localhost:{port}" - ) - - # Function to handle Ctrl+C (SIGINT) gracefully - def signal_handler(sig, frame): - logger.info(f"Stopping Docker container '{container_name}'...") - subprocess.run(["docker", "stop", container_name], check=True) - process.terminate() - logger.info(f"Docker container '{container_name}' stopped successfully!") - time.sleep(1) - sys.exit(0) - - # Register the signal handler for SIGINT (Ctrl+C) - signal.signal(signal.SIGINT, signal_handler) - # Wait for the process to finish (keeps the container running until it's stopped) - process.wait() - except subprocess.CalledProcessError as e: - logger.info(f"Error occurred while running the Docker container: {e}") - sys.exit(1) - except Exception as e: - logger.info(f"Error occurred while running the Docker container: {e}") - sys.exit(1) - - def test_model_container(self, - image_name, - container_name="clarifai-model-container", - env_vars=None): - """Test the model inside the Docker container.""" - try: - logger.info("Testing the model inside the Docker container...") - # Base docker run command - cmd = ["docker", "run", "--name", container_name, '--rm', "--network", "host"] - if self._gpu_is_available(): - cmd.extend(["--gpus", "all"]) - # update the entrypoint for testing the model - cmd.extend(["--entrypoint", "python"]) - # Add volume mappings - cmd.extend(["-v", f"{self.model_path}:/home/nonroot/main"]) - # Add environment variables - if env_vars: - for key, value in env_vars.items(): - cmd.extend(["-e", f"{key}={value}"]) - # Add the image name - cmd.append(image_name) - # update the CMD to test the model inside the container - cmd.extend([ - "-c", - "from clarifai.runners.models.model_run_locally import ModelRunLocally; ModelRunLocally('/home/nonroot/main')._run_test()" - ]) - # Run the container - subprocess.check_call(cmd) - logger.info("Model tested successfully!") - except subprocess.CalledProcessError as e: - logger.error(f"Error testing the model inside the Docker container: {e}") - sys.exit(1) - - def container_exists(self, container_name="clarifai-model-container"): - """Check if the Docker container exists.""" - try: - # Run docker ps -a to list all containers (running and stopped) - result = subprocess.run( - ["docker", "ps", "-a", "--filter", f"name={container_name}", "--format", "{{.Names}}"], - check=True, - capture_output=True, - text=True) - # If the container name is returned, it exists - if result.stdout.strip() == container_name: - logger.info(f"Docker container '{container_name}' exists.") - return True - else: - return False - except subprocess.CalledProcessError as e: - logger.error(f"Error occurred while checking if container exists: {e}") - return False - - def stop_docker_container(self, container_name="clarifai-model-container"): - """Stop the Docker container if it's running.""" - try: - # Check if the container is running - result = subprocess.run( - ["docker", "ps", "--filter", f"name={container_name}", "--format", "{{.Names}}"], - check=True, - capture_output=True, - text=True) - if result.stdout.strip() == container_name: - logger.info(f"Docker container '{container_name}' is running. Stopping it...") - subprocess.run(["docker", "stop", container_name], check=True) - logger.info(f"Docker container '{container_name}' stopped successfully!") - except subprocess.CalledProcessError as e: - logger.error(f"Error occurred while stopping the Docker container: {e}") - - def remove_docker_container(self, container_name="clarifai-model-container"): - """Remove the Docker container.""" - try: - logger.info(f"Removing Docker container '{container_name}'...") - subprocess.run(["docker", "rm", container_name], check=True) - logger.info(f"Docker container '{container_name}' removed successfully!") - except subprocess.CalledProcessError as e: - logger.error(f"Error occurred while removing the Docker container: {e}") - - def remove_docker_image(self, image_name): - """Remove the Docker image.""" - try: - logger.info(f"Removing Docker image '{image_name}'...") - subprocess.run(["docker", "rmi", image_name], check=True) - logger.info(f"Docker image '{image_name}' removed successfully!") - except subprocess.CalledProcessError as e: - logger.error(f"Error occurred while removing the Docker image: {e}") - - def clean_up(self): - """Clean up the temporary virtual environment.""" - if os.path.exists(self.temp_dir): - logger.info("Cleaning up temporary virtual environment...") - shutil.rmtree(self.temp_dir) - - -def main(model_path, - run_model_server=False, - inside_container=False, - port=8080, - keep_env=False, - keep_image=False, - skip_dockerfile: bool = False): - - manager = ModelRunLocally(model_path) - # get whatever stage is in config.yaml to force download now - # also always write to where upload/build wants to, not the /tmp folder that runtime stage uses - _, _, _, when = manager.builder._validate_config_checkpoints() - manager.builder.download_checkpoints( - stage=when, checkpoint_path_override=manager.builder.checkpoint_path) - if inside_container: - if not manager.is_docker_installed(): - sys.exit(1) - if not skip_dockerfile: - manager.builder.create_dockerfile() - image_tag = manager._docker_hash() - model_id = manager.config['model']['id'].lower() - # must be in lowercase - image_name = f"{model_id}:{image_tag}" - container_name = model_id - if not manager.docker_image_exists(image_name): - manager.build_docker_image(image_name=image_name) - try: - if run_model_server: - manager.run_docker_container( - image_name=image_name, container_name=container_name, port=port) - else: - manager.test_model_container(image_name=image_name, container_name=container_name) - finally: - if manager.container_exists(container_name): - manager.stop_docker_container(container_name) - manager.remove_docker_container(container_name=container_name) - if not keep_image: - manager.remove_docker_image(image_name) - - else: - try: - use_existing_env = manager.create_temp_venv() - if not use_existing_env: - manager.install_requirements() - if run_model_server: - manager.run_model_server(port) - else: - manager.test_model() - finally: - if not keep_env: - manager.clean_up() + use_existing_env = manager.create_temp_venv() + if not use_existing_env: + manager.install_requirements() + if run_model_server: + manager.run_model_server(port) + else: + manager.test_model() + finally: + if not keep_env: + manager.clean_up() From 76b15874bc549ba111246b050c0460c568519d8d Mon Sep 17 00:00:00 2001 From: Mansi Khamkar Date: Thu, 27 Mar 2025 21:59:42 +0530 Subject: [PATCH 4/6] printed last token in stream --- clarifai/runners/models/model_run_locally.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/clarifai/runners/models/model_run_locally.py b/clarifai/runners/models/model_run_locally.py index 30a9a4ac..3d884b7f 100644 --- a/clarifai/runners/models/model_run_locally.py +++ b/clarifai/runners/models/model_run_locally.py @@ -10,6 +10,7 @@ import traceback import venv import base64 +import itertools from clarifai_grpc.grpc.api import resources_pb2, service_pb2 @@ -288,12 +289,12 @@ def _run_model_on_single_request(self, model, request, stream_request): ) if stream_response: - stream_first_res = next(stream_response) - if stream_first_res.outputs[0].status.code != status_code_pb2.SUCCESS: - logger.error(f"Model Prediction failed: {stream_first_res}") + stream_last_res = next(itertools.islice(stream_response, len(list(stream_response))-1, None)) + if stream_last_res.outputs[0].status.code != status_code_pb2.SUCCESS: + logger.error(f"Model Prediction failed: {stream_last_res}") else: logger.info( - f"Model Prediction succeeded for stream and first response: {stream_first_res}" + f"Model Prediction succeeded for stream and last response: {stream_last_res}" ) def _run_model_inference(self, model): From be4f456ea50f94bbab6d34a024f5292b797b4371 Mon Sep 17 00:00:00 2001 From: Mansi Khamkar Date: Wed, 2 Apr 2025 15:18:36 +0530 Subject: [PATCH 5/6] printed all tokens in stream --- clarifai/runners/models/model_run_locally.py | 3 +++ tests/runners/test_model_run_locally.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/clarifai/runners/models/model_run_locally.py b/clarifai/runners/models/model_run_locally.py index 3d884b7f..ead05876 100644 --- a/clarifai/runners/models/model_run_locally.py +++ b/clarifai/runners/models/model_run_locally.py @@ -289,6 +289,9 @@ def _run_model_on_single_request(self, model, request, stream_request): ) if stream_response: + stream_output = "" + for i, token in enumerate(stream_response): + stream_output += token stream_last_res = next(itertools.islice(stream_response, len(list(stream_response))-1, None)) if stream_last_res.outputs[0].status.code != status_code_pb2.SUCCESS: logger.error(f"Model Prediction failed: {stream_last_res}") diff --git a/tests/runners/test_model_run_locally.py b/tests/runners/test_model_run_locally.py index 5122df85..9f9a4edc 100644 --- a/tests/runners/test_model_run_locally.py +++ b/tests/runners/test_model_run_locally.py @@ -79,9 +79,14 @@ def test_build_request(model_run_locally): """ Test that _build_request returns a well-formed PostModelOutputsRequest """ - request = model_run_locally._build_request() - assert request is not None - assert len(request.inputs) == 1, "Expected exactly one input in constructed request." + requests = model_run_locally._build_request() + assert requests is not None + assert requests is not [] + assert isinstance(requests, list), "Expected a list of requests." + assert len(requests) >=1, "Expected at least one request to be built." + # Check that each request has the expected attributes + for req in requests: + assert len(req.inputs) == 1, "Expected exactly one input in constructed request." def test_create_temp_venv(model_run_locally): From 372393f0e704fd9101b8d74890232d5d66334716 Mon Sep 17 00:00:00 2001 From: Mansi Khamkar Date: Wed, 2 Apr 2025 16:01:54 +0530 Subject: [PATCH 6/6] test fix --- clarifai/runners/models/model_run_locally.py | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/clarifai/runners/models/model_run_locally.py b/clarifai/runners/models/model_run_locally.py index 83e98e4e..4a349f26 100644 --- a/clarifai/runners/models/model_run_locally.py +++ b/clarifai/runners/models/model_run_locally.py @@ -707,15 +707,15 @@ def main(model_path, if not keep_image: manager.remove_docker_image(image_name) - else: - try: - use_existing_env = manager.create_temp_venv() - if not use_existing_env: - manager.install_requirements() - if run_model_server: - manager.run_model_server(port) - else: - manager.test_model() - finally: - if not keep_env: - manager.clean_up() + else: + try: + use_existing_env = manager.create_temp_venv() + if not use_existing_env: + manager.install_requirements() + if run_model_server: + manager.run_model_server(port) + else: + manager.test_model() + finally: + if not keep_env: + manager.clean_up()