Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,78 @@
# Release History

## 1.44.0
## 1.45.0

### New Features

* Feature Store: `FeatureView` now supports an `initialization_warehouse` that is used for the initial build and any
subsequent reinitializations of the backing dynamic table (a full scan of the source data), while `warehouse`
continues to drive the lighter incremental refreshes. This mirrors the dynamic table `INITIALIZATION_WAREHOUSE`
knob, lets you pair a larger warehouse for initialization with a smaller one for steady-state refresh, and is also
used for the one-time backfill of streaming feature views. It can be set at registration, changed via
`update_feature_view(initialization_warehouse=...)`, and is surfaced by `list_feature_views(verbose=True)`.

```python
draft_fv = FeatureView(
name="F_TRIP",
entities=[entity],
feature_df=feature_df,
refresh_freq="1d",
warehouse="SMALL_WH", # incremental refreshes
initialization_warehouse="LARGE_WH", # initial build / reinitialization
)
fv = fs.register_feature_view(draft_fv, version="1.0")
```

* Registry: LLM models deployed with the OpenAI chat signatures now support structured outputs
through an optional `response_format` param matching the OpenAI Chat Completions API
(`{"type": "json_schema", "json_schema": {"name": "...", "schema": {...}}}`), letting callers
constrain model output to a JSON Schema.

```python
from pydantic import BaseModel
import pandas as pd

class CityCountry(BaseModel):
city: str
country: str

response_format = {
"type": "json_schema",
"json_schema": {
"name": "city_country",
"schema": CityCountry.model_json_schema(),
},
}

x_df = pd.DataFrame.from_records(
[
{
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "What is the capital of France?"},
],
},
],
}
]
)

mv.run(
X=x_df,
params={"response_format": response_format},
service_name=...,
)
```

### Bug Fixes

### Behavior Changes

### Deprecations

## 1.44.0 (2026-06-23)

### New Features

Expand Down
1 change: 1 addition & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ use_repo(
"com_github_apache_arrow_go_v18",
"com_github_buger_jsonparser",
"com_github_caarlos0_env_v11",
"com_github_goccy_go_json",
"com_github_snowflakedb_gosnowflake",
"in_gopkg_natefinch_lumberjack_v2",
"in_gopkg_yaml_v2",
Expand Down
2 changes: 1 addition & 1 deletion bazel/environments/conda-optional-dependency-llm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ channels:
- https://repo.anaconda.com/pkgs/snowflake
- nodefaults
dependencies:
- mlflow>=2.16.0, <4
- mlflow>=2.16.0, <3
- pytorch>=2.0.1,<3
- sentence-transformers>=2.7.0,<6
- sentencepiece>=0.1.95,<0.3
Expand Down
2 changes: 1 addition & 1 deletion bazel/environments/conda-optional-dependency-ml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ dependencies:
- altair>=5,<7
- catboost>=1.2.0, <2
- lightgbm>=4.1.0, <5
- mlflow>=2.16.0, <4
- mlflow>=2.16.0, <3
- prophet>=1.1.0, <2
- streamlit>=1.30.0,<2
2 changes: 1 addition & 1 deletion bazel/environments/conda-optional-dependency-torch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ channels:
- https://repo.anaconda.com/pkgs/snowflake
- nodefaults
dependencies:
- mlflow>=2.16.0, <4
- mlflow>=2.16.0, <3
- pytorch>=2.0.1,<3
- sentence-transformers>=2.7.0,<6
- sentencepiece>=0.1.95,<0.3
Expand Down
4 changes: 2 additions & 2 deletions ci/conda_recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ build:
noarch: python
package:
name: snowflake-ml-python
version: 1.44.0
version: 1.45.0
requirements:
build:
- python
Expand Down Expand Up @@ -58,7 +58,7 @@ requirements:
- catboost>=1.2.0, <2
- keras>=2.0.0,<4
- lightgbm>=4.1.0, <5
- mlflow>=2.16.0, <4
- mlflow>=2.16.0, <3
- prophet>=1.1.0, <2
- pytorch>=2.0.1,<3
- sentence-transformers>=2.7.0,<6
Expand Down
1 change: 1 addition & 0 deletions codegen/sklearn_wrapper_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ def _is_deterministic(class_object: tuple[str, type]) -> bool:
WrapperGeneratorFactory._is_class_of_type(class_object[1], "LinearDiscriminantAnalysis")
or WrapperGeneratorFactory._is_class_of_type(class_object[1], "BernoulliRBM")
or WrapperGeneratorFactory._is_class_of_type(class_object[1], "TSNE")
or WrapperGeneratorFactory._is_class_of_type(class_object[1], "MDS")
)

@staticmethod
Expand Down
10 changes: 0 additions & 10 deletions codegen/transformer_autogen_test_template.py_template
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,6 @@ class {transform.test_class_name}(TestCase):
fit_with_sproc: bool = True,
inference_with_udf: bool = True
) -> None:
# sklearn _parallel_pairwise bug (https://github.com/scikit-learn/scikit-learn/issues/33877):
# when effective_n_jobs(None) > 1 (warehouse environment), Y is chunked but Y_norm_squared
# is passed through unchanged, causing a dimension mismatch in Birch._predict.
# Fixed in https://github.com/scikit-learn/scikit-learn/pull/33876 (sklearn 1.9.0).
# TODO(SNOW-952252): Re-enable when test env sklearn is bumped to >= 1.9.0.
if "{transform.original_class_name}" == "Birch" and (fit_with_sproc or inference_with_udf): # type: ignore[comparison-overlap,unused-ignore]
import joblib
if joblib.effective_n_jobs(None) > 1:
self.skipTest("sklearn _parallel_pairwise bug causes dimension mismatch when n_jobs > 1")

input_df_pandas = {transform.test_dataset_func}(as_frame=True).frame
cols = [inflection.parameterize(c, "_").upper() for c in input_df_pandas.columns if not c.startswith("target")]
cols_half_1, cols_half_2 = cols[:int(len(cols)/2)], cols[int(len(cols)/2)+1:]
Expand Down
2 changes: 1 addition & 1 deletion requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@
version_requirements: '>=0.46.0,<1'
- name: mlflow
dev_version: 2.16.2
version_requirements: '>=2.16.0, <4'
version_requirements: '>=2.16.0, <3'
requirements_extra_tags:
- mlflow
- llm
Expand Down
51 changes: 36 additions & 15 deletions snowflake/ml/_internal/file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,38 @@ def _resolve_stage_dir_path(
return str((stage_path / relative_path).parent)


def upload_file_to_stage(
session: snowpark.Session,
local_file_path: Union[str, pathlib.Path],
stage_dir_path: str,
*,
statement_params: Optional[dict[str, Any]] = None,
) -> None:
"""Upload a single local file to a stage directory.

Args:
session: Snowpark Session.
local_file_path: Local path of the file to upload.
stage_dir_path: Destination directory path in the stage.
statement_params: Statement Params.
"""
import retrying

file_operation = snowpark.FileOperation(session=session)
retrying.retry(
retry_on_exception=_retry_on_sql_error,
stop_max_attempt_number=5,
wait_exponential_multiplier=100,
wait_exponential_max=10000,
)(file_operation.put)(
str(local_file_path),
str(stage_dir_path),
auto_compress=False,
overwrite=False,
statement_params=statement_params,
)


def upload_directory_to_stage(
session: snowpark.Session,
local_path: pathlib.Path,
Expand All @@ -328,27 +360,16 @@ def upload_directory_to_stage(
stage_path: Base path in the stage.
statement_params: Statement Params.
"""
import retrying

file_operation = snowpark.FileOperation(session=session)

for root, _, filenames in os.walk(local_path):
root_path = pathlib.Path(root)
for filename in filenames:
local_file_path = root_path / filename
relative_path = pathlib.PurePosixPath(local_file_path.relative_to(local_path).as_posix())
stage_dir_path = _resolve_stage_dir_path(stage_path, relative_path)

retrying.retry(
retry_on_exception=_retry_on_sql_error,
stop_max_attempt_number=5,
wait_exponential_multiplier=100,
wait_exponential_max=10000,
)(file_operation.put)(
str(local_file_path),
str(stage_dir_path),
auto_compress=False,
overwrite=False,
upload_file_to_stage(
session,
local_file_path,
stage_dir_path,
statement_params=statement_params,
)

Expand Down
17 changes: 13 additions & 4 deletions snowflake/ml/experiment/_client/experiment_tracking_sql_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,20 @@ def drop_experiment(
).validate()

@telemetry.send_api_usage_telemetry(project=telemetry.TelemetryProject.EXPERIMENT_TRACKING.value)
def add_run(self, *, experiment_name: sql_identifier.SqlIdentifier, run_name: sql_identifier.SqlIdentifier) -> None:
def add_run(
self,
*,
experiment_name: sql_identifier.SqlIdentifier,
run_name: sql_identifier.SqlIdentifier,
source_info: Optional[str] = None,
) -> None:
experiment_fqn = self.fully_qualified_object_name(self._database_name, self._schema_name, experiment_name)
query_result_checker.SqlResultValidator(
self._session, f"ALTER EXPERIMENT {experiment_fqn} ADD RUN {run_name}"
).has_dimensions(expected_rows=1, expected_cols=1).validate()
query = f"ALTER EXPERIMENT {experiment_fqn} ADD RUN {run_name}"
if source_info:
query += f" WITH (SOURCE_INFO = $${source_info}$$)"
query_result_checker.SqlResultValidator(self._session, query).has_dimensions(
expected_rows=1, expected_cols=1
).validate()

@telemetry.send_api_usage_telemetry(project=telemetry.TelemetryProject.EXPERIMENT_TRACKING.value)
def commit_run(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,38 @@ def test_add_run(self) -> None:
)
self.client.add_run(experiment_name=experiment_name, run_name=run_name)

def test_add_run_with_source_info(self) -> None:
# The caller passes an already-serialized JSON string; the client embeds it verbatim.
experiment_name = sql_identifier.SqlIdentifier("TEST_EXPERIMENT")
run_name = sql_identifier.SqlIdentifier("TEST_RUN")

payload = '{"entry_point": "train/main.py", "git": {"commit_hash": "abc"}}'
self.m_session.add_mock_sql(
"ALTER EXPERIMENT TEST_DB.TEST_SCHEMA.TEST_EXPERIMENT ADD RUN TEST_RUN"
f" WITH (SOURCE_INFO = $${payload}$$)",
self._create_mock_df(),
)
self.client.add_run(experiment_name=experiment_name, run_name=run_name, source_info=payload)

def test_add_run_with_none_source_info_omits_clause(self) -> None:
experiment_name = sql_identifier.SqlIdentifier("TEST_EXPERIMENT")
run_name = sql_identifier.SqlIdentifier("TEST_RUN")

self.m_session.add_mock_sql(
"ALTER EXPERIMENT TEST_DB.TEST_SCHEMA.TEST_EXPERIMENT ADD RUN TEST_RUN", self._create_mock_df()
)
self.client.add_run(experiment_name=experiment_name, run_name=run_name, source_info=None)

def test_add_run_with_empty_source_info_omits_clause(self) -> None:
# An empty string is falsy and must be treated like "nothing to send".
experiment_name = sql_identifier.SqlIdentifier("TEST_EXPERIMENT")
run_name = sql_identifier.SqlIdentifier("TEST_RUN")

self.m_session.add_mock_sql(
"ALTER EXPERIMENT TEST_DB.TEST_SCHEMA.TEST_EXPERIMENT ADD RUN TEST_RUN", self._create_mock_df()
)
self.client.add_run(experiment_name=experiment_name, run_name=run_name, source_info="")

def test_commit_run(self) -> None:
# Test committing a run
experiment_name = sql_identifier.SqlIdentifier("TEST_EXPERIMENT")
Expand Down
12 changes: 12 additions & 0 deletions snowflake/ml/feature_store/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,18 @@ py_test(
],
)

py_test(
name = "feature_store_initialization_warehouse_test",
srcs = ["feature_store_initialization_warehouse_test.py"],
tags = [
"feature:feature_store",
"short_regress",
],
deps = [
":feature_store_lib",
],
)

py_test(
name = "realtime_config_test",
srcs = ["realtime_config_test.py"],
Expand Down
Empty file.
Loading