snowflake-ml-python 1.0.1__py3-none-any.whl → 1.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. snowflake/ml/_internal/env_utils.py +2 -1
  2. snowflake/ml/_internal/file_utils.py +35 -40
  3. snowflake/ml/_internal/telemetry.py +5 -8
  4. snowflake/ml/_internal/utils/identifier.py +74 -7
  5. snowflake/ml/_internal/utils/uri.py +7 -2
  6. snowflake/ml/model/_core_requirements.py +1 -1
  7. snowflake/ml/model/_deploy_client/image_builds/base_image_builder.py +15 -0
  8. snowflake/ml/model/_deploy_client/image_builds/client_image_builder.py +259 -0
  9. snowflake/ml/model/_deploy_client/image_builds/docker_context.py +89 -0
  10. snowflake/ml/model/_deploy_client/image_builds/gunicorn_run.sh +24 -0
  11. snowflake/ml/model/_deploy_client/image_builds/inference_server/main.py +118 -0
  12. snowflake/ml/model/_deploy_client/image_builds/templates/dockerfile_template +40 -0
  13. snowflake/ml/model/_deploy_client/snowservice/deploy.py +199 -0
  14. snowflake/ml/model/_deploy_client/snowservice/deploy_options.py +88 -0
  15. snowflake/ml/model/_deploy_client/snowservice/templates/service_spec_template +24 -0
  16. snowflake/ml/model/_deploy_client/utils/constants.py +47 -0
  17. snowflake/ml/model/_deploy_client/utils/snowservice_client.py +178 -0
  18. snowflake/ml/model/_deploy_client/warehouse/deploy.py +25 -28
  19. snowflake/ml/model/_deploy_client/warehouse/infer_template.py +7 -4
  20. snowflake/ml/model/_deployer.py +14 -27
  21. snowflake/ml/model/_env.py +4 -4
  22. snowflake/ml/model/_handlers/_base.py +3 -1
  23. snowflake/ml/model/_handlers/custom.py +14 -2
  24. snowflake/ml/model/_handlers/pytorch.py +186 -0
  25. snowflake/ml/model/_handlers/sklearn.py +14 -8
  26. snowflake/ml/model/_handlers/snowmlmodel.py +14 -9
  27. snowflake/ml/model/_handlers/torchscript.py +180 -0
  28. snowflake/ml/model/_handlers/xgboost.py +19 -9
  29. snowflake/ml/model/_model.py +27 -21
  30. snowflake/ml/model/_model_meta.py +33 -19
  31. snowflake/ml/model/model_signature.py +446 -66
  32. snowflake/ml/model/type_hints.py +28 -15
  33. snowflake/ml/modeling/calibration/calibrated_classifier_cv.py +79 -43
  34. snowflake/ml/modeling/cluster/affinity_propagation.py +79 -43
  35. snowflake/ml/modeling/cluster/agglomerative_clustering.py +79 -43
  36. snowflake/ml/modeling/cluster/birch.py +79 -43
  37. snowflake/ml/modeling/cluster/bisecting_k_means.py +79 -43
  38. snowflake/ml/modeling/cluster/dbscan.py +79 -43
  39. snowflake/ml/modeling/cluster/feature_agglomeration.py +79 -43
  40. snowflake/ml/modeling/cluster/k_means.py +79 -43
  41. snowflake/ml/modeling/cluster/mean_shift.py +79 -43
  42. snowflake/ml/modeling/cluster/mini_batch_k_means.py +79 -43
  43. snowflake/ml/modeling/cluster/optics.py +79 -43
  44. snowflake/ml/modeling/cluster/spectral_biclustering.py +79 -43
  45. snowflake/ml/modeling/cluster/spectral_clustering.py +79 -43
  46. snowflake/ml/modeling/cluster/spectral_coclustering.py +79 -43
  47. snowflake/ml/modeling/compose/column_transformer.py +79 -43
  48. snowflake/ml/modeling/compose/transformed_target_regressor.py +79 -43
  49. snowflake/ml/modeling/covariance/elliptic_envelope.py +79 -43
  50. snowflake/ml/modeling/covariance/empirical_covariance.py +79 -43
  51. snowflake/ml/modeling/covariance/graphical_lasso.py +79 -43
  52. snowflake/ml/modeling/covariance/graphical_lasso_cv.py +79 -43
  53. snowflake/ml/modeling/covariance/ledoit_wolf.py +79 -43
  54. snowflake/ml/modeling/covariance/min_cov_det.py +79 -43
  55. snowflake/ml/modeling/covariance/oas.py +79 -43
  56. snowflake/ml/modeling/covariance/shrunk_covariance.py +79 -43
  57. snowflake/ml/modeling/decomposition/dictionary_learning.py +79 -43
  58. snowflake/ml/modeling/decomposition/factor_analysis.py +79 -43
  59. snowflake/ml/modeling/decomposition/fast_ica.py +79 -43
  60. snowflake/ml/modeling/decomposition/incremental_pca.py +79 -43
  61. snowflake/ml/modeling/decomposition/kernel_pca.py +79 -43
  62. snowflake/ml/modeling/decomposition/mini_batch_dictionary_learning.py +79 -43
  63. snowflake/ml/modeling/decomposition/mini_batch_sparse_pca.py +79 -43
  64. snowflake/ml/modeling/decomposition/pca.py +79 -43
  65. snowflake/ml/modeling/decomposition/sparse_pca.py +79 -43
  66. snowflake/ml/modeling/decomposition/truncated_svd.py +79 -43
  67. snowflake/ml/modeling/discriminant_analysis/linear_discriminant_analysis.py +79 -43
  68. snowflake/ml/modeling/discriminant_analysis/quadratic_discriminant_analysis.py +79 -43
  69. snowflake/ml/modeling/ensemble/ada_boost_classifier.py +79 -43
  70. snowflake/ml/modeling/ensemble/ada_boost_regressor.py +79 -43
  71. snowflake/ml/modeling/ensemble/bagging_classifier.py +79 -43
  72. snowflake/ml/modeling/ensemble/bagging_regressor.py +79 -43
  73. snowflake/ml/modeling/ensemble/extra_trees_classifier.py +79 -43
  74. snowflake/ml/modeling/ensemble/extra_trees_regressor.py +79 -43
  75. snowflake/ml/modeling/ensemble/gradient_boosting_classifier.py +79 -43
  76. snowflake/ml/modeling/ensemble/gradient_boosting_regressor.py +79 -43
  77. snowflake/ml/modeling/ensemble/hist_gradient_boosting_classifier.py +79 -43
  78. snowflake/ml/modeling/ensemble/hist_gradient_boosting_regressor.py +79 -43
  79. snowflake/ml/modeling/ensemble/isolation_forest.py +79 -43
  80. snowflake/ml/modeling/ensemble/random_forest_classifier.py +79 -43
  81. snowflake/ml/modeling/ensemble/random_forest_regressor.py +79 -43
  82. snowflake/ml/modeling/ensemble/stacking_regressor.py +79 -43
  83. snowflake/ml/modeling/ensemble/voting_classifier.py +79 -43
  84. snowflake/ml/modeling/ensemble/voting_regressor.py +79 -43
  85. snowflake/ml/modeling/feature_selection/generic_univariate_select.py +79 -43
  86. snowflake/ml/modeling/feature_selection/select_fdr.py +79 -43
  87. snowflake/ml/modeling/feature_selection/select_fpr.py +79 -43
  88. snowflake/ml/modeling/feature_selection/select_fwe.py +79 -43
  89. snowflake/ml/modeling/feature_selection/select_k_best.py +79 -43
  90. snowflake/ml/modeling/feature_selection/select_percentile.py +79 -43
  91. snowflake/ml/modeling/feature_selection/sequential_feature_selector.py +79 -43
  92. snowflake/ml/modeling/feature_selection/variance_threshold.py +79 -43
  93. snowflake/ml/modeling/gaussian_process/gaussian_process_classifier.py +79 -43
  94. snowflake/ml/modeling/gaussian_process/gaussian_process_regressor.py +79 -43
  95. snowflake/ml/modeling/impute/iterative_imputer.py +79 -43
  96. snowflake/ml/modeling/impute/knn_imputer.py +79 -43
  97. snowflake/ml/modeling/impute/missing_indicator.py +79 -43
  98. snowflake/ml/modeling/kernel_approximation/additive_chi2_sampler.py +79 -43
  99. snowflake/ml/modeling/kernel_approximation/nystroem.py +79 -43
  100. snowflake/ml/modeling/kernel_approximation/polynomial_count_sketch.py +79 -43
  101. snowflake/ml/modeling/kernel_approximation/rbf_sampler.py +79 -43
  102. snowflake/ml/modeling/kernel_approximation/skewed_chi2_sampler.py +79 -43
  103. snowflake/ml/modeling/kernel_ridge/kernel_ridge.py +79 -43
  104. snowflake/ml/modeling/lightgbm/lgbm_classifier.py +79 -43
  105. snowflake/ml/modeling/lightgbm/lgbm_regressor.py +79 -43
  106. snowflake/ml/modeling/linear_model/ard_regression.py +79 -43
  107. snowflake/ml/modeling/linear_model/bayesian_ridge.py +79 -43
  108. snowflake/ml/modeling/linear_model/elastic_net.py +79 -43
  109. snowflake/ml/modeling/linear_model/elastic_net_cv.py +79 -43
  110. snowflake/ml/modeling/linear_model/gamma_regressor.py +79 -43
  111. snowflake/ml/modeling/linear_model/huber_regressor.py +79 -43
  112. snowflake/ml/modeling/linear_model/lars.py +79 -43
  113. snowflake/ml/modeling/linear_model/lars_cv.py +79 -43
  114. snowflake/ml/modeling/linear_model/lasso.py +79 -43
  115. snowflake/ml/modeling/linear_model/lasso_cv.py +79 -43
  116. snowflake/ml/modeling/linear_model/lasso_lars.py +79 -43
  117. snowflake/ml/modeling/linear_model/lasso_lars_cv.py +79 -43
  118. snowflake/ml/modeling/linear_model/lasso_lars_ic.py +79 -43
  119. snowflake/ml/modeling/linear_model/linear_regression.py +79 -43
  120. snowflake/ml/modeling/linear_model/logistic_regression.py +79 -43
  121. snowflake/ml/modeling/linear_model/logistic_regression_cv.py +79 -43
  122. snowflake/ml/modeling/linear_model/multi_task_elastic_net.py +79 -43
  123. snowflake/ml/modeling/linear_model/multi_task_elastic_net_cv.py +79 -43
  124. snowflake/ml/modeling/linear_model/multi_task_lasso.py +79 -43
  125. snowflake/ml/modeling/linear_model/multi_task_lasso_cv.py +79 -43
  126. snowflake/ml/modeling/linear_model/orthogonal_matching_pursuit.py +79 -43
  127. snowflake/ml/modeling/linear_model/passive_aggressive_classifier.py +79 -43
  128. snowflake/ml/modeling/linear_model/passive_aggressive_regressor.py +79 -43
  129. snowflake/ml/modeling/linear_model/perceptron.py +79 -43
  130. snowflake/ml/modeling/linear_model/poisson_regressor.py +79 -43
  131. snowflake/ml/modeling/linear_model/ransac_regressor.py +79 -43
  132. snowflake/ml/modeling/linear_model/ridge.py +79 -43
  133. snowflake/ml/modeling/linear_model/ridge_classifier.py +79 -43
  134. snowflake/ml/modeling/linear_model/ridge_classifier_cv.py +79 -43
  135. snowflake/ml/modeling/linear_model/ridge_cv.py +79 -43
  136. snowflake/ml/modeling/linear_model/sgd_classifier.py +79 -43
  137. snowflake/ml/modeling/linear_model/sgd_one_class_svm.py +79 -43
  138. snowflake/ml/modeling/linear_model/sgd_regressor.py +79 -43
  139. snowflake/ml/modeling/linear_model/theil_sen_regressor.py +79 -43
  140. snowflake/ml/modeling/linear_model/tweedie_regressor.py +79 -43
  141. snowflake/ml/modeling/manifold/isomap.py +79 -43
  142. snowflake/ml/modeling/manifold/mds.py +79 -43
  143. snowflake/ml/modeling/manifold/spectral_embedding.py +79 -43
  144. snowflake/ml/modeling/manifold/tsne.py +79 -43
  145. snowflake/ml/modeling/metrics/classification.py +6 -1
  146. snowflake/ml/modeling/metrics/regression.py +517 -9
  147. snowflake/ml/modeling/mixture/bayesian_gaussian_mixture.py +79 -43
  148. snowflake/ml/modeling/mixture/gaussian_mixture.py +79 -43
  149. snowflake/ml/modeling/model_selection/grid_search_cv.py +79 -43
  150. snowflake/ml/modeling/model_selection/randomized_search_cv.py +79 -43
  151. snowflake/ml/modeling/multiclass/one_vs_one_classifier.py +79 -43
  152. snowflake/ml/modeling/multiclass/one_vs_rest_classifier.py +79 -43
  153. snowflake/ml/modeling/multiclass/output_code_classifier.py +79 -43
  154. snowflake/ml/modeling/naive_bayes/bernoulli_nb.py +79 -43
  155. snowflake/ml/modeling/naive_bayes/categorical_nb.py +79 -43
  156. snowflake/ml/modeling/naive_bayes/complement_nb.py +79 -43
  157. snowflake/ml/modeling/naive_bayes/gaussian_nb.py +79 -43
  158. snowflake/ml/modeling/naive_bayes/multinomial_nb.py +79 -43
  159. snowflake/ml/modeling/neighbors/k_neighbors_classifier.py +79 -43
  160. snowflake/ml/modeling/neighbors/k_neighbors_regressor.py +79 -43
  161. snowflake/ml/modeling/neighbors/kernel_density.py +79 -43
  162. snowflake/ml/modeling/neighbors/local_outlier_factor.py +79 -43
  163. snowflake/ml/modeling/neighbors/nearest_centroid.py +79 -43
  164. snowflake/ml/modeling/neighbors/nearest_neighbors.py +79 -43
  165. snowflake/ml/modeling/neighbors/neighborhood_components_analysis.py +79 -43
  166. snowflake/ml/modeling/neighbors/radius_neighbors_classifier.py +79 -43
  167. snowflake/ml/modeling/neighbors/radius_neighbors_regressor.py +79 -43
  168. snowflake/ml/modeling/neural_network/bernoulli_rbm.py +79 -43
  169. snowflake/ml/modeling/neural_network/mlp_classifier.py +79 -43
  170. snowflake/ml/modeling/neural_network/mlp_regressor.py +79 -43
  171. snowflake/ml/modeling/pipeline/pipeline.py +24 -0
  172. snowflake/ml/modeling/preprocessing/one_hot_encoder.py +18 -19
  173. snowflake/ml/modeling/preprocessing/ordinal_encoder.py +2 -0
  174. snowflake/ml/modeling/preprocessing/polynomial_features.py +79 -43
  175. snowflake/ml/modeling/semi_supervised/label_propagation.py +79 -43
  176. snowflake/ml/modeling/semi_supervised/label_spreading.py +79 -43
  177. snowflake/ml/modeling/svm/linear_svc.py +79 -43
  178. snowflake/ml/modeling/svm/linear_svr.py +79 -43
  179. snowflake/ml/modeling/svm/nu_svc.py +79 -43
  180. snowflake/ml/modeling/svm/nu_svr.py +79 -43
  181. snowflake/ml/modeling/svm/svc.py +79 -43
  182. snowflake/ml/modeling/svm/svr.py +79 -43
  183. snowflake/ml/modeling/tree/decision_tree_classifier.py +79 -43
  184. snowflake/ml/modeling/tree/decision_tree_regressor.py +79 -43
  185. snowflake/ml/modeling/tree/extra_tree_classifier.py +79 -43
  186. snowflake/ml/modeling/tree/extra_tree_regressor.py +79 -43
  187. snowflake/ml/modeling/xgboost/xgb_classifier.py +79 -43
  188. snowflake/ml/modeling/xgboost/xgb_regressor.py +79 -43
  189. snowflake/ml/modeling/xgboost/xgbrf_classifier.py +79 -43
  190. snowflake/ml/modeling/xgboost/xgbrf_regressor.py +79 -43
  191. snowflake/ml/registry/model_registry.py +123 -121
  192. snowflake/ml/version.py +1 -1
  193. {snowflake_ml_python-1.0.1.dist-info → snowflake_ml_python-1.0.3.dist-info}/METADATA +50 -8
  194. snowflake_ml_python-1.0.3.dist-info/RECORD +259 -0
  195. snowflake_ml_python-1.0.1.dist-info/RECORD +0 -246
  196. {snowflake_ml_python-1.0.1.dist-info → snowflake_ml_python-1.0.3.dist-info}/WHEEL +0 -0
@@ -0,0 +1,89 @@
1
+ import importlib
2
+ import os
3
+ import shutil
4
+ import string
5
+ from abc import ABC
6
+
7
+ from snowflake.ml.model._deploy_client.utils import constants
8
+
9
+
10
+ class DockerContext(ABC):
11
+ """
12
+ Constructs the Docker context directory required for image building.
13
+ """
14
+
15
+ def __init__(self, context_dir: str, model_dir: str, *, use_gpu: bool = False) -> None:
16
+ """Initialization
17
+
18
+ Args:
19
+ context_dir: Path to context directory.
20
+ model_dir: Path to local model directory.
21
+ use_gpu: Boolean flag for generating the CPU or GPU base image.
22
+ """
23
+ self.context_dir = context_dir
24
+ self.model_dir = model_dir
25
+ # TODO(shchen): SNOW-825995, Define dockerfile template used for model deployment. use_gpu will be used.
26
+ self.use_gpu = use_gpu
27
+
28
+ def build(self) -> None:
29
+ """
30
+ Generates and/or moves resources into the Docker context directory.Rename the random model directory name to
31
+ constant "model_dir" instead for better readability.
32
+ """
33
+ self._generate_inference_code()
34
+ self._copy_entrypoint_script_to_docker_context()
35
+ self._copy_snowml_source_code_to_docker_context()
36
+ self._generate_docker_file()
37
+
38
+ def _copy_snowml_source_code_to_docker_context(self) -> None:
39
+ """Copy the entire snowflake/ml source code to docker context. This will be particularly useful for CI tests
40
+ against latest changes.
41
+
42
+ Note that we exclude the experimental directory mainly for development scenario; as experimental directory won't
43
+ be included in the release.
44
+ """
45
+ snow_ml_source_dir = list(importlib.import_module("snowflake.ml").__path__)[0]
46
+ shutil.copytree(
47
+ snow_ml_source_dir,
48
+ os.path.join(self.context_dir, "snowflake", "ml"),
49
+ ignore=shutil.ignore_patterns("*.pyc", "experimental"),
50
+ )
51
+
52
+ def _copy_entrypoint_script_to_docker_context(self) -> None:
53
+ """Copy gunicorn_run.sh entrypoint to docker context directory."""
54
+ path = os.path.join(os.path.dirname(__file__), constants.ENTRYPOINT_SCRIPT)
55
+ assert os.path.exists(path), f"Run script file missing at path: {path}"
56
+ shutil.copy(path, os.path.join(self.context_dir, constants.ENTRYPOINT_SCRIPT))
57
+
58
+ def _generate_docker_file(self) -> None:
59
+ """
60
+ Generates dockerfile based on dockerfile template.
61
+ """
62
+ docker_file_path = os.path.join(self.context_dir, "Dockerfile")
63
+ docker_file_template = os.path.join(os.path.dirname(__file__), "templates/dockerfile_template")
64
+
65
+ with open(docker_file_path, "w", encoding="utf-8") as dockerfile, open(
66
+ docker_file_template, encoding="utf-8"
67
+ ) as template:
68
+ dockerfile_content = string.Template(template.read()).safe_substitute(
69
+ {
70
+ # TODO(shchen): SNOW-835411, Support overwriting base image
71
+ "base_image": "mambaorg/micromamba:focal-cuda-11.7.1"
72
+ if self.use_gpu
73
+ else "mambaorg/micromamba:1.4.3",
74
+ "model_dir": constants.MODEL_DIR,
75
+ "inference_server_dir": constants.INFERENCE_SERVER_DIR,
76
+ "entrypoint_script": constants.ENTRYPOINT_SCRIPT,
77
+ }
78
+ )
79
+ dockerfile.write(dockerfile_content)
80
+
81
+ def _generate_inference_code(self) -> None:
82
+ """
83
+ Generates inference code based on the app template and creates a folder named 'server' to house the inference
84
+ server code.
85
+ """
86
+ inference_server_folder_path = os.path.join(os.path.dirname(__file__), constants.INFERENCE_SERVER_DIR)
87
+ destination_folder_path = os.path.join(self.context_dir, constants.INFERENCE_SERVER_DIR)
88
+ ignore_patterns = shutil.ignore_patterns("BUILD.bazel", "*test.py", "*.\\.*", "__pycache__")
89
+ shutil.copytree(inference_server_folder_path, destination_folder_path, ignore=ignore_patterns)
@@ -0,0 +1,24 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ OS=$(uname)
5
+
6
+ if [ "${OS}" = "Linux" ]; then
7
+ NUM_CORES=$(nproc)
8
+ elif [ "${OS}" = "Darwin" ]; then
9
+ # macOS
10
+ NUM_CORES=$(sysctl -n hw.ncpu)
11
+ elif [ "${OS}" = "Windows" ]; then
12
+ NUM_CORES=$(wmic cpu get NumberOfCores | grep -Eo '[0-9]+')
13
+ else
14
+ echo "Unsupported operating system: ${OS}"
15
+ exit 1
16
+ fi
17
+
18
+ # Based on the Gunicorn documentation, set the number of workers to number_of_cores * 2 + 1. This assumption is
19
+ # based on an ideal scenario where one core is handling two processes simultaneously, while one process is dedicated to
20
+ # IO operations and the other process is performing compute tasks.
21
+ NUM_WORKERS=$((NUM_CORES * 2 + 1))
22
+ echo "Number of CPU cores: $NUM_CORES"
23
+ echo "Setting number of workers to $NUM_WORKERS"
24
+ exec /opt/conda/bin/gunicorn --preload -w "$NUM_WORKERS" -k uvicorn.workers.UvicornWorker -b 0.0.0.0:5000 inference_server.main:app
@@ -0,0 +1,118 @@
1
+ import logging
2
+ import os
3
+ import tempfile
4
+ import zipfile
5
+
6
+ import pandas as pd
7
+ from starlette import applications, requests, responses, routing
8
+
9
+ logger = logging.getLogger(__name__)
10
+ loaded_model = None
11
+
12
+
13
+ def _run_setup() -> None:
14
+ """Set up logging and load model into memory."""
15
+ # Align the application logger's handler with Gunicorn's to capture logs from all processes.
16
+ gunicorn_logger = logging.getLogger("gunicorn.error")
17
+ logger.handlers = gunicorn_logger.handlers
18
+ logger.setLevel(gunicorn_logger.level)
19
+
20
+ from snowflake.ml.model import _model as model_api
21
+
22
+ global loaded_model
23
+
24
+ MODEL_ZIP_STAGE_PATH = os.getenv("MODEL_ZIP_STAGE_PATH")
25
+ assert MODEL_ZIP_STAGE_PATH, "Missing environment variable MODEL_ZIP_STAGE_PATH"
26
+ root_path = os.path.abspath(os.sep)
27
+ model_zip_stage_path = os.path.join(root_path, MODEL_ZIP_STAGE_PATH)
28
+
29
+ with tempfile.TemporaryDirectory() as tmp_dir:
30
+ if zipfile.is_zipfile(model_zip_stage_path):
31
+ extracted_dir = os.path.join(tmp_dir, "extracted_model_dir")
32
+ logger.info(f"Extracting model zip from {model_zip_stage_path} to {extracted_dir}")
33
+ with zipfile.ZipFile(model_zip_stage_path, "r") as model_zip:
34
+ if len(model_zip.namelist()) > 1:
35
+ model_zip.extractall(extracted_dir)
36
+ else:
37
+ raise RuntimeError(f"No model zip found at stage path: {model_zip_stage_path}")
38
+ logger.info(f"Loading model from {extracted_dir} into memory")
39
+ loaded_model, _ = model_api._load_model_for_deploy(model_dir_path=extracted_dir)
40
+ logger.info("Successfully loaded model into memory")
41
+
42
+
43
+ async def ready(request: requests.Request) -> responses.JSONResponse:
44
+ """Endpoint to check if the application is ready."""
45
+ return responses.JSONResponse({"status": "ready"})
46
+
47
+
48
+ async def predict(request: requests.Request) -> responses.JSONResponse:
49
+ """Endpoint to make predictions based on input data.
50
+
51
+ Args:
52
+ request: The input data is expected to be in the following JSON format:
53
+ {
54
+ "data": [
55
+ [0, 5.1, 3.5, 4.2, 1.3],
56
+ [1, 4.7, 3.2, 4.1, 4.2]
57
+ }
58
+ Each row is represented as a list, where the first element denotes the index of the row.
59
+
60
+ Returns:
61
+ Two possible responses:
62
+ For success, return a JSON response {"data": [[0, 1], [1, 2]]}, where the first element of each resulting list
63
+ denotes the index of the row, and the rest of the elements represent the prediction results for that row.
64
+ For an error, return {"error": error_message, "status_code": http_response_status_code}.
65
+ """
66
+ try:
67
+ input = await request.json()
68
+ assert "data" in input, "missing data field in the request input"
69
+ # The expression x[1:] is used to exclude the index of the data row.
70
+ input_data = [x[1:] for x in input.get("data")]
71
+ x = pd.DataFrame(input_data)
72
+ assert len(input_data) != 0 and not all(not row for row in input_data), "empty data"
73
+ except Exception as e:
74
+ error_message = f"Input data malformed: {str(e)}"
75
+ return responses.JSONResponse({"error": error_message}, status_code=400)
76
+
77
+ assert loaded_model
78
+
79
+ try:
80
+ # TODO(shchen): SNOW-835369, Support target method in inference server (Multi-task model).
81
+ # Mypy ignore will be fixed along with the above ticket.
82
+ predictions = loaded_model.predict(x) # type: ignore[attr-defined]
83
+ result = predictions.to_records(index=True).tolist()
84
+ response = {"data": result}
85
+ return responses.JSONResponse(response)
86
+ except Exception as e:
87
+ error_message = f"Prediction failed: {str(e)}"
88
+ return responses.JSONResponse({"error": error_message}, status_code=400)
89
+
90
+
91
+ def _in_test_mode() -> bool:
92
+ """Check if the code is running in test mode.
93
+
94
+ Specifically, it checks for the presence of
95
+ - "PYTEST_CURRENT_TEST" environment variable, which is automatically set by Pytest when running tests, and
96
+ - "TEST_WORKSPACE" environment variable, which is set by Bazel test, and
97
+ - "TEST_SRCDIR" environment variable, which is set by the Absl test.
98
+
99
+ Returns:
100
+ True if in test mode; otherwise, returns False
101
+ """
102
+ is_running_under_py_test = "PYTEST_CURRENT_TEST" in os.environ
103
+ is_running_under_bazel_test = "TEST_WORKSPACE" in os.environ
104
+ is_running_under_absl_test = "TEST_SRCDIR" in os.environ
105
+ return is_running_under_py_test or is_running_under_bazel_test or is_running_under_absl_test
106
+
107
+
108
+ def run_app() -> applications.Starlette:
109
+ if not _in_test_mode():
110
+ _run_setup()
111
+ routes = [
112
+ routing.Route("/health", endpoint=ready, methods=["GET"]),
113
+ routing.Route("/predict", endpoint=predict, methods=["POST"]),
114
+ ]
115
+ return applications.Starlette(routes=routes)
116
+
117
+
118
+ app = run_app()
@@ -0,0 +1,40 @@
1
+ FROM $base_image as build
2
+
3
+ COPY $model_dir/env/conda.yaml conda.yaml
4
+ COPY $model_dir/env/requirements.txt requirements.txt
5
+
6
+ # Set MAMBA_DOCKERFILE_ACTIVATE=1 to activate the conda environment during build time.
7
+ ARG MAMBA_DOCKERFILE_ACTIVATE=1
8
+
9
+ # The micromamba image comes with an empty environment named base.
10
+ RUN --mount=type=cache,target=/opt/conda/pkgs micromamba install -y -n base -f conda.yaml && \
11
+ python -m pip install "uvicorn[standard]" gunicorn starlette && \
12
+ python -m pip install -r requirements.txt
13
+
14
+ FROM debian:buster-slim AS runtime
15
+
16
+ ENV USER nonrootuser
17
+ ENV UID 1000
18
+ ENV HOME /home/$USER
19
+ RUN adduser --disabled-password \
20
+ --gecos "A non-root user for running inference server" \
21
+ --uid $UID \
22
+ --home $HOME \
23
+ $USER
24
+
25
+ COPY $inference_server_dir ./$inference_server_dir
26
+ COPY $entrypoint_script ./$entrypoint_script
27
+ RUN chmod +x /$entrypoint_script
28
+ # Copy Snowflake/ml source code
29
+ # TODO: not needed as source code is either in model, or pulled from conda
30
+ COPY snowflake ./snowflake
31
+
32
+ # The mamba root prefix by default is set to /opt/conda, in which the base conda environment is built at.
33
+ COPY --from=build /opt/conda /opt/conda
34
+
35
+ # Expose the port on which the Starlette app will run.
36
+ EXPOSE 5000
37
+
38
+ USER nonrootuser
39
+
40
+ CMD ["/$entrypoint_script"]
@@ -0,0 +1,199 @@
1
+ import logging
2
+ import os
3
+ import posixpath
4
+ import string
5
+ import tempfile
6
+ from abc import ABC
7
+ from typing import Any, Dict, cast
8
+
9
+ from typing_extensions import Unpack
10
+
11
+ from snowflake.ml.model._deploy_client.image_builds import (
12
+ base_image_builder,
13
+ client_image_builder,
14
+ )
15
+ from snowflake.ml.model._deploy_client.snowservice import deploy_options
16
+ from snowflake.ml.model._deploy_client.utils import constants, snowservice_client
17
+ from snowflake.snowpark import Session
18
+
19
+
20
+ def _deploy(
21
+ session: Session,
22
+ *,
23
+ model_id: str,
24
+ service_func_name: str,
25
+ model_zip_stage_path: str,
26
+ **kwargs: Unpack[deploy_options.SnowServiceDeployOptionsTypedHint],
27
+ ) -> None:
28
+ """Entrypoint for model deployment to SnowService. This function will trigger a docker image build followed by
29
+ workflow deployment to SnowService.
30
+
31
+ Args:
32
+ session: Snowpark session
33
+ model_id: Unique hex string of length 32, provided by model registry.
34
+ service_func_name: The service function name in SnowService associated with the created service.
35
+ model_zip_stage_path: Path to model zip file in stage. Note that this path has a "@" prefix.
36
+ **kwargs: various SnowService deployment options.
37
+
38
+ Raises:
39
+ ValueError: Raised when model_id is empty.
40
+ ValueError: Raised when service_func_name is empty.
41
+ ValueError: Raised when model_stage_file_path is empty.
42
+ """
43
+ snowpark_logger = logging.getLogger("snowflake.snowpark")
44
+ snowflake_connector_logger = logging.getLogger("snowflake.connector")
45
+ snowpark_log_level = snowpark_logger.level
46
+ snowflake_connector_log_level = snowflake_connector_logger.level
47
+ try:
48
+ # Setting appropriate log level to prevent console from being polluted by vast amount of snowpark and snowflake
49
+ # connector logging.
50
+ snowpark_logger.setLevel(logging.WARNING)
51
+ snowflake_connector_logger.setLevel(logging.WARNING)
52
+ if not model_id:
53
+ raise ValueError('Must provide a non-empty string for "model_id" when deploying to SnowService')
54
+ if not service_func_name:
55
+ raise ValueError('Must provide a non-empty string for "service_func_name" when deploying to SnowService')
56
+ if not model_zip_stage_path:
57
+ raise ValueError(
58
+ 'Must provide a non-empty string for "model_stage_file_path" when deploying to SnowService'
59
+ )
60
+ assert model_zip_stage_path.startswith("@"), f"stage path should start with @, actual: {model_zip_stage_path}"
61
+ options = deploy_options.SnowServiceDeployOptions.from_dict(cast(Dict[str, Any], kwargs))
62
+ image_builder = client_image_builder.ClientImageBuilder(
63
+ id=model_id, image_repo=options.image_repo, model_zip_stage_path=model_zip_stage_path, session=session
64
+ )
65
+ ss_deployment = SnowServiceDeployment(
66
+ session=session,
67
+ model_id=model_id,
68
+ service_func_name=service_func_name,
69
+ model_zip_stage_path=model_zip_stage_path,
70
+ image_builder=image_builder,
71
+ options=options,
72
+ )
73
+ ss_deployment.deploy()
74
+ finally:
75
+ # Preserve the original logging level.
76
+ snowpark_logger.setLevel(snowpark_log_level)
77
+ snowflake_connector_logger.setLevel(snowflake_connector_log_level)
78
+
79
+
80
+ class SnowServiceDeployment(ABC):
81
+ """
82
+ Class implementation that encapsulates image build and workflow deployment to SnowService
83
+
84
+ #TODO[shchen], SNOW-830093 GPU support on model deployment to SnowService
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ session: Session,
90
+ model_id: str,
91
+ service_func_name: str,
92
+ model_zip_stage_path: str,
93
+ image_builder: base_image_builder.ImageBuilder,
94
+ options: deploy_options.SnowServiceDeployOptions,
95
+ ) -> None:
96
+ """Initialization
97
+
98
+ Args:
99
+ session: Snowpark session
100
+ model_id: Unique hex string of length 32, provided by model registry; if not provided, auto-generate one for
101
+ resource naming.The model_id serves as an idempotent key throughout the deployment workflow.
102
+ service_func_name: The service function name in SnowService associated with the created service.
103
+ model_zip_stage_path: Path to model zip file in stage.
104
+ image_builder: InferenceImageBuilder instance that handles image build and upload to image registry.
105
+ options: A SnowServiceDeployOptions object containing deployment options.
106
+ """
107
+
108
+ self.session = session
109
+ self.id = model_id
110
+ self.service_func_name = service_func_name
111
+ self.model_zip_stage_path = model_zip_stage_path
112
+ self.image_builder = image_builder
113
+ self.options = options
114
+ self._service_name = f"service_{model_id}"
115
+ # Spec file and future deployment related artifacts will be stored under {stage}/models/{model_id}
116
+ self._model_artifact_stage_location = posixpath.join(options.stage, "models", self.id)
117
+
118
+ def deploy(self) -> None:
119
+ """
120
+ This function triggers image build followed by workflow deployment to SnowService.
121
+ """
122
+ if self.options.prebuilt_snowflake_image:
123
+ image = self.options.prebuilt_snowflake_image
124
+ logging.info(f"Skipped image build. Use Snowflake prebuilt image: {self.options.prebuilt_snowflake_image}")
125
+ else:
126
+ image = self._build_and_upload_image()
127
+ self._deploy_workflow(image)
128
+
129
+ def _build_and_upload_image(self) -> str:
130
+ """This function handles image build and upload to image registry.
131
+
132
+ Returns:
133
+ Path to the image in the remote image repository.
134
+ """
135
+ return self.image_builder.build_and_upload_image()
136
+
137
+ def _prepare_and_upload_artifacts_to_stage(self, image: str) -> None:
138
+ """Constructs and upload service spec to stage.
139
+
140
+ Args:
141
+ image: Name of the image to create SnowService container from.
142
+ """
143
+
144
+ with tempfile.TemporaryDirectory() as tempdir:
145
+ spec_template_path = os.path.join(os.path.dirname(__file__), "templates/service_spec_template")
146
+ spec_file_path = os.path.join(tempdir, f"{constants.SERVICE_SPEC}.yaml")
147
+
148
+ with open(spec_template_path, encoding="utf-8") as template, open(
149
+ spec_file_path, "w", encoding="utf-8"
150
+ ) as spec_file:
151
+ content = string.Template(template.read()).substitute(
152
+ {
153
+ "image": image,
154
+ "predict_endpoint_name": constants.PREDICT,
155
+ "stage": self.options.stage,
156
+ "model_zip_stage_path": self.model_zip_stage_path[1:], # Remove the @ prefix
157
+ "inference_server_container_name": constants.INFERENCE_SERVER_CONTAINER,
158
+ }
159
+ )
160
+ spec_file.write(content)
161
+ logging.info(f"Create service spec: \n {content}")
162
+
163
+ self.session.file.put(
164
+ local_file_name=spec_file_path,
165
+ stage_location=self._model_artifact_stage_location,
166
+ auto_compress=False,
167
+ overwrite=True,
168
+ )
169
+ logging.info(
170
+ f"Uploaded spec file {os.path.basename(spec_file_path)} " f"to {self._model_artifact_stage_location}"
171
+ )
172
+
173
+ def _deploy_workflow(self, image: str) -> None:
174
+ """This function handles workflow deployment to SnowService with the given image.
175
+
176
+ Args:
177
+ image: Name of the image to create SnowService container from.
178
+ """
179
+
180
+ self._prepare_and_upload_artifacts_to_stage(image)
181
+ client = snowservice_client.SnowServiceClient(self.session)
182
+ spec_stage_location = posixpath.join(
183
+ self._model_artifact_stage_location.rstrip("/"), f"{constants.SERVICE_SPEC}.yaml"
184
+ )
185
+ client.create_or_replace_service(
186
+ service_name=self._service_name,
187
+ compute_pool=self.options.compute_pool,
188
+ spec_stage_location=spec_stage_location,
189
+ min_instances=self.options.min_instances,
190
+ max_instances=self.options.max_instances,
191
+ )
192
+ client.block_until_resource_is_ready(
193
+ resource_name=self._service_name, resource_type=constants.ResourceType.SERVICE
194
+ )
195
+ client.create_or_replace_service_function(
196
+ service_func_name=self.service_func_name,
197
+ service_name=self._service_name,
198
+ endpoint_name=constants.PREDICT,
199
+ )
@@ -0,0 +1,88 @@
1
+ from typing import Any, Dict, Optional, TypedDict
2
+
3
+ from typing_extensions import NotRequired
4
+
5
+ from snowflake.ml.model._deploy_client.utils import constants
6
+
7
+
8
+ class SnowServiceDeployOptionsTypedHint(TypedDict):
9
+ """Deployment options for deploying to SnowService.
10
+
11
+ stage: the name of the stage for uploading artifacts.
12
+ compute_pool: SnowService compute pool name.
13
+ image_repo: SnowService image repo path. e.g. "<image_registry>/<db>/<schema>/<repo>"
14
+ min_instances: Minimum number of service replicas.
15
+ max_instances: Maximum number of service replicas.
16
+ endpoint: The specific name of the endpoint that the service function will communicate with. Default to
17
+ "predict". This option is useful when service has multiple endpoints.
18
+ overridden_base_image: When provided, it will override the base image.
19
+ """
20
+
21
+ stage: str
22
+ compute_pool: str
23
+ image_repo: str
24
+ min_instances: NotRequired[int]
25
+ max_instances: NotRequired[int]
26
+ endpoint: NotRequired[str]
27
+ overridden_base_image: NotRequired[str]
28
+
29
+
30
+ class SnowServiceDeployOptions:
31
+ def __init__(
32
+ self,
33
+ stage: str,
34
+ compute_pool: str,
35
+ image_repo: str,
36
+ *,
37
+ min_instances: int = 1,
38
+ max_instances: int = 1,
39
+ endpoint: str = constants.PREDICT,
40
+ overridden_base_image: Optional[str] = None,
41
+ prebuilt_snowflake_image: Optional[str] = None,
42
+ ) -> None:
43
+ """Initialization
44
+
45
+ Args:
46
+ stage: the name of the stage for uploading artifacts.
47
+ compute_pool: SnowService compute pool name.
48
+ image_repo: SnowService image repo path. e.g. "<image_registry>/<db>/<schema>/<repo>"
49
+ min_instances: Minimum number of service replicas.
50
+ max_instances: Maximum number of service replicas.
51
+ endpoint: The specific name of the endpoint that the service function will communicate with. Default to
52
+ "predict". This option is useful when service has multiple endpoints.
53
+ overridden_base_image: When provided, it will override the base image.
54
+ prebuilt_snowflake_image: When provided, the image building step is skipped, and the pre-built image from
55
+ Snowflake is used as is. This option is for users who consistently use the same image for multiple use
56
+ cases, allowing faster deployment. The snowflake image used for deployment is logged to the console for
57
+ future use.
58
+ """
59
+
60
+ self.stage = stage
61
+ self.compute_pool = compute_pool
62
+ self.image_repo = image_repo
63
+ self.min_instances = min_instances
64
+ self.max_instances = max_instances
65
+ self.endpoint = endpoint
66
+ self.overridden_base_image = overridden_base_image
67
+ self.prebuilt_snowflake_image = prebuilt_snowflake_image
68
+
69
+ @classmethod
70
+ def from_dict(cls, options_dict: Dict[str, Any]) -> "SnowServiceDeployOptions":
71
+ """Construct SnowServiceDeployOptions instance based from an option dictionary.
72
+
73
+ Args:
74
+ options_dict: The dict containing various deployment options.
75
+
76
+ Raises:
77
+ ValueError: When required option is missing.
78
+
79
+ Returns:
80
+ A SnowServiceDeployOptions object
81
+ """
82
+ required_options = [constants.STAGE, constants.COMPUTE_POOL, constants.IMAGE_REPO]
83
+ missing_keys = [key for key in required_options if options_dict.get(key) is None]
84
+ if missing_keys:
85
+ raise ValueError(f"Must provide options when deploying to SnowService: {', '.join(missing_keys)}")
86
+ # SnowService image repo cannot handle upper case repo name.
87
+ options_dict[constants.IMAGE_REPO] = options_dict[constants.IMAGE_REPO].lower()
88
+ return cls(**options_dict)
@@ -0,0 +1,24 @@
1
+ spec:
2
+ container:
3
+ - name: ${inference_server_container_name}
4
+ image: ${image}
5
+ env:
6
+ MODEL_ZIP_STAGE_PATH: ${model_zip_stage_path}
7
+ readinessProbe:
8
+ port: 5000
9
+ path: /health
10
+ volumeMounts:
11
+ - name: vol1
12
+ mountPath: /local/user/vol1
13
+ - name: stage
14
+ mountPath: ${stage}
15
+ endpoint:
16
+ - name: ${predict_endpoint_name}
17
+ port: 5000
18
+ volume:
19
+ - name: vol1
20
+ source: local # only local emptyDir volume is supported
21
+ - name: stage
22
+ source: "@${stage}"
23
+ uid: 1000
24
+ gid: 1000
@@ -0,0 +1,47 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ResourceType(Enum):
5
+ SERVICE = "service"
6
+ JOB = "job"
7
+
8
+
9
+ """
10
+ Potential SnowService status based on existing ResourceSetStatus proto:
11
+
12
+ github.com/snowflakedb/snowflake/blob/main/GlobalServices/src/main/protobuf/snowservices_resourceset_reconciler.proto
13
+ """
14
+
15
+
16
+ class ResourceStatus(Enum):
17
+ UNKNOWN = "UNKNOWN" # status is unknown because we have not received enough data from K8s yet.
18
+ PENDING = "PENDING" # resource set is being created, can't be used yet
19
+ READY = "READY" # resource set has been deployed.
20
+ DELETING = "DELETING" # resource set is being deleted
21
+ FAILED = "FAILED" # resource set has failed and cannot be used anymore
22
+ DONE = "DONE" # resource set has finished running
23
+ NOT_FOUND = "NOT_FOUND" # not found or deleted
24
+ INTERNAL_ERROR = "INTERNAL_ERROR" # there was an internal service error.
25
+
26
+
27
+ RESOURCE_TO_STATUS_FUNCTION_MAPPING = {
28
+ ResourceType.SERVICE: "SYSTEM$GET_SNOWSERVICE_STATUS",
29
+ ResourceType.JOB: "SYSTEM$GET_JOB_STATUS",
30
+ }
31
+
32
+ PREDICT = "predict"
33
+ STAGE = "stage"
34
+ COMPUTE_POOL = "compute_pool"
35
+ IMAGE_REPO = "image_repo"
36
+ MIN_INSTANCES = "min_instances"
37
+ MAX_INSTANCES = "max_instances"
38
+ GPU_COUNT = "gpu"
39
+ OVERRIDDEN_BASE_IMAGE = "image"
40
+ ENDPOINT = "endpoint"
41
+ SERVICE_SPEC = "service_spec"
42
+ INFERENCE_SERVER_CONTAINER = "inference-server"
43
+
44
+ """Image build related constants"""
45
+ MODEL_DIR = "model_dir"
46
+ INFERENCE_SERVER_DIR = "inference_server"
47
+ ENTRYPOINT_SCRIPT = "gunicorn_run.sh"