snowflake-ml-python 2.2.0__py3-none-any.whl → 2.4.0__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.
- snowflake/cortex/_classify_text.py +51 -5
- snowflake/cortex/_complete.py +41 -9
- snowflake/cortex/_embed_text_1024.py +11 -5
- snowflake/cortex/_embed_text_768.py +11 -5
- snowflake/cortex/_extract_answer.py +54 -12
- snowflake/cortex/_sentiment.py +50 -10
- snowflake/cortex/_summarize.py +11 -5
- snowflake/cortex/_translate.py +11 -5
- snowflake/cortex/_util.py +35 -0
- snowflake/ml/experiment/_entities/run.py +14 -6
- snowflake/ml/experiment/callback/catboost.py +75 -0
- snowflake/ml/experiment/experiment_tracking.py +41 -24
- snowflake/ml/feature_store/feature_store.py +55 -0
- snowflake/ml/feature_store/tile_sql_generator.py +40 -31
- snowflake/ml/jobs/job.py +19 -3
- snowflake/ml/model/_client/model/batch_inference_job_specs.py +81 -1
- snowflake/ml/model/_client/model/batch_inference_task.py +24 -6
- snowflake/ml/model/_client/model/model_version_impl.py +97 -5
- snowflake/ml/model/_client/ops/model_ops.py +253 -6
- snowflake/ml/model/_client/ops/service_ops.py +5 -4
- snowflake/ml/model/_client/service/inference_job_service_spec.py +1 -1
- snowflake/ml/model/_client/sql/model.py +11 -3
- snowflake/ml/model/_signatures/core.py +9 -4
- snowflake/ml/model/model_signature.py +85 -0
- snowflake/ml/modeling/_internal/model_specifications.py +10 -4
- snowflake/ml/modeling/_internal/snowpark_implementations/distributed_hpo_trainer.py +0 -2
- snowflake/ml/modeling/calibration/calibrated_classifier_cv.py +2 -1
- snowflake/ml/modeling/cluster/affinity_propagation.py +2 -1
- snowflake/ml/modeling/cluster/agglomerative_clustering.py +2 -1
- snowflake/ml/modeling/cluster/birch.py +2 -1
- snowflake/ml/modeling/cluster/bisecting_k_means.py +2 -1
- snowflake/ml/modeling/cluster/dbscan.py +2 -1
- snowflake/ml/modeling/cluster/feature_agglomeration.py +2 -1
- snowflake/ml/modeling/cluster/k_means.py +2 -1
- snowflake/ml/modeling/cluster/mean_shift.py +2 -1
- snowflake/ml/modeling/cluster/mini_batch_k_means.py +2 -1
- snowflake/ml/modeling/cluster/optics.py +2 -1
- snowflake/ml/modeling/cluster/spectral_biclustering.py +2 -1
- snowflake/ml/modeling/cluster/spectral_clustering.py +2 -1
- snowflake/ml/modeling/cluster/spectral_coclustering.py +2 -1
- snowflake/ml/modeling/compose/column_transformer.py +2 -1
- snowflake/ml/modeling/compose/transformed_target_regressor.py +2 -1
- snowflake/ml/modeling/covariance/elliptic_envelope.py +2 -1
- snowflake/ml/modeling/covariance/empirical_covariance.py +2 -1
- snowflake/ml/modeling/covariance/graphical_lasso.py +2 -1
- snowflake/ml/modeling/covariance/graphical_lasso_cv.py +2 -1
- snowflake/ml/modeling/covariance/ledoit_wolf.py +2 -1
- snowflake/ml/modeling/covariance/min_cov_det.py +2 -1
- snowflake/ml/modeling/covariance/oas.py +2 -1
- snowflake/ml/modeling/covariance/shrunk_covariance.py +2 -1
- snowflake/ml/modeling/decomposition/dictionary_learning.py +2 -1
- snowflake/ml/modeling/decomposition/factor_analysis.py +2 -1
- snowflake/ml/modeling/decomposition/fast_ica.py +2 -1
- snowflake/ml/modeling/decomposition/incremental_pca.py +2 -1
- snowflake/ml/modeling/decomposition/kernel_pca.py +2 -1
- snowflake/ml/modeling/decomposition/mini_batch_dictionary_learning.py +2 -1
- snowflake/ml/modeling/decomposition/mini_batch_sparse_pca.py +2 -1
- snowflake/ml/modeling/decomposition/pca.py +2 -1
- snowflake/ml/modeling/decomposition/sparse_pca.py +2 -1
- snowflake/ml/modeling/decomposition/truncated_svd.py +2 -1
- snowflake/ml/modeling/discriminant_analysis/linear_discriminant_analysis.py +2 -1
- snowflake/ml/modeling/discriminant_analysis/quadratic_discriminant_analysis.py +2 -1
- snowflake/ml/modeling/ensemble/ada_boost_classifier.py +2 -1
- snowflake/ml/modeling/ensemble/ada_boost_regressor.py +2 -1
- snowflake/ml/modeling/ensemble/bagging_classifier.py +2 -1
- snowflake/ml/modeling/ensemble/bagging_regressor.py +2 -1
- snowflake/ml/modeling/ensemble/extra_trees_classifier.py +2 -1
- snowflake/ml/modeling/ensemble/extra_trees_regressor.py +2 -1
- snowflake/ml/modeling/ensemble/gradient_boosting_classifier.py +2 -1
- snowflake/ml/modeling/ensemble/gradient_boosting_regressor.py +2 -1
- snowflake/ml/modeling/ensemble/hist_gradient_boosting_classifier.py +2 -1
- snowflake/ml/modeling/ensemble/hist_gradient_boosting_regressor.py +2 -1
- snowflake/ml/modeling/ensemble/isolation_forest.py +2 -1
- snowflake/ml/modeling/ensemble/random_forest_classifier.py +2 -1
- snowflake/ml/modeling/ensemble/random_forest_regressor.py +2 -1
- snowflake/ml/modeling/ensemble/stacking_regressor.py +2 -1
- snowflake/ml/modeling/ensemble/voting_classifier.py +2 -1
- snowflake/ml/modeling/ensemble/voting_regressor.py +2 -1
- snowflake/ml/modeling/feature_selection/generic_univariate_select.py +2 -1
- snowflake/ml/modeling/feature_selection/select_fdr.py +2 -1
- snowflake/ml/modeling/feature_selection/select_fpr.py +2 -1
- snowflake/ml/modeling/feature_selection/select_fwe.py +2 -1
- snowflake/ml/modeling/feature_selection/select_k_best.py +2 -1
- snowflake/ml/modeling/feature_selection/select_percentile.py +2 -1
- snowflake/ml/modeling/feature_selection/sequential_feature_selector.py +2 -1
- snowflake/ml/modeling/feature_selection/variance_threshold.py +2 -1
- snowflake/ml/modeling/gaussian_process/gaussian_process_classifier.py +2 -1
- snowflake/ml/modeling/gaussian_process/gaussian_process_regressor.py +2 -1
- snowflake/ml/modeling/impute/iterative_imputer.py +2 -1
- snowflake/ml/modeling/impute/knn_imputer.py +2 -1
- snowflake/ml/modeling/impute/missing_indicator.py +2 -1
- snowflake/ml/modeling/kernel_approximation/additive_chi2_sampler.py +2 -1
- snowflake/ml/modeling/kernel_approximation/nystroem.py +2 -1
- snowflake/ml/modeling/kernel_approximation/polynomial_count_sketch.py +2 -1
- snowflake/ml/modeling/kernel_approximation/rbf_sampler.py +2 -1
- snowflake/ml/modeling/kernel_approximation/skewed_chi2_sampler.py +2 -1
- snowflake/ml/modeling/kernel_ridge/kernel_ridge.py +2 -1
- snowflake/ml/modeling/lightgbm/lgbm_classifier.py +2 -1
- snowflake/ml/modeling/lightgbm/lgbm_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/ard_regression.py +2 -1
- snowflake/ml/modeling/linear_model/bayesian_ridge.py +2 -1
- snowflake/ml/modeling/linear_model/elastic_net.py +2 -1
- snowflake/ml/modeling/linear_model/elastic_net_cv.py +2 -1
- snowflake/ml/modeling/linear_model/gamma_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/huber_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/lars.py +2 -1
- snowflake/ml/modeling/linear_model/lars_cv.py +2 -1
- snowflake/ml/modeling/linear_model/lasso.py +2 -1
- snowflake/ml/modeling/linear_model/lasso_cv.py +2 -1
- snowflake/ml/modeling/linear_model/lasso_lars.py +2 -1
- snowflake/ml/modeling/linear_model/lasso_lars_cv.py +2 -1
- snowflake/ml/modeling/linear_model/lasso_lars_ic.py +2 -1
- snowflake/ml/modeling/linear_model/linear_regression.py +2 -1
- snowflake/ml/modeling/linear_model/logistic_regression.py +2 -1
- snowflake/ml/modeling/linear_model/logistic_regression_cv.py +2 -1
- snowflake/ml/modeling/linear_model/multi_task_elastic_net.py +2 -1
- snowflake/ml/modeling/linear_model/multi_task_elastic_net_cv.py +2 -1
- snowflake/ml/modeling/linear_model/multi_task_lasso.py +2 -1
- snowflake/ml/modeling/linear_model/multi_task_lasso_cv.py +2 -1
- snowflake/ml/modeling/linear_model/orthogonal_matching_pursuit.py +2 -1
- snowflake/ml/modeling/linear_model/passive_aggressive_classifier.py +2 -1
- snowflake/ml/modeling/linear_model/passive_aggressive_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/perceptron.py +2 -1
- snowflake/ml/modeling/linear_model/poisson_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/ransac_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/ridge.py +2 -1
- snowflake/ml/modeling/linear_model/ridge_classifier.py +2 -1
- snowflake/ml/modeling/linear_model/ridge_classifier_cv.py +2 -1
- snowflake/ml/modeling/linear_model/ridge_cv.py +2 -1
- snowflake/ml/modeling/linear_model/sgd_classifier.py +2 -1
- snowflake/ml/modeling/linear_model/sgd_one_class_svm.py +2 -1
- snowflake/ml/modeling/linear_model/sgd_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/theil_sen_regressor.py +2 -1
- snowflake/ml/modeling/linear_model/tweedie_regressor.py +2 -1
- snowflake/ml/modeling/manifold/isomap.py +2 -1
- snowflake/ml/modeling/manifold/mds.py +2 -1
- snowflake/ml/modeling/manifold/spectral_embedding.py +2 -1
- snowflake/ml/modeling/manifold/tsne.py +2 -1
- snowflake/ml/modeling/metrics/classification.py +1 -0
- snowflake/ml/modeling/metrics/metrics_utils.py +6 -0
- snowflake/ml/modeling/metrics/ranking.py +3 -0
- snowflake/ml/modeling/metrics/regression.py +3 -0
- snowflake/ml/modeling/mixture/bayesian_gaussian_mixture.py +2 -1
- snowflake/ml/modeling/mixture/gaussian_mixture.py +2 -1
- snowflake/ml/modeling/model_selection/grid_search_cv.py +2 -0
- snowflake/ml/modeling/model_selection/randomized_search_cv.py +2 -0
- snowflake/ml/modeling/multiclass/one_vs_one_classifier.py +2 -1
- snowflake/ml/modeling/multiclass/one_vs_rest_classifier.py +2 -1
- snowflake/ml/modeling/multiclass/output_code_classifier.py +2 -1
- snowflake/ml/modeling/naive_bayes/bernoulli_nb.py +2 -1
- snowflake/ml/modeling/naive_bayes/categorical_nb.py +2 -1
- snowflake/ml/modeling/naive_bayes/complement_nb.py +2 -1
- snowflake/ml/modeling/naive_bayes/gaussian_nb.py +2 -1
- snowflake/ml/modeling/naive_bayes/multinomial_nb.py +2 -1
- snowflake/ml/modeling/neighbors/k_neighbors_classifier.py +2 -1
- snowflake/ml/modeling/neighbors/k_neighbors_regressor.py +2 -1
- snowflake/ml/modeling/neighbors/kernel_density.py +2 -1
- snowflake/ml/modeling/neighbors/local_outlier_factor.py +2 -1
- snowflake/ml/modeling/neighbors/nearest_centroid.py +2 -1
- snowflake/ml/modeling/neighbors/nearest_neighbors.py +2 -1
- snowflake/ml/modeling/neighbors/neighborhood_components_analysis.py +2 -1
- snowflake/ml/modeling/neighbors/radius_neighbors_classifier.py +2 -1
- snowflake/ml/modeling/neighbors/radius_neighbors_regressor.py +2 -1
- snowflake/ml/modeling/neural_network/bernoulli_rbm.py +2 -1
- snowflake/ml/modeling/neural_network/mlp_classifier.py +2 -1
- snowflake/ml/modeling/neural_network/mlp_regressor.py +2 -1
- snowflake/ml/modeling/pipeline/pipeline.py +6 -1
- snowflake/ml/modeling/preprocessing/polynomial_features.py +2 -1
- snowflake/ml/modeling/semi_supervised/label_propagation.py +2 -1
- snowflake/ml/modeling/semi_supervised/label_spreading.py +2 -1
- snowflake/ml/modeling/svm/linear_svc.py +2 -1
- snowflake/ml/modeling/svm/linear_svr.py +2 -1
- snowflake/ml/modeling/svm/nu_svc.py +2 -1
- snowflake/ml/modeling/svm/nu_svr.py +2 -1
- snowflake/ml/modeling/svm/svc.py +2 -1
- snowflake/ml/modeling/svm/svr.py +2 -1
- snowflake/ml/modeling/tree/decision_tree_classifier.py +2 -1
- snowflake/ml/modeling/tree/decision_tree_regressor.py +2 -1
- snowflake/ml/modeling/tree/extra_tree_classifier.py +2 -1
- snowflake/ml/modeling/tree/extra_tree_regressor.py +2 -1
- snowflake/ml/modeling/xgboost/xgb_classifier.py +2 -1
- snowflake/ml/modeling/xgboost/xgb_regressor.py +2 -1
- snowflake/ml/modeling/xgboost/xgbrf_classifier.py +2 -1
- snowflake/ml/modeling/xgboost/xgbrf_regressor.py +2 -1
- snowflake/ml/registry/_manager/model_manager.py +78 -0
- snowflake/ml/registry/_manager/model_parameter_reconciler.py +7 -7
- snowflake/ml/version.py +1 -1
- {snowflake_ml_python-2.2.0.dist-info → snowflake_ml_python-2.4.0.dist-info}/METADATA +71 -13
- {snowflake_ml_python-2.2.0.dist-info → snowflake_ml_python-2.4.0.dist-info}/RECORD +192 -191
- {snowflake_ml_python-2.2.0.dist-info → snowflake_ml_python-2.4.0.dist-info}/WHEEL +0 -0
- {snowflake_ml_python-2.2.0.dist-info → snowflake_ml_python-2.4.0.dist-info}/licenses/LICENSE.txt +0 -0
- {snowflake_ml_python-2.2.0.dist-info → snowflake_ml_python-2.4.0.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
import functools
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
import sys
|
|
5
6
|
import traceback
|
|
6
|
-
import warnings
|
|
7
7
|
from collections.abc import Callable
|
|
8
8
|
from types import TracebackType
|
|
9
9
|
from typing import Any
|
|
@@ -32,6 +32,8 @@ from snowflake.snowpark import functions
|
|
|
32
32
|
|
|
33
33
|
DEFAULT_EXPERIMENT_NAME = sql_identifier.SqlIdentifier("DEFAULT")
|
|
34
34
|
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
35
37
|
|
|
36
38
|
class ExperimentTracking:
|
|
37
39
|
"""
|
|
@@ -52,6 +54,8 @@ class ExperimentTracking:
|
|
|
52
54
|
database_name: str | None = None,
|
|
53
55
|
schema_name: str | None = None,
|
|
54
56
|
capture_source_info: bool = True,
|
|
57
|
+
model_registry: registry.Registry | None = None,
|
|
58
|
+
warnings_as_errors: bool = False,
|
|
55
59
|
) -> None:
|
|
56
60
|
"""
|
|
57
61
|
Initializes experiment tracking within a pre-created schema.
|
|
@@ -68,20 +72,27 @@ class ExperimentTracking:
|
|
|
68
72
|
of the enclosing ML job when running inside one. Set to False to skip collection
|
|
69
73
|
entirely. Collection is always non-fatal and never blocks run creation. Defaults to
|
|
70
74
|
True.
|
|
75
|
+
model_registry: An optional Registry instance to use for ``log_model``. When provided,
|
|
76
|
+
models are logged to the database and schema owned by this registry instead of the
|
|
77
|
+
experiment's database and schema. This is useful when the experiment lives in one
|
|
78
|
+
schema (e.g. ``analytics.experiments``) but models should be stored in another
|
|
79
|
+
(e.g. ``ml.models``). If None, models are logged to the same location as the
|
|
80
|
+
experiment. Defaults to None.
|
|
81
|
+
warnings_as_errors: If True, conditions that are normally reported as warnings are raised
|
|
82
|
+
instead.
|
|
71
83
|
|
|
72
84
|
Raises:
|
|
73
85
|
ValueError: If no database is provided and no active database exists in the session.
|
|
74
86
|
"""
|
|
75
87
|
# Applied on every construction (even when reusing the singleton)
|
|
76
88
|
self._capture_source_info = capture_source_info
|
|
89
|
+
self._warnings_as_errors = warnings_as_errors
|
|
77
90
|
|
|
78
91
|
if hasattr(self, "_initialized"):
|
|
79
|
-
|
|
92
|
+
logger.info(
|
|
80
93
|
"ExperimentTracking is a singleton class. Reusing the existing instance, which has the setting:\n"
|
|
81
94
|
f" Database: {self._database_name}, Schema: {self._schema_name}\n"
|
|
82
|
-
"To change the database or schema, use the database_name and schema_name arguments to set_experiment."
|
|
83
|
-
UserWarning,
|
|
84
|
-
stacklevel=2,
|
|
95
|
+
"To change the database or schema, use the database_name and schema_name arguments to set_experiment."
|
|
85
96
|
)
|
|
86
97
|
return
|
|
87
98
|
|
|
@@ -109,11 +120,7 @@ class ExperimentTracking:
|
|
|
109
120
|
database_name=self._database_name,
|
|
110
121
|
schema_name=self._schema_name,
|
|
111
122
|
)
|
|
112
|
-
self._registry =
|
|
113
|
-
session=session,
|
|
114
|
-
database_name=self._database_name,
|
|
115
|
-
schema_name=self._schema_name,
|
|
116
|
-
)
|
|
123
|
+
self._registry = model_registry
|
|
117
124
|
self._session = session
|
|
118
125
|
|
|
119
126
|
# The experiment in context
|
|
@@ -139,7 +146,10 @@ class ExperimentTracking:
|
|
|
139
146
|
else:
|
|
140
147
|
state = dict(self.__dict__)
|
|
141
148
|
|
|
142
|
-
# Remove unpicklable attributes
|
|
149
|
+
# Remove unpicklable attributes; save registry coordinates for reconstruction.
|
|
150
|
+
if self._registry is not None:
|
|
151
|
+
state["_registry_database_name"] = self._registry._database_name
|
|
152
|
+
state["_registry_schema_name"] = self._registry._schema_name
|
|
143
153
|
state["_session"] = None
|
|
144
154
|
state["_sql_client"] = None
|
|
145
155
|
state["_registry"] = None
|
|
@@ -168,11 +178,14 @@ class ExperimentTracking:
|
|
|
168
178
|
database_name=self._database_name,
|
|
169
179
|
schema_name=self._schema_name,
|
|
170
180
|
)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
181
|
+
registry_db = state.pop("_registry_database_name", None)
|
|
182
|
+
registry_schema = state.pop("_registry_schema_name", None)
|
|
183
|
+
if registry_db is not None:
|
|
184
|
+
self._registry = registry.Registry(
|
|
185
|
+
session=self._session,
|
|
186
|
+
database_name=registry_db,
|
|
187
|
+
schema_name=registry_schema,
|
|
188
|
+
)
|
|
176
189
|
if self._run:
|
|
177
190
|
self._patch_stdout_and_stderr()
|
|
178
191
|
# _patch_stdout_and_stderr sets _logging_context if live logging is enabled
|
|
@@ -277,8 +290,17 @@ class ExperimentTracking:
|
|
|
277
290
|
**kwargs: Any,
|
|
278
291
|
) -> ml_model.ModelVersion:
|
|
279
292
|
run = self._get_or_start_run()
|
|
293
|
+
target_registry = (
|
|
294
|
+
self._registry
|
|
295
|
+
if self._registry is not None
|
|
296
|
+
else registry.Registry(
|
|
297
|
+
session=self._session,
|
|
298
|
+
database_name=self._database_name,
|
|
299
|
+
schema_name=self._schema_name,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
280
302
|
with experiment_info.ExperimentInfoPatcher(experiment_info=run._get_experiment_info()):
|
|
281
|
-
return
|
|
303
|
+
return target_registry.log_model(model, model_name=model_name, **kwargs)
|
|
282
304
|
|
|
283
305
|
def list_model_versions(self, run_name: str | None = None) -> list[ml_model.ModelVersion]:
|
|
284
306
|
"""
|
|
@@ -475,7 +497,7 @@ class ExperimentTracking:
|
|
|
475
497
|
)
|
|
476
498
|
except snowpark.exceptions.SnowparkSQLException as e:
|
|
477
499
|
if e.sql_error_code == 400003: # EXPERIMENT_RUN_PROPERTY_SIZE_LIMIT_EXCEEDED
|
|
478
|
-
run.
|
|
500
|
+
run._handle_run_metadata_size_limit(e.message)
|
|
479
501
|
else:
|
|
480
502
|
raise
|
|
481
503
|
|
|
@@ -520,7 +542,7 @@ class ExperimentTracking:
|
|
|
520
542
|
)
|
|
521
543
|
except snowpark.exceptions.SnowparkSQLException as e:
|
|
522
544
|
if e.sql_error_code == 400003: # EXPERIMENT_RUN_PROPERTY_SIZE_LIMIT_EXCEEDED
|
|
523
|
-
run.
|
|
545
|
+
run._handle_run_metadata_size_limit(e.message)
|
|
524
546
|
else:
|
|
525
547
|
raise
|
|
526
548
|
|
|
@@ -784,11 +806,6 @@ class ExperimentTracking:
|
|
|
784
806
|
database_name=database_name,
|
|
785
807
|
schema_name=schema_name,
|
|
786
808
|
)
|
|
787
|
-
self._registry = registry.Registry(
|
|
788
|
-
session=self._session,
|
|
789
|
-
database_name=database_name,
|
|
790
|
-
schema_name=schema_name,
|
|
791
|
-
)
|
|
792
809
|
|
|
793
810
|
def _print_urls(
|
|
794
811
|
self,
|
|
@@ -1770,6 +1770,8 @@ class FeatureStore:
|
|
|
1770
1770
|
error_code=error_codes.INVALID_ARGUMENT, original_exception=e
|
|
1771
1771
|
) from e
|
|
1772
1772
|
|
|
1773
|
+
self._validate_online_target_lag_change(feature_view, online_config)
|
|
1774
|
+
|
|
1773
1775
|
if updated_feature_df is not None:
|
|
1774
1776
|
if not feature_view.append_only:
|
|
1775
1777
|
raise snowml_exceptions.SnowflakeMLException(
|
|
@@ -4543,6 +4545,59 @@ class FeatureStore:
|
|
|
4543
4545
|
rollback_operations: list[tuple[str, str | FeatureView]]
|
|
4544
4546
|
final_config: fv_mod.OnlineConfig | None
|
|
4545
4547
|
|
|
4548
|
+
@staticmethod
|
|
4549
|
+
def _validate_online_target_lag_change(feature_view: FeatureView, online_config: fv_mod.OnlineConfig) -> None:
|
|
4550
|
+
"""Reject a target_lag change on an online feature table backed by Postgres.
|
|
4551
|
+
|
|
4552
|
+
Postgres-backed online feature tables accept the target lag they are created with but
|
|
4553
|
+
not a later change to it: the ALTER is acknowledged without being applied. Rejecting the
|
|
4554
|
+
change here keeps the call from reporting a success that did not happen.
|
|
4555
|
+
|
|
4556
|
+
Only a change that would otherwise be attempted is rejected. Enabling online storage
|
|
4557
|
+
creates the table with the requested lag, disabling it ignores the lag, and a lag equal
|
|
4558
|
+
to the current one is already a no-op.
|
|
4559
|
+
|
|
4560
|
+
Args:
|
|
4561
|
+
feature_view: The registered feature view being updated.
|
|
4562
|
+
online_config: The caller-supplied target online configuration.
|
|
4563
|
+
|
|
4564
|
+
Raises:
|
|
4565
|
+
SnowflakeMLException: [ValueError] The feature view's online store is Postgres and
|
|
4566
|
+
the requested target_lag differs from the current one.
|
|
4567
|
+
"""
|
|
4568
|
+
existing_config = feature_view.online_config
|
|
4569
|
+
if online_config.target_lag is None or online_config.enable is False:
|
|
4570
|
+
return
|
|
4571
|
+
if not feature_view.online or existing_config is None:
|
|
4572
|
+
return
|
|
4573
|
+
if existing_config.store_type != OnlineStoreType.POSTGRES:
|
|
4574
|
+
return
|
|
4575
|
+
deployed_lag = existing_config.target_lag
|
|
4576
|
+
if deployed_lag is None:
|
|
4577
|
+
return
|
|
4578
|
+
|
|
4579
|
+
# Neither lag is canonical: the table may report "300 SECONDS" where the caller asked
|
|
4580
|
+
# for "5 minutes", so an unchanged lag is only recognizable on seconds. An unreadable
|
|
4581
|
+
# lag lets the call through, because OnlineConfig accepts any non-empty string and
|
|
4582
|
+
# reporting malformed intervals is not this check's job.
|
|
4583
|
+
try:
|
|
4584
|
+
if interval_utils.interval_to_seconds(online_config.target_lag) == interval_utils.interval_to_seconds(
|
|
4585
|
+
deployed_lag
|
|
4586
|
+
):
|
|
4587
|
+
return
|
|
4588
|
+
except ValueError:
|
|
4589
|
+
return
|
|
4590
|
+
|
|
4591
|
+
raise snowml_exceptions.SnowflakeMLException(
|
|
4592
|
+
error_code=error_codes.INVALID_ARGUMENT,
|
|
4593
|
+
original_exception=ValueError(
|
|
4594
|
+
f"target_lag cannot be changed for a feature view whose online store is "
|
|
4595
|
+
f"{OnlineStoreType.POSTGRES.name}; it is fixed at "
|
|
4596
|
+
f"'{existing_config.target_lag}'. Delete and re-register the feature view with "
|
|
4597
|
+
f"the desired target_lag."
|
|
4598
|
+
),
|
|
4599
|
+
)
|
|
4600
|
+
|
|
4546
4601
|
def _plan_online_update(
|
|
4547
4602
|
self, feature_view: FeatureView, online_config: fv_mod.OnlineConfig | None
|
|
4548
4603
|
) -> _OnlineUpdateStrategy:
|
|
@@ -1143,6 +1143,34 @@ class MergingSqlGenerator:
|
|
|
1143
1143
|
|
|
1144
1144
|
return cte_name, cte_body.strip()
|
|
1145
1145
|
|
|
1146
|
+
def _left_join_subqueries_on_bounds(self, subqueries: list[tuple[str, list[str]]]) -> str:
|
|
1147
|
+
"""LEFT JOIN each per-subquery result onto the unique (entity, boundary) rows.
|
|
1148
|
+
|
|
1149
|
+
Driving from UNIQUE_BOUNDS rather than from the first subquery keeps row
|
|
1150
|
+
presence independent of any single window or feature: each subquery applies
|
|
1151
|
+
its own row-level filter, so one with no qualifying tiles must contribute
|
|
1152
|
+
NULLs for its own columns instead of dropping the entity from the CTE and
|
|
1153
|
+
taking every other subquery's results with it.
|
|
1154
|
+
|
|
1155
|
+
Args:
|
|
1156
|
+
subqueries: One (subquery_sql, output_columns) pair per window group or
|
|
1157
|
+
feature. Each subquery must be grouped by the join keys plus
|
|
1158
|
+
TILE_BOUNDARY so the joins stay one-to-one.
|
|
1159
|
+
|
|
1160
|
+
Returns:
|
|
1161
|
+
SQL string selecting the join keys plus every subquery's output columns.
|
|
1162
|
+
"""
|
|
1163
|
+
group_by_cols = list(self._join_keys) + [_TILE_BOUNDARY_COL]
|
|
1164
|
+
projected = [f"UB.{col}" for col in group_by_cols]
|
|
1165
|
+
for index, (_, output_cols) in enumerate(subqueries):
|
|
1166
|
+
projected.extend(f"sq{index}.{col}" for col in output_cols)
|
|
1167
|
+
|
|
1168
|
+
result = f"SELECT {', '.join(projected)}\n FROM UNIQUE_BOUNDS_FV{self._fv_index} UB"
|
|
1169
|
+
for index, (query, _) in enumerate(subqueries):
|
|
1170
|
+
join_cond = " AND ".join(f"UB.{col} = sq{index}.{col}" for col in group_by_cols)
|
|
1171
|
+
result += f"\n LEFT JOIN {query} sq{index}\n ON {join_cond}"
|
|
1172
|
+
return result
|
|
1173
|
+
|
|
1146
1174
|
def _generate_secondary_key_merged_cte(self) -> tuple[str, str]:
|
|
1147
1175
|
"""Generate the CTE for secondary-key aggregations.
|
|
1148
1176
|
|
|
@@ -1315,22 +1343,13 @@ class MergingSqlGenerator:
|
|
|
1315
1343
|
if len(subqueries) == 1:
|
|
1316
1344
|
cte_body = subqueries[0][2].strip()
|
|
1317
1345
|
else:
|
|
1318
|
-
# Multiple (sk, window) groups
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
projected.append(f"sq{i}.{keys_col}")
|
|
1326
|
-
for col in value_cols:
|
|
1327
|
-
projected.append(f"sq{i}.{col}")
|
|
1328
|
-
|
|
1329
|
-
result = f"SELECT {', '.join(projected)}\n FROM {first_query} sq0"
|
|
1330
|
-
for i, (_, _, query) in enumerate(subqueries[1:], 1):
|
|
1331
|
-
join_cond = " AND ".join(f"sq0.{col} = sq{i}.{col}" for col in group_by_cols)
|
|
1332
|
-
result += f"\n LEFT JOIN {query} sq{i}\n ON {join_cond}"
|
|
1333
|
-
cte_body = result
|
|
1346
|
+
# Multiple (sk, window) groups: a group whose window covers no tiles
|
|
1347
|
+
# emits no row, so the chain is driven from the unique (entity,
|
|
1348
|
+
# boundary) rows to keep each group's arrays independent. Missing
|
|
1349
|
+
# groups surface as [] via the combined CTE's COALESCE.
|
|
1350
|
+
cte_body = self._left_join_subqueries_on_bounds(
|
|
1351
|
+
[(query, [keys_col, *value_cols]) for keys_col, value_cols, query in subqueries]
|
|
1352
|
+
)
|
|
1334
1353
|
|
|
1335
1354
|
return cte_name, cte_body.strip()
|
|
1336
1355
|
|
|
@@ -1574,21 +1593,11 @@ class MergingSqlGenerator:
|
|
|
1574
1593
|
if len(feature_subqueries) == 1:
|
|
1575
1594
|
return feature_subqueries[0][1]
|
|
1576
1595
|
|
|
1577
|
-
# Multiple list features:
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
if i < len(feature_subqueries) - 1:
|
|
1583
|
-
result += ", "
|
|
1584
|
-
|
|
1585
|
-
result += f"\n FROM {first_query} sq0"
|
|
1586
|
-
|
|
1587
|
-
for i, (_name, query) in enumerate(feature_subqueries[1:], 1):
|
|
1588
|
-
join_cond = " AND ".join(f"sq0.{col} = sq{i}.{col}" for col in group_by_cols)
|
|
1589
|
-
result += f"\n LEFT JOIN {query} sq{i}\n ON {join_cond}"
|
|
1590
|
-
|
|
1591
|
-
return result
|
|
1596
|
+
# Multiple list features: a feature with no non-NULL values in the window
|
|
1597
|
+
# emits no row, so the chain is driven from the unique (entity, boundary)
|
|
1598
|
+
# rows to keep each feature's array independent of the others. A missing
|
|
1599
|
+
# feature stays NULL, which is this path's "no data" encoding.
|
|
1600
|
+
return self._left_join_subqueries_on_bounds([(query, [name]) for name, query in feature_subqueries])
|
|
1592
1601
|
|
|
1593
1602
|
def _generate_combined_cte(self) -> tuple[str, str]:
|
|
1594
1603
|
"""Generate the final CTE that combines and expands results.
|
snowflake/ml/jobs/job.py
CHANGED
|
@@ -362,13 +362,29 @@ class MLJob(Generic[T], SerializableSessionMixin):
|
|
|
362
362
|
"""
|
|
363
363
|
if self._result is None:
|
|
364
364
|
self.wait(timeout)
|
|
365
|
+
# Retrying would push the call past the caller's timeout, so only retry when unbounded.
|
|
366
|
+
remaining = _INSTANCE_RECORD_TIMEOUT_SECONDS if timeout < 0 else 0
|
|
365
367
|
try:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
368
|
+
while True:
|
|
369
|
+
try:
|
|
370
|
+
self._result = interop_utils.load(
|
|
371
|
+
self._result_path, session=self._session, path_transform=self._transform_path
|
|
372
|
+
)
|
|
373
|
+
break
|
|
374
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
375
|
+
if remaining <= 0:
|
|
376
|
+
raise
|
|
377
|
+
logger.warning(
|
|
378
|
+
"job result retrieval: result is not yet readable: %s; retrying in %d second(s)",
|
|
379
|
+
error,
|
|
380
|
+
_INSTANCE_RECORD_POLL_INTERVAL_SECONDS,
|
|
381
|
+
)
|
|
382
|
+
time.sleep(_INSTANCE_RECORD_POLL_INTERVAL_SECONDS)
|
|
383
|
+
remaining -= _INSTANCE_RECORD_POLL_INTERVAL_SECONDS
|
|
369
384
|
except Exception as e:
|
|
370
385
|
raise RuntimeError(f"Failed to retrieve result for job, error: {e!r}") from e
|
|
371
386
|
|
|
387
|
+
assert self._result is not None
|
|
372
388
|
return cast(T, self._result.get_value())
|
|
373
389
|
|
|
374
390
|
@telemetry.send_api_usage_telemetry(project=_PROJECT, func_params_to_log=["timeout"])
|
|
@@ -132,6 +132,20 @@ class EngineOptions(BaseModel):
|
|
|
132
132
|
return engine.name if engine is not None else None
|
|
133
133
|
|
|
134
134
|
|
|
135
|
+
class AdapterSpec(BaseModel):
|
|
136
|
+
"""Serialized adapter identity nested under ``inference.adapters``.
|
|
137
|
+
|
|
138
|
+
``alias`` is optional. Dict keys from the Python API set it; list items and
|
|
139
|
+
adapter-as-target omit it so the server can default to ``{name}/VERSIONS/{version}``.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
model_config = ConfigDict(extra="forbid")
|
|
143
|
+
|
|
144
|
+
name: str
|
|
145
|
+
version: str
|
|
146
|
+
alias: str | None = None
|
|
147
|
+
|
|
148
|
+
|
|
135
149
|
class InferenceSpec(BaseModel):
|
|
136
150
|
"""Inference block of the batch inference job specification.
|
|
137
151
|
|
|
@@ -141,13 +155,79 @@ class InferenceSpec(BaseModel):
|
|
|
141
155
|
max_batch_rows (Optional[int]): Maximum number of rows to process in a single batch.
|
|
142
156
|
Auto determined if None. Larger values may improve throughput.
|
|
143
157
|
engine_options (Optional[EngineOptions]): Options for a custom inference engine.
|
|
158
|
+
adapters (Optional[dict[str, ModelVersion] | list[ModelVersion]]): Adapter versions for
|
|
159
|
+
this job. A dict maps job alias (``input.params.model``) to adapter versions; keys
|
|
160
|
+
become ``alias`` on each emitted list item. A nonempty list and adapter-as-target omit
|
|
161
|
+
``alias``. Empty ``{}`` / ``[]`` are treated as unset so the YAML key is omitted.
|
|
162
|
+
``run_batch`` serializes these to name/version identities before writing the job spec.
|
|
144
163
|
"""
|
|
145
164
|
|
|
146
|
-
model_config = ConfigDict(extra="forbid")
|
|
165
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="forbid")
|
|
147
166
|
|
|
148
167
|
num_workers: int | None = None
|
|
149
168
|
max_batch_rows: int | None = None
|
|
150
169
|
engine_options: EngineOptions | None = None
|
|
170
|
+
adapters: dict[str, Any] | list[Any] | None = None
|
|
171
|
+
|
|
172
|
+
@field_validator("adapters", mode="before")
|
|
173
|
+
@classmethod
|
|
174
|
+
def _empty_adapters_to_none(cls, value: Any) -> Any:
|
|
175
|
+
if value == [] or value == {}:
|
|
176
|
+
return None
|
|
177
|
+
return value
|
|
178
|
+
|
|
179
|
+
@field_serializer("adapters")
|
|
180
|
+
def _serialize_adapters(self, value: Any) -> list[dict[str, Any]] | None:
|
|
181
|
+
if not value:
|
|
182
|
+
return None
|
|
183
|
+
if not isinstance(value, list) or not all(isinstance(item, AdapterSpec) for item in value):
|
|
184
|
+
raise ValueError(
|
|
185
|
+
"batch inference: adapter versions cannot be written into the job spec until they are "
|
|
186
|
+
"resolved to name and version."
|
|
187
|
+
)
|
|
188
|
+
return [item.model_dump(mode="json", exclude_none=True) for item in value]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def split_inference_adapters(
|
|
192
|
+
inference_spec: InferenceSpec | None,
|
|
193
|
+
) -> tuple[InferenceSpec | None, dict[str, Any] | list[Any] | None]:
|
|
194
|
+
"""Peel ``adapters`` off ``inference_spec`` without mutating the caller object.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
inference_spec: Existing inference block, or None.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
The spec with ``adapters`` cleared (or None if that leaves an empty spec), and the
|
|
201
|
+
peeled adapters value (or None when unset).
|
|
202
|
+
"""
|
|
203
|
+
if inference_spec is None or inference_spec.adapters is None:
|
|
204
|
+
return inference_spec, None
|
|
205
|
+
raw = inference_spec.adapters
|
|
206
|
+
stripped = inference_spec.model_copy(update={"adapters": None})
|
|
207
|
+
if not stripped.model_dump(exclude_none=True):
|
|
208
|
+
return None, raw
|
|
209
|
+
return stripped, raw
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def inference_spec_with_adapters(
|
|
213
|
+
inference_spec: InferenceSpec | None,
|
|
214
|
+
adapters: dict[str, Any] | list[Any] | None,
|
|
215
|
+
) -> InferenceSpec | None:
|
|
216
|
+
"""Return ``inference_spec`` with ``adapters`` set when nonempty.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
inference_spec: Existing inference block, or None.
|
|
220
|
+
adapters: Adapter versions or serialized identities. Empty or None leaves
|
|
221
|
+
``inference_spec`` unchanged.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
The original spec when ``adapters`` is empty, otherwise a spec that includes them.
|
|
225
|
+
"""
|
|
226
|
+
if not adapters:
|
|
227
|
+
return inference_spec
|
|
228
|
+
if inference_spec is None:
|
|
229
|
+
return InferenceSpec(adapters=adapters)
|
|
230
|
+
return inference_spec.model_copy(update={"adapters": adapters})
|
|
151
231
|
|
|
152
232
|
|
|
153
233
|
class ImageBuildSpec(BaseModel):
|
|
@@ -79,7 +79,13 @@ class BatchInferenceTask(DAGTask):
|
|
|
79
79
|
one of ``query`` or ``input_stage_location``.
|
|
80
80
|
input_spec: Optional input block.
|
|
81
81
|
resources_spec: Optional resources block.
|
|
82
|
-
inference_spec: Optional inference block.
|
|
82
|
+
inference_spec: Optional inference block. Optional ``adapters`` is a mapping from
|
|
83
|
+
job alias (``input.params.model``) to adapter ``ModelVersion``s, or a list of
|
|
84
|
+
adapter versions. Dict keys become ``alias`` on each emitted list item. A
|
|
85
|
+
nonempty list and adapter-as-target omit ``alias``. Omit, ``None``, ``{}``, or
|
|
86
|
+
``[]`` attaches none. Constructing the task on an adapter version is equivalent
|
|
87
|
+
to running the lineage pin with a one-item list, and defaults job-level
|
|
88
|
+
``params.model`` to the defaulted alias when unset.
|
|
83
89
|
image_build_spec: Optional image build block.
|
|
84
90
|
function_name: Model function name. Resolved against the model's function list
|
|
85
91
|
when omitted.
|
|
@@ -125,24 +131,36 @@ class BatchInferenceTask(DAGTask):
|
|
|
125
131
|
if (self._query is None) == (self._input_stage_location is None):
|
|
126
132
|
raise ValueError("Exactly one of query or input_stage_location must be provided.")
|
|
127
133
|
|
|
128
|
-
|
|
134
|
+
inference_spec, raw_adapters = batch_inference_job_specs.split_inference_adapters(self._inference_spec)
|
|
135
|
+
deploy_target, adapters_map, input_spec = self._model_version._prepare_lora_batch(
|
|
136
|
+
adapters=raw_adapters,
|
|
129
137
|
input_spec=self._input_spec,
|
|
138
|
+
input_spec_cls=batch_inference_job_specs.InputSpec,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
target_function_info = deploy_target._validate_batch_inference_request(
|
|
142
|
+
input_spec=input_spec,
|
|
130
143
|
resources_spec=self._resources_spec,
|
|
131
144
|
function_name=self._function_name,
|
|
132
145
|
)
|
|
133
146
|
|
|
147
|
+
inference_spec = batch_inference_job_specs.inference_spec_with_adapters(
|
|
148
|
+
inference_spec,
|
|
149
|
+
deploy_target._serialize_batch_adapters(adapters_map) if adapters_map else None,
|
|
150
|
+
)
|
|
151
|
+
|
|
134
152
|
return service_ops.build_batch_inference_task_definition(
|
|
135
153
|
session=self._model_version._service_ops._session,
|
|
136
|
-
model_fqn=
|
|
137
|
-
version_name=
|
|
154
|
+
model_fqn=deploy_target.fully_qualified_model_name,
|
|
155
|
+
version_name=deploy_target.version_name,
|
|
138
156
|
compute_pool=self._compute_pool,
|
|
139
157
|
function_name=target_function_info["target_method"],
|
|
140
158
|
query=self._query,
|
|
141
159
|
input_stage_location=self._input_stage_location,
|
|
142
|
-
input_spec=
|
|
160
|
+
input_spec=input_spec,
|
|
143
161
|
output_spec=self._output_spec,
|
|
144
162
|
resources_spec=self._resources_spec,
|
|
145
|
-
inference_spec=
|
|
163
|
+
inference_spec=inference_spec,
|
|
146
164
|
image_build_spec=self._image_build_spec,
|
|
147
165
|
replicas=self._replicas,
|
|
148
166
|
)
|
|
@@ -688,7 +688,14 @@ class ModelVersion(lineage_node.LineageNode):
|
|
|
688
688
|
``<stage_location>/<job_name>/``.
|
|
689
689
|
input_spec: Input block.
|
|
690
690
|
resources_spec: Resources block.
|
|
691
|
-
inference_spec: Inference block.
|
|
691
|
+
inference_spec: Inference block. Optional ``adapters`` is a mapping from job alias
|
|
692
|
+
(``input.params.model``) to adapter ``ModelVersion``s, or a list of adapter
|
|
693
|
+
versions. Dict keys become ``alias`` on each emitted list item. A nonempty list
|
|
694
|
+
and adapter-as-target omit ``alias`` (the server defaults each to
|
|
695
|
+
``<FQN>/VERSIONS/<version>``). Omit, ``None``, ``{}``, or ``[]`` attaches none.
|
|
696
|
+
Calling ``run_batch`` on an adapter version is equivalent to running the lineage
|
|
697
|
+
pin with a one-item list, and defaults job-level ``params.model`` to that
|
|
698
|
+
defaulted alias when unset.
|
|
692
699
|
image_build_spec: Image build block.
|
|
693
700
|
function_name: Model function name. Resolved against the model's
|
|
694
701
|
function list when omitted.
|
|
@@ -763,6 +770,29 @@ class ModelVersion(lineage_node.LineageNode):
|
|
|
763
770
|
if (X is None) == (input_stage_location is None):
|
|
764
771
|
raise ValueError("Exactly one of X or input_stage_location must be provided.")
|
|
765
772
|
|
|
773
|
+
inference_spec, raw_adapters = batch_inference_job_specs.split_inference_adapters(inference_spec)
|
|
774
|
+
deploy_target, adapters_map, input_spec = self._prepare_lora_batch(
|
|
775
|
+
adapters=raw_adapters,
|
|
776
|
+
input_spec=input_spec,
|
|
777
|
+
input_spec_cls=batch_inference_job_specs.InputSpec,
|
|
778
|
+
)
|
|
779
|
+
if deploy_target is not self:
|
|
780
|
+
forwarded = [mv for _, mv in adapters_map] if adapters_map else None
|
|
781
|
+
return deploy_target.run_batch(
|
|
782
|
+
X,
|
|
783
|
+
input_stage_location=input_stage_location,
|
|
784
|
+
compute_pool=compute_pool,
|
|
785
|
+
output_spec=output_spec,
|
|
786
|
+
input_spec=input_spec,
|
|
787
|
+
resources_spec=resources_spec,
|
|
788
|
+
inference_spec=batch_inference_job_specs.inference_spec_with_adapters(inference_spec, forwarded),
|
|
789
|
+
image_build_spec=image_build_spec,
|
|
790
|
+
function_name=function_name,
|
|
791
|
+
job_name=job_name,
|
|
792
|
+
replicas=replicas,
|
|
793
|
+
async_=async_,
|
|
794
|
+
)
|
|
795
|
+
|
|
766
796
|
target_function_info = self._validate_batch_inference_request(
|
|
767
797
|
input_spec=input_spec,
|
|
768
798
|
resources_spec=resources_spec,
|
|
@@ -770,6 +800,11 @@ class ModelVersion(lineage_node.LineageNode):
|
|
|
770
800
|
statement_params=statement_params,
|
|
771
801
|
)
|
|
772
802
|
|
|
803
|
+
inference_spec = batch_inference_job_specs.inference_spec_with_adapters(
|
|
804
|
+
inference_spec,
|
|
805
|
+
self._serialize_batch_adapters(adapters_map) if adapters_map else None,
|
|
806
|
+
)
|
|
807
|
+
|
|
773
808
|
return self._service_ops.execute_inference_job_service(
|
|
774
809
|
X=X,
|
|
775
810
|
input_stage_location=input_stage_location,
|
|
@@ -1238,11 +1273,13 @@ class ModelVersion(lineage_node.LineageNode):
|
|
|
1238
1273
|
return caps(features={}).is_lora_adapters_enabled()
|
|
1239
1274
|
|
|
1240
1275
|
@staticmethod
|
|
1241
|
-
def _require_lora_adapters_enabled(
|
|
1276
|
+
def _require_lora_adapters_enabled(
|
|
1277
|
+
*,
|
|
1278
|
+
unavailable_what: str = "Creating a service with adapters",
|
|
1279
|
+
) -> None:
|
|
1242
1280
|
if not ModelVersion._lora_adapters_enabled():
|
|
1243
1281
|
ModelVersion._adapter_invalid_argument(
|
|
1244
|
-
"
|
|
1245
|
-
"for this account."
|
|
1282
|
+
f"{unavailable_what} is unavailable because ENABLE_LORA_ADAPTERS is not enabled " "for this account."
|
|
1246
1283
|
)
|
|
1247
1284
|
|
|
1248
1285
|
def _is_peft_adapter_version(self) -> bool:
|
|
@@ -1263,7 +1300,7 @@ class ModelVersion(lineage_node.LineageNode):
|
|
|
1263
1300
|
|
|
1264
1301
|
def _normalize_adapters(
|
|
1265
1302
|
self,
|
|
1266
|
-
adapters: dict[str,
|
|
1303
|
+
adapters: dict[str, Any] | list[Any] | None,
|
|
1267
1304
|
) -> list[tuple[str | None, "ModelVersion"]] | None:
|
|
1268
1305
|
if adapters is None:
|
|
1269
1306
|
return None
|
|
@@ -1300,6 +1337,61 @@ class ModelVersion(lineage_node.LineageNode):
|
|
|
1300
1337
|
for alias, adapter_mv in adapters
|
|
1301
1338
|
]
|
|
1302
1339
|
|
|
1340
|
+
@staticmethod
|
|
1341
|
+
def _serialize_batch_adapters(
|
|
1342
|
+
adapters: list[tuple[str | None, "ModelVersion"]],
|
|
1343
|
+
) -> list[batch_inference_job_specs.AdapterSpec]:
|
|
1344
|
+
return [
|
|
1345
|
+
batch_inference_job_specs.AdapterSpec(
|
|
1346
|
+
name=adapter_mv.fully_qualified_model_name,
|
|
1347
|
+
version=adapter_mv.version_name,
|
|
1348
|
+
alias=alias,
|
|
1349
|
+
)
|
|
1350
|
+
for alias, adapter_mv in adapters
|
|
1351
|
+
]
|
|
1352
|
+
|
|
1353
|
+
@staticmethod
|
|
1354
|
+
def _default_adapter_alias(mv: "ModelVersion") -> str:
|
|
1355
|
+
return f"{mv.fully_qualified_model_name}/VERSIONS/{mv.version_name}"
|
|
1356
|
+
|
|
1357
|
+
@staticmethod
|
|
1358
|
+
def _with_default_batch_params_model(
|
|
1359
|
+
input_spec: Any | None,
|
|
1360
|
+
*,
|
|
1361
|
+
alias: str,
|
|
1362
|
+
spec_cls: Any,
|
|
1363
|
+
) -> Any:
|
|
1364
|
+
if input_spec is not None and input_spec.params and input_spec.params.get("model") is not None:
|
|
1365
|
+
return input_spec
|
|
1366
|
+
params = dict(input_spec.params) if input_spec and input_spec.params else {}
|
|
1367
|
+
params["model"] = alias
|
|
1368
|
+
if input_spec is None:
|
|
1369
|
+
return spec_cls(params=params)
|
|
1370
|
+
return input_spec.model_copy(update={"params": params})
|
|
1371
|
+
|
|
1372
|
+
def _prepare_lora_batch(
|
|
1373
|
+
self,
|
|
1374
|
+
*,
|
|
1375
|
+
adapters: dict[str, Any] | list[Any] | None,
|
|
1376
|
+
input_spec: Any | None,
|
|
1377
|
+
input_spec_cls: Any,
|
|
1378
|
+
) -> tuple["ModelVersion", list[tuple[str | None, "ModelVersion"]] | None, Any]:
|
|
1379
|
+
adapters_items = self._normalize_adapters(adapters)
|
|
1380
|
+
if adapters_items:
|
|
1381
|
+
self._require_lora_adapters_enabled(unavailable_what="Running a batch job with adapters")
|
|
1382
|
+
if self._is_peft_adapter_version():
|
|
1383
|
+
self._require_lora_adapters_enabled(unavailable_what="Running a batch job on a LoRA adapter")
|
|
1384
|
+
if adapters_items:
|
|
1385
|
+
self._adapter_invalid_argument(
|
|
1386
|
+
"batch inference: an adapter version does not accept adapters on the inference spec. "
|
|
1387
|
+
"The adapter is attached under its default FQN/VERSIONS alias."
|
|
1388
|
+
)
|
|
1389
|
+
pin = self._resolve_adapter_pin()
|
|
1390
|
+
alias = self._default_adapter_alias(self)
|
|
1391
|
+
input_spec = self._with_default_batch_params_model(input_spec, alias=alias, spec_cls=input_spec_cls)
|
|
1392
|
+
return (pin, [(None, self)], input_spec)
|
|
1393
|
+
return (self, adapters_items, input_spec)
|
|
1394
|
+
|
|
1303
1395
|
@overload
|
|
1304
1396
|
def create_service(
|
|
1305
1397
|
self,
|