feat: Version-pinning for FeatureService (online + offline) - #6718
feat: Version-pinning for FeatureService (online + offline)#6718HSarwat wants to merge 8 commits into
Conversation
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #6718 +/- ##
==========================================
+ Coverage 46.92% 46.99% +0.07%
==========================================
Files 416 416
Lines 50549 50620 +71
Branches 7252 7270 +18
==========================================
+ Hits 23718 23789 +71
+ Misses 25157 25156 -1
- Partials 1674 1675 +1
... and 1 file with indirect coverage changes Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
|
@HSarwat Please fix CI |
bc41ab7 to
6083f35
Compare
|
@ntkathole This PR doesnt introduce any dependency change, yet the CI is failing because it cant resolve greenlet 3.5.5 . I am not really sure where the error is coming from. Rebased my PR with the latest changed from main in case this error was resolved in a later commit and to trigger the CI again |
A FeatureService built from a version-pinned FeatureView (e.g.
FeatureView(version="v2")) silently served the promoted version instead
of the pinned one, defeating the guarantee a FeatureService is meant to
provide. Two bugs caused this:
1. FeatureService.__init__ appended the source view's projection without
translating its `version` string into `projection.version_tag` (the
field name_to_use() checks to render "fv@v2").
2. utils._get_feature_views_to_use hard-coded the version as None for the
FeatureService branch, so retrieval always fell through to the
promoted snapshot.
Stamp version_tag from the source view's version in __init__, and read
projection.version_tag (instead of None) during retrieval. The default
version ("latest") leaves version_tag as None, so every existing
unversioned FeatureService is unaffected. Covers both online and offline
retrieval, which share _get_feature_views_to_use.
Signed-off-by: h-sarwat <hussein_sarwat@yahoo.com>
FeatureService.features now accepts string refs using the same '<feature_view>[@<version>][:<feature>]' syntax as get_historical_features/get_online_features, e.g. "driver_stats@v2" or "driver_stats@v2:trips_today". This lets a service pin a historical FeatureView version without importing or reconstructing the underlying object. String entries are stashed at construction (no registry is available then) and resolved once, in FeatureStore.apply/plan, via a new FeatureService.resolve_pending_refs. A pinned ref always resolves from the registry snapshot for that version; an unversioned ref prefers the apply batch, then the promoted version. utils._parse_feature_ref is refactored onto a new _parse_feature_or_view_ref that makes the ':<feature>' suffix optional (whole-view refs); _parse_feature_ref is unchanged for existing callers. Signed-off-by: h-sarwat <hussein_sarwat@yahoo.com>
Offline stores re-fetch OnDemandFeatureViews from the registry independently of the version-aware online path, using an unversioned registry.list_on_demand_feature_views. A version-pinned ODFV (e.g. from a FeatureService pinning a specific ODFV version) therefore either raised "Could not find feature view from reference odfv@v1:feat" or silently served the promoted version during get_historical_features. Add a shared utils._get_requested_on_demand_feature_views that resolves each ref version-aware — pinned refs via get_feature_view_by_version (stamping projection.version_tag so name_to_use() matches downstream), unversioned refs via the promoted list as before. Route the three unversioned call sites through it: OnDemandFeatureView.get_requested_odfvs, offline_utils.get_feature_view_query_context, and dask.py's inline duplicate. This covers every offline backend, which all funnel through one of those. Unversioned refs are unchanged. Signed-off-by: h-sarwat <hussein_sarwat@yahoo.com>
Document pinning a historical feature view version inside a
FeatureService, via both string feature refs
("driver_stats@v2:trips_today") and version-pinned FeatureView objects,
on the alpha feature-view-versioning page. Clarify that
enable_online_feature_view_versioning gates both online and offline
versioned resolution (they share one code path), both in the page and in
the RegistryConfig field docstring. Update Known Limitations: offline
version-qualified retrieval and version-pinned feature services are now
supported.
Signed-off-by: h-sarwat <hussein_sarwat@yahoo.com>
6083f35 to
573931b
Compare
HaoXuAI
left a comment
There was a problem hiding this comment.
String refs introduce an unresolved lifecycle state that retrieval and serialization do not handle. A freshly constructed FeatureService(name="svc", features=["driver_stats@v2:conv_rate"]) has no projections until apply/plan. Passing that object directly to get_online_features or get_historical_features is a supported Feast pattern, but _get_features() consults the registered service while _get_feature_views_to_use() consumes the unresolved passed object, producing inconsistent resolution; to_proto() before apply also silently serializes an empty service.
Please either resolve the passed service from the registry in all retrieval paths, or reject unresolved services with a clear error. Add an integration-style test that reconstructs the string-based FeatureService in a fresh process after apply and passes that object to both online and historical retrieval.
|
I resolved the passed FeatureService from the registry in _get_feature_views_to_use when it has unresolved string refs, so it matches what _get_features already does and both retrieval paths agree instead of one seeing empty projections. Also added an integration test in test_versioning.py that reconstructs the string-ref service in a fresh store after apply and passes it to both online and historical retrieval. |
ntkathole
left a comment
There was a problem hiding this comment.
Thanks for the follow-up on the unresolved string-ref retrieval path — that looks correct.
A few remaining issues that I think should land before merge:
- Object-based pinning mutates the source FeatureView's projection (string-ref path already copies).
- Recommended string refs cannot resolve an ODFV created in the same
apply(fvs_to_update_maponly includesodfvs_to_write). prepare_for_apply/to_proto()still treat an unresolved string-ref service as a valid empty service (all([])isTrue).
Inline comments have details. Would also like one integration test that actually pins v0 vs a later v1 and asserts retrieval, not only the unversioned reconstructed-service path.
| if projection.version_tag is None and fv_version: | ||
| is_latest, version_num = parse_version(fv_version) | ||
| if not is_latest: | ||
| projection.version_tag = version_num |
There was a problem hiding this comment.
This mutates the source FeatureView's own projection. FeatureView(version="v2") does not set projection.version_tag itself, so after FeatureService(features=[fv]) the caller's fv.projection.version_tag becomes 2.
Online table naming (compute_versioned_name) reads that field, so a later materialize/get on the same object can unexpectedly hit *_v2. The string-ref path already copy.copys for this reason — please copy here too before stamping version_tag.
| for feature_service in feature_services_to_update: | ||
| # Resolve string feature refs (e.g. "driver_stats@v2") before | ||
| # inference. No-op for object-only services. | ||
| feature_service.resolve_pending_refs( |
There was a problem hiding this comment.
fvs_to_update_map only includes odfvs_to_write (write_to_online_store=True). A normal ODFV is absent, and _make_inferences runs before apply_feature_view, so the registry lookup fails too.
The docs recommend string refs, but this fails:
odfv = on_demand_feature_view(...)(src_fv)
svc = FeatureService(name="svc", features=["my_odfv"])
store.apply([src_fv, odfv, svc]) # FeatureViewNotFoundObject form features=[odfv] still works. Please pass odfvs_to_update (not just odfvs_to_write) into resolve_pending_refs.
| ) -> None: | ||
| """Resolve string feature refs (see ``__init__``) into projections. | ||
|
|
||
| Called automatically by ``FeatureStore.apply``/``plan`` so the pin is |
There was a problem hiding this comment.
Docstring says apply/plan resolve these refs, but prepare_for_apply does not call resolve_pending_refs, and its early return is if self._features and all(p.features for p in self.feature_view_projections).
all([]) is True, so a service with only string refs and empty projections returns immediately. to_proto() then serializes zero features.
FeatureStore.apply happens to call this first, so the happy path is OK. Direct registry.apply_feature_service(...) or any proto round-trip before apply will persist an empty service with no error.
Please reject (or resolve) unresolved _pending_feature_refs in prepare_for_apply and to_proto().
Closes #6717.
Related to #6389 (feature versioning in offline retrieval).
What
Lets a
FeatureServicepin a specific historical FeatureView / OnDemandFeatureView version, honored by bothget_online_featuresandget_historical_features. Feast's docs currently list this as a known limitation ("Feature services always resolve to the active (promoted) version"); this closes that gap.Commits
projection.version_tagfrom the source view'sversioninFeatureService.__init__; readprojection.version_tag(instead ofNone) inutils._get_feature_views_to_use.version="latest"is unaffected.featuresaccepts"<fv>[@<version>][:<feature>]"strings, resolved once inFeatureStore.applyviaFeatureService.resolve_pending_refs. Factors_parse_feature_or_view_refout of_parse_feature_ref(optional:feature).utils._get_requested_on_demand_feature_viewsused by all three unversioned ODFV call sites; pinned ODFV refs now resolve to the pinned snapshot offline. This is the offline-retrieval versioning gap tracked in Proposal: Version-qualified feature refs in get_historical_features (offline store versioning) #6389.enable_online_feature_view_versioningdocstring clarified to note it gates offline too; Known Limitations updated.Problem
Building a service from a version-pinned
FeatureViewsilently falls back to the promoted version:Two bugs cause this:
FeatureService.__init__never translates the view'sversionstring intoprojection.version_tag, andutils._get_feature_views_to_usehard-codes the version asNonefor theFeatureServicebranch. A related pre-existing bug (#6389) breaks offline retrieval of version-pinnedOnDemandFeatureViews: offline stores re-fetch ODFVs via an unversionedregistry.list_on_demand_feature_views, so a pinned ODFV ref raisesValueError: Could not find feature view from reference ...or silently drops features across every offline backend.Design notes for reviewers
registry.get_feature_view_by_version, never fromfvs_to_update(the apply batch only ever holds "latest" objects and would substitute the wrong schema). Covered bytest_pinned_ref_ignores_fvs_to_update.version="latest"behave exactly as before; every existing (unversioned) FeatureService is unaffected.enable_online_feature_view_versioningis misleadingly scoped to "online" — it gates both paths. Clarified in the docstring/docs here rather than renamed (rename would be a breaking config change; happy to follow up if maintainers prefer).Tests
New unit tests:
test_feature_service_versioning.py(object + string-ref pinning, proto round-trip, backward compat, error guards, priority ordering) andtest_odfv_offline_versioning.py(version-aware ODFV resolution, v0 fallback, non-ODFV skip, dedup). All pass; ruff (0.16.0) + mypy clean on changed files.