Skip to content

Commit 8011550

Browse files
committed
feat: Pre-compute feature service
Signed-off-by: ntkathole <nikhilkathole2683@gmail.com>
1 parent a88efd9 commit 8011550

15 files changed

Lines changed: 2248 additions & 651 deletions

‎docs/getting-started/concepts/feature-retrieval.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,31 @@ Applying a feature service does not result in an actual service being deployed.
5252

5353
Feature services enable referencing all or some features from a feature view.
5454

55+
#### Pre-computed feature vectors (`precompute_online`)
56+
57+
For latency-critical online serving, you can enable **pre-computed feature vectors** on a feature service. When `precompute_online=True`, Feast stores all of the service's features for each entity as a single serialized blob in the online store. At read time, this reduces the number of store reads from O(N feature views) to O(1), regardless of how many feature views the service spans.
58+
59+
```python
60+
# A feature service with pre-computed vectors enabled
61+
low_latency_service = FeatureService(
62+
name="low_latency_inference",
63+
features=[driver_stats_fv, vehicle_stats_fv, route_features_fv],
64+
precompute_online=True,
65+
)
66+
```
67+
68+
After running `feast apply`, the pre-computed vectors are automatically built and refreshed whenever you run `feast materialize` or `feast materialize-incremental`. Feast detects which feature services have `precompute_online=True` and rebuilds their vectors for every affected entity after the per-feature-view writes complete. Vectors are also refreshed automatically on `feast push`.
69+
70+
{% hint style="info" %}
71+
`precompute_online` is **opt-in** — it defaults to `False`. When enabled, the pre-computed path is used exclusively for that service; there is no silent fallback to per-feature-view reads. If vectors are missing or stale, the server raises an error, making problems visible immediately.
72+
{% endhint %}
73+
74+
{% hint style="warning" %}
75+
`precompute_online` is not compatible with on-demand feature views (ODFVs) that have `write_to_online_store=False`. ODFVs with `write_to_online_store=True` are supported since their values are materialized.
76+
{% endhint %}
77+
78+
See the [performance tuning guide](../../how-to-guides/online-server-performance-tuning.md#pre-computed-feature-vectors) for benchmarks and detailed configuration.
79+
5580
Retrieving from the online store with a feature service
5681

5782
```python

‎docs/how-to-guides/online-server-performance-tuning.md‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@ When the server processes a `get_online_features()` call, it groups the requeste
3535
**Redis exception:** The Redis online store overrides `get_online_features()` to batch all `HMGET` commands across every feature view into a **single pipeline execution**. Because all feature views for the same entity share one Redis hash key, the number of Redis round trips is always **1**, regardless of how many feature views the request touches. This means the "fewer feature views" guideline is less critical for Redis than for other stores — but consolidating feature views still reduces serialization and protobuf overhead at the application layer.
3636
{% endhint %}
3737

38-
### Feature services are free
38+
### Feature services are free (and can be faster)
3939

4040
A [Feature Service](../getting-started/concepts/feature-retrieval.md) is a named collection of feature references — it's a convenience grouping, not a separate storage or execution unit. Using a feature service adds only a registry lookup (cached) compared to listing features individually. There is no performance penalty for using feature services, and they are the recommended way to define stable, versioned feature sets for production models.
4141

42+
For latency-critical services, feature services also unlock **pre-computed feature vectors** (`precompute_online=True`), which reduce store reads from O(N feature views) to O(1). See the [Pre-computed feature vectors](#pre-computed-feature-vectors) section below for details and benchmarks.
43+
4244
### ODFV overhead is additive
4345

4446
Regular feature views incur only **store read** cost. On-demand feature views add **CPU-bound transformation** cost on top:
@@ -83,6 +85,45 @@ Requesting just `combined_score` triggers reads from **both** `driver_stats_fv`
8385

8486
---
8587

88+
## Pre-computed feature vectors
89+
90+
When a `get_online_features()` request touches multiple feature views, the server issues a separate store read per feature view. For services spanning 5–15+ feature views, this fan-out dominates latency — even with Redis pipeline batching, the protobuf deserialization and response-building overhead grows linearly with the number of views.
91+
92+
**Pre-computed feature vectors** solve this by storing all of a feature service's features for each entity as a single serialized blob. At read time, the server fetches one blob per entity instead of N reads per feature view, reducing the operation to O(1).
93+
94+
### How it works
95+
96+
1. **Define** a feature service with `precompute_online=True`:
97+
98+
```python
99+
benchmark_service = FeatureService(
100+
name="benchmark_customer_service",
101+
features=[
102+
customer_demographics_fv,
103+
customer_behavioral_profile,
104+
transaction_7d_aggregations,
105+
transaction_30d_aggregations,
106+
transaction_90d_patterns,
107+
atm_usage_30d,
108+
],
109+
precompute_online=True,
110+
)
111+
```
112+
113+
2. **Apply** the feature service: `feast apply`
114+
3. **Materialize** as usual — vectors are built automatically: `feast materialize-incremental $(date -u +"%Y-%m-%dT%H:%M:%S")`
115+
4. **Read** features as usual — the server automatically uses the pre-computed path:
116+
117+
```python
118+
features = store.get_online_features(
119+
features=store.get_feature_service("benchmark_customer_service"),
120+
entity_rows=[{"customer_id": "CUST_000001"}],
121+
full_feature_names=True,
122+
)
123+
```
124+
125+
---
126+
86127
## Worker and connection tuning
87128

88129
The Python feature server uses Gunicorn with async workers. Tuning workers, connections, and timeouts directly impacts throughput and tail latency.
@@ -1086,4 +1127,5 @@ Reset `skip_dedup` to `false` (or remove it) after the bulk reload. Under normal
10861127
- [PostgreSQL Online Store](../reference/online-stores/postgres.md) — Connection pooling and SSL configuration
10871128
- [Redis Online Store](../reference/online-stores/redis.md) — Cluster mode, Sentinel, TTL configuration, and batched reads
10881129
- [On Demand Feature Views](../reference/beta-on-demand-feature-view.md) — Transformation modes and write-time transforms
1130+
- [Feature Services & `precompute_online`](../getting-started/concepts/feature-retrieval.md#pre-computed-feature-vectors-precompute_online) — Concept docs for pre-computed feature vectors
10891131
- [feature_store.yaml reference](../reference/feature-repository/feature-store-yaml.md) — Full configuration reference including `materialization` options

‎protos/feast/core/FeatureService.proto‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ message FeatureServiceSpec {
3838

3939
// (optional) if provided logging will be enabled for this feature service.
4040
LoggingConfig logging_config = 7;
41+
42+
// When true, a pre-computed feature vector is maintained per entity for this
43+
// service, enabling single-read online retrieval instead of per-feature-view reads.
44+
bool precompute_online = 8;
4145
}
4246

4347

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
syntax = "proto3";
2+
package feast.core;
3+
4+
option go_package = "github.com/feast-dev/feast/go/protos/feast/core";
5+
option java_outer_classname = "PrecomputedFeatureVectorProto";
6+
option java_package = "feast.proto.core";
7+
8+
import "google/protobuf/timestamp.proto";
9+
import "feast/types/Value.proto";
10+
11+
// A pre-computed feature vector stores all features for a FeatureService
12+
// as a single serialized blob per entity, enabling O(1) online retrieval.
13+
message PrecomputedFeatureVector {
14+
// Fully-qualified feature names in deterministic order, e.g. "fv1__feat1".
15+
repeated string feature_names = 1;
16+
17+
// Feature values in the same order as feature_names.
18+
repeated feast.types.Value values = 2;
19+
20+
// Per-feature-view event timestamps for TTL enforcement at read time.
21+
repeated FeatureViewTimestamp fv_timestamps = 3;
22+
23+
// Wall-clock time when this vector was assembled.
24+
google.protobuf.Timestamp precomputed_at = 4;
25+
}
26+
27+
// Event timestamp associated with a specific feature view within the vector.
28+
message FeatureViewTimestamp {
29+
string feature_view_name = 1;
30+
google.protobuf.Timestamp event_timestamp = 2;
31+
}

‎sdk/python/feast/feature_service.py‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ class FeatureService:
4949
last_updated_timestamp: Optional[datetime] = None
5050
logging_config: Optional[LoggingConfig] = None
5151

52+
precompute_online: bool = False
53+
5254
def __init__(
5355
self,
5456
*,
@@ -58,18 +60,21 @@ def __init__(
5860
description: str = "",
5961
owner: str = "",
6062
logging_config: Optional[LoggingConfig] = None,
63+
precompute_online: bool = False,
6164
):
6265
"""
6366
Creates a FeatureService object.
6467
6568
Args:
6669
name: The unique name of the feature service.
67-
feature_view_projections: A list containing feature views and feature view
70+
features: A list containing feature views and feature view
6871
projections, representing the features in the feature service.
6972
description (optional): A human-readable description.
7073
tags (optional): A dictionary of key-value pairs to store arbitrary metadata.
7174
owner (optional): The owner of the feature view, typically the email of the
7275
primary maintainer.
76+
precompute_online (optional): When True, a pre-computed feature vector is
77+
maintained per entity for single-read online retrieval.
7378
"""
7479
self.name = name
7580
self._features = features
@@ -80,6 +85,7 @@ def __init__(
8085
self.created_timestamp = None
8186
self.last_updated_timestamp = None
8287
self.logging_config = logging_config
88+
self.precompute_online = precompute_online
8389
for feature_grouping in self._features:
8490
if isinstance(feature_grouping, BaseFeatureView):
8591
self.feature_view_projections.append(feature_grouping.projection)
@@ -175,6 +181,7 @@ def __eq__(self, other):
175181
or self.description != other.description
176182
or self.tags != other.tags
177183
or self.owner != other.owner
184+
or self.precompute_online != other.precompute_online
178185
):
179186
return False
180187

@@ -202,6 +209,7 @@ def from_proto(cls, feature_service_proto: FeatureServiceProto):
202209
logging_config=LoggingConfig.from_proto(
203210
feature_service_proto.spec.logging_config
204211
),
212+
precompute_online=feature_service_proto.spec.precompute_online,
205213
)
206214
fs.feature_view_projections.extend(
207215
[
@@ -245,9 +253,20 @@ def to_proto(self) -> FeatureServiceProto:
245253
logging_config=self.logging_config.to_proto()
246254
if self.logging_config
247255
else None,
256+
precompute_online=self.precompute_online,
248257
)
249258

250259
return FeatureServiceProto(spec=spec, meta=meta)
251260

252261
def validate(self):
253-
pass
262+
if not self.precompute_online:
263+
return
264+
265+
for fv in self._features:
266+
if isinstance(fv, OnDemandFeatureView) and not fv.write_to_online_store:
267+
raise ValueError(
268+
f"FeatureService '{self.name}' has precompute_online=True but "
269+
f"contains OnDemandFeatureView '{fv.name}' with "
270+
f"write_to_online_store=False. On-demand transforms computed at "
271+
f"serve time cannot be pre-computed."
272+
)

0 commit comments

Comments
 (0)