Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
97f938a
Compile new protos
jchmura-sc Aug 3, 2026
1862938
Add quantization ops
jchmura-sc Aug 3, 2026
3b9134e
Minor format updates
jchmura-sc Aug 3, 2026
17a9d84
Add tests
jchmura-sc Aug 3, 2026
4376ea7
Whitespace
jchmura-sc Aug 3, 2026
838f2ed
WIP
jchmura-sc Aug 3, 2026
d357421
Add preprocessor diff
jchmura-sc Aug 3, 2026
459b8cb
Add test
jchmura-sc Aug 3, 2026
be34beb
Update read side
jchmura-sc Aug 3, 2026
be4a9fe
Add integration code
jchmura-sc Aug 4, 2026
0400961
Add docs
jchmura-sc Aug 4, 2026
ec88d0f
Update docs
jchmura-sc Aug 4, 2026
3e76a52
Add inline comment explaining uint16 cast
jchmura-sc Aug 4, 2026
c0b4847
Add quantize dequantize roundtrip test
jchmura-sc Aug 4, 2026
7c70b02
Simplify test logic
jchmura-sc Aug 4, 2026
1510b43
Defensive check against nan or inf
jchmura-sc Aug 4, 2026
5116725
Pass clip args directly to quantize
jchmura-sc Aug 4, 2026
f670c83
Merge branch 'jchmura/feature_quant_ops' into jchmura/feature_quant_p…
jchmura-sc Aug 4, 2026
d74590c
Upd
jchmura-sc Aug 4, 2026
1ebbd19
Upd
jchmura-sc Aug 7, 2026
1fd3e28
upd
jchmura-sc Aug 7, 2026
48bab63
WIP
jchmura-sc Aug 7, 2026
1ecf397
WIP
jchmura-sc Aug 7, 2026
2fd2c64
WIP
jchmura-sc Aug 7, 2026
3fbe66a
Add SUPPORTED_QUANTIZATION_BITS const
jchmura-sc Aug 7, 2026
109e1e5
Add docstring to quantization transform
jchmura-sc Aug 7, 2026
02bb49a
Merge main
jchmura-sc Aug 10, 2026
7ed4b29
Format docs
jchmura-sc Aug 10, 2026
b490889
Revert diff
jchmura-sc Aug 10, 2026
42676f5
Revert diff in graph types
jchmura-sc Aug 10, 2026
46803e3
Merge branch 'jchmura/feature_quant_preprocessor' into jchmura/featur…
jchmura-sc Aug 10, 2026
6c8c3c4
Cleanup
jchmura-sc Aug 10, 2026
8e59f96
Update unit tests
jchmura-sc Aug 10, 2026
6ce5922
Better comments
jchmura-sc Aug 10, 2026
be16dae
Expand multi-line decleration with typedefs
jchmura-sc Aug 10, 2026
5bf105b
Use len() check instead of full set equality for partial quantization…
jchmura-sc Aug 10, 2026
fa02749
Merge main
jchmura-sc Aug 11, 2026
29aabbb
Run format
jchmura-sc Aug 11, 2026
8f01851
Merge upstream
jchmura-sc Aug 11, 2026
6270a0b
Lazy log format
jchmura-sc Aug 11, 2026
99ef03d
Update graph store dist server contract to match distdataset
jchmura-sc Aug 11, 2026
2b42042
Add unit tests
jchmura-sc Aug 11, 2026
779a947
Update tests
jchmura-sc Aug 11, 2026
bdc3e0f
Fix implicit default homogenous key mismatch in quant metadata when a…
jchmura-sc Aug 11, 2026
7bcafc4
Fix test
jchmura-sc Aug 11, 2026
37291ad
Inline quant metadata normalization
jchmura-sc Aug 11, 2026
6e25193
Remove collate timers
jchmura-sc Aug 11, 2026
5d8d148
Whitesapce
jchmura-sc Aug 11, 2026
438e07c
Add descriptive comment for why we need to remap quantization metadat
jchmura-sc Aug 11, 2026
c8d43a5
Don't need node quantized feature info
jchmura-sc Aug 11, 2026
0f2f9b9
Remove debug timing
jchmura-sc Aug 11, 2026
c9a9912
Value error instead of assertion
jchmura-sc Aug 11, 2026
feb138e
Remove stale share ipc entry
jchmura-sc Aug 11, 2026
6795731
Upd
jchmura-sc Aug 11, 2026
41f3a21
Upd
jchmura-sc Aug 11, 2026
1e375ac
Update tests
jchmura-sc Aug 11, 2026
bec1f10
Remove quantization metadata property to make RPC call explicit
jchmura-sc Aug 11, 2026
84e3e58
Update
jchmura-sc Aug 11, 2026
d2e34c5
Merge branch 'main' into jchmura/feature_quant_integration
jchmura-sc Aug 12, 2026
ca7fdea
Add type to storage
jchmura-sc Aug 12, 2026
54e53c5
No need for metadata promotion to dict on labeled homogeneous ablp
jchmura-sc Aug 12, 2026
1cdeb88
Simplify test
jchmura-sc Aug 12, 2026
50e6a8d
Add type to scatter index
jchmura-sc Aug 12, 2026
207631a
Add edge feature quantization
jchmura-sc Aug 12, 2026
40e1460
Update
jchmura-sc Aug 12, 2026
dcb7722
Improve test readability
jchmura-sc Aug 12, 2026
5de7dd9
Fix heterogenous sampler collate with only partially quantized node t…
jchmura-sc Aug 12, 2026
eccc0ea
Merge branch 'jchmura/feature_quant_integration' into jchmura/edge-fe…
jchmura-sc Aug 12, 2026
a528164
Remove planning docs
jchmura-sc Aug 12, 2026
d141185
Simple improvements
jchmura-sc Aug 13, 2026
7dfee9c
Clarify sampling weight quantization metadata
jchmura-sc Aug 13, 2026
10c6574
Simplify sampling weight metadata adjustment
jchmura-sc Aug 13, 2026
828ef2d
Inline sampled edge metadata mapping
jchmura-sc Aug 13, 2026
06c3b71
Format edge quantization changes
jchmura-sc Aug 13, 2026
20bd8b4
Improved docs for materialize_quantized_node_features
jchmura-sc Aug 13, 2026
03b0e49
Return partitioned quantized edge features
jchmura-sc Aug 13, 2026
d0f62cd
Minimize range partitioner documentation diff
jchmura-sc Aug 13, 2026
3e58bd9
Simplify edge partitioner diffs
jchmura-sc Aug 13, 2026
4bcab57
Revert diff
jchmura-sc Aug 13, 2026
a488153
Remove redundant diff
jchmura-sc Aug 13, 2026
df6517b
Merge branch 'jchmura/feature_quant_integration' into jchmura/edge-fe…
jchmura-sc Aug 13, 2026
df98e0f
Merge main
jchmura-sc Aug 13, 2026
34ea459
Extract common helper function for scatter quantized feats
jchmura-sc Aug 13, 2026
14a2f37
Merge branch 'main' of github.com:Snapchat/GiGL into jchmura/edge-fea…
jchmura-sc Aug 13, 2026
b1a26f9
Simplify quantized feature materialization
jchmura-sc Aug 13, 2026
217ce93
Run review
jchmura-sc Aug 13, 2026
9b82deb
Remove private dep from test
jchmura-sc Aug 13, 2026
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
21 changes: 11 additions & 10 deletions gigl/common/data/dataloaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,16 +398,6 @@ def load_as_torch_tensors(
feature_spec_dict[entity_key] = tf.io.FixedLenFeature(
shape=[], dtype=tf.int64
)
if (
packed_feature_key is not None
and packed_feature_key not in feature_spec_dict
):
logger.info(
f"Injecting packed feature key {packed_feature_key} into feature spec dictionary with value `tf.io.FixedLenFeature(shape=[], dtype=tf.string)`"
)
feature_spec_dict[packed_feature_key] = tf.io.FixedLenFeature(
shape=[], dtype=tf.string
)
else:
id_concat_axis = 1
proccess_id_tensor = lambda t: tf.stack(
Expand All @@ -433,6 +423,17 @@ def load_as_torch_tensors(
shape=[], dtype=tf.int64
)

if (
packed_feature_key is not None
and packed_feature_key not in feature_spec_dict
):
logger.info(
f"Injecting packed feature key {packed_feature_key} into feature spec dictionary with value `tf.io.FixedLenFeature(shape=[], dtype=tf.string)`"
)
feature_spec_dict[packed_feature_key] = tf.io.FixedLenFeature(
shape=[], dtype=tf.string
)

uris = self._partition_children_uris(
serialized_tf_record_info.tfrecord_uri_prefix,
serialized_tf_record_info.tfrecord_uri_pattern,
Expand Down
149 changes: 139 additions & 10 deletions gigl/common/data/load_torch_tensors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import time
import traceback
from dataclasses import dataclass
from typing import MutableMapping, Optional, Union
from dataclasses import dataclass, replace
from typing import MutableMapping, Optional, Union, cast

import torch
import torch.multiprocessing as mp
Expand Down Expand Up @@ -119,6 +119,134 @@ class SerializedGraphMetadata:
node_quantization_metadata: Optional[
Union[FeatureQuantizationMetadata, dict[NodeType, FeatureQuantizationMetadata]]
] = None
edge_quantization_metadata: Optional[
Union[FeatureQuantizationMetadata, dict[EdgeType, FeatureQuantizationMetadata]]
] = None


def _validate_weight_edge_feature_name(
Comment thread
jchmura-sc marked this conversation as resolved.
edge_entity_info: Union[
SerializedTFRecordInfo, dict[EdgeType, SerializedTFRecordInfo]
],
weight_edge_feat_name: Optional[Union[str, dict[EdgeType, str]]],
) -> None:
if weight_edge_feat_name is None:
return

configured_weights: list[tuple[EdgeType, str, SerializedTFRecordInfo]]
if isinstance(edge_entity_info, SerializedTFRecordInfo):
if not isinstance(weight_edge_feat_name, str):
raise ValueError("weight_edge_feat_name must be str for homogeneous graph")
edge_type = DEFAULT_HOMOGENEOUS_EDGE_TYPE
configured_weights = [(edge_type, weight_edge_feat_name, edge_entity_info)]
else:
if isinstance(weight_edge_feat_name, str):
if len(edge_entity_info) != 1:
raise ValueError(
"weight_edge_feat_name must be dict[EdgeType, str] for heterogeneous graph with multiple edge types"
)
edge_type, serialized_info = next(iter(edge_entity_info.items()))
configured_weights = [(edge_type, weight_edge_feat_name, serialized_info)]
else:
unknown_edge_types = set(weight_edge_feat_name) - set(edge_entity_info)
if unknown_edge_types:
raise ValueError(
f"weight_edge_feat_name contains unknown edge types: {unknown_edge_types}"
)
configured_weights = [
(edge_type, feature_name, edge_entity_info[edge_type])
for edge_type, feature_name in weight_edge_feat_name.items()
]

for edge_type, feature_name, serialized_info in configured_weights:
if feature_name not in serialized_info.feature_keys:
raise ValueError(
f"Sampling-weight field '{feature_name}' for edge type {edge_type} must be an unquantized raw edge feature."
)


def remove_sampling_weight_from_edge_quantization_metadata(
serialized_graph_metadata: SerializedGraphMetadata,
weight_edge_feat_name: Optional[Union[str, dict[EdgeType, str]]],
) -> Optional[
Union[FeatureQuantizationMetadata, dict[EdgeType, FeatureQuantizationMetadata]]
]:
"""Remove separately stored sampling weights from edge reconstruction metadata.

TFRecord loading removes the sampling-weight column from raw edge features
before registering it with the weighted sampler. The resulting metadata
must describe the remaining model features so batch reconstruction scatters
raw and dequantized columns into the correct positions.

Args:
serialized_graph_metadata: Serialized edge schema and quantization metadata.
weight_edge_feat_name: Raw scalar feature configured as sampling weights.

Returns:
Quantization metadata for the model-facing edge features.
"""
quantization_metadata = serialized_graph_metadata.edge_quantization_metadata
if quantization_metadata is None or weight_edge_feat_name is None:
return quantization_metadata

if isinstance(serialized_graph_metadata.edge_entity_info, SerializedTFRecordInfo):
assert isinstance(quantization_metadata, FeatureQuantizationMetadata)
assert isinstance(weight_edge_feat_name, str)
edge_info_by_type: dict[EdgeType, SerializedTFRecordInfo] = {
DEFAULT_HOMOGENEOUS_EDGE_TYPE: serialized_graph_metadata.edge_entity_info
}
metadata_by_type: dict[EdgeType, FeatureQuantizationMetadata] = {
DEFAULT_HOMOGENEOUS_EDGE_TYPE: quantization_metadata
}
weight_by_type: dict[EdgeType, str] = {
DEFAULT_HOMOGENEOUS_EDGE_TYPE: weight_edge_feat_name
}
is_homogeneous = True
else:
assert isinstance(quantization_metadata, dict)
edge_info_by_type: dict[EdgeType, SerializedTFRecordInfo] = (
serialized_graph_metadata.edge_entity_info
)
metadata_by_type: dict[EdgeType, FeatureQuantizationMetadata] = cast(
dict[EdgeType, FeatureQuantizationMetadata], quantization_metadata
)
if isinstance(weight_edge_feat_name, str):
edge_type = next(iter(edge_info_by_type))
weight_by_type: dict[EdgeType, str] = {edge_type: weight_edge_feat_name}
else:
weight_by_type: dict[EdgeType, str] = weight_edge_feat_name
is_homogeneous = False

adjusted_metadata: dict[EdgeType, FeatureQuantizationMetadata] = {}
for edge_type, metadata in metadata_by_type.items():
weight_feature_name = weight_by_type.get(edge_type)
if weight_feature_name is None:
adjusted_metadata[edge_type] = metadata
continue

edge_info = edge_info_by_type[edge_type]
raw_column_offset = 0
for feature_name in edge_info.feature_keys:
if feature_name == weight_feature_name:
break
feature_spec = edge_info.feature_spec[feature_name]
raw_column_offset += feature_spec.shape[-1] if feature_spec.shape else 1
weight_logical_index = metadata.raw_feature_indices[raw_column_offset]
adjusted_quantized_feature_indices = tuple(
quantized_feature_index - 1
if quantized_feature_index > weight_logical_index
else quantized_feature_index
for quantized_feature_index in metadata.quantized_feature_indices
)
adjusted_metadata[edge_type] = replace(
metadata,
feature_dim=metadata.feature_dim - 1,
quantized_feature_indices=adjusted_quantized_feature_indices,
)

if is_homogeneous:
return adjusted_metadata[DEFAULT_HOMOGENEOUS_EDGE_TYPE]
return adjusted_metadata


def _data_loading_process(
Expand Down Expand Up @@ -199,14 +327,6 @@ def _data_loading_process(
raise NotImplementedError(
"Label keys are not supported for edge entities"
)
if (
serialized_entity_tf_record_info.packed_feature_key is not None
and not serialized_entity_tf_record_info.is_node_entity
):
# TODO(quantization): Support feature quantization for edge features.
raise NotImplementedError(
"Packed feature keys are not supported for edge entities"
)
loaded_entity = tf_record_dataloader.load_as_torch_tensors(
serialized_tf_record_info=serialized_entity_tf_record_info,
tf_dataset_options=tf_dataset_options,
Expand Down Expand Up @@ -396,6 +516,11 @@ def load_torch_tensors_from_tf_record(
loaded_graph_tensors (LoadedGraphTensors): Unpartitioned Graph Tensors
"""

_validate_weight_edge_feature_name(
edge_entity_info=serialized_graph_metadata.edge_entity_info,
weight_edge_feat_name=weight_edge_feat_name,
)

logger.info(f"Rank {rank} starting loading torch tensors from serialized info ...")
start_time = time.time()

Expand Down Expand Up @@ -525,6 +650,9 @@ def load_torch_tensors_from_tf_record(

edge_index = edge_output_dict[_ID_FMT.format(entity=_EDGE_KEY)]
edge_features = edge_output_dict.get(_FEATURE_FMT.format(entity=_EDGE_KEY), None)
edge_quantized_features = edge_output_dict.get(
_PACKED_FEATURE_FMT.format(entity=_EDGE_KEY), None
)
edge_weights = edge_output_dict.get(_EDGE_WEIGHTS_KEY, None)

positive_labels = edge_output_dict.get(
Expand Down Expand Up @@ -552,6 +680,7 @@ def load_torch_tensors_from_tf_record(
node_labels=node_labels,
edge_index=edge_index,
edge_features=edge_features,
edge_quantized_features=edge_quantized_features,
positive_label=positive_labels,
negative_label=negative_labels,
edge_weights=edge_weights,
Expand Down
4 changes: 2 additions & 2 deletions gigl/common/utils/feature_quantization/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ this as a useful tradeoff for GiGL.
The built-in flow is:

1. The data preprocessor computes feature summary statistics offline.
2. The preprocessor quantizes selected scalar feature columns with NumPy.
2. The preprocessor quantizes selected scalar node or main-edge feature columns with NumPy.
3. The packed `uint8` feature sidecar is written to TFRecords.
4. Distributed dataset construction partitions and samples the packed bytes.
5. The dataloader collate path dequantizes sampled packed features with Torch.
6. Dequantized columns are scattered back into the logical `x` feature matrix.
6. Dequantized columns are scattered back into the logical `x` or `edge_attr` feature matrix.

The NumPy/Torch split is intentional:

Expand Down
6 changes: 5 additions & 1 deletion gigl/distributed/base_dist_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ def __init__(
self._node_feature_info = dataset_schema.node_feature_info
self._edge_feature_info = dataset_schema.edge_feature_info
self._node_quantization_metadata = dataset_schema.node_quantization_metadata
self._edge_quantization_metadata = dataset_schema.edge_quantization_metadata

self._sampler_options = sampler_options
self._non_blocking_transfers = non_blocking_transfers
Expand Down Expand Up @@ -435,7 +436,10 @@ def create_sampling_config(
batch_size=batch_size,
shuffle=shuffle,
drop_last=drop_last,
with_edge=dataset_schema.edge_feature_info is not None,
with_edge=(
dataset_schema.edge_feature_info is not None
or dataset_schema.edge_quantization_metadata is not None
),
collect_features=True,
with_neg=False,
with_weight=with_weight,
Expand Down
42 changes: 42 additions & 0 deletions gigl/distributed/base_sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from gigl.common.logger import Logger
from gigl.distributed.sampler import (
EDGE_PACKED_FEATURES_METADATA_KEY,
NEGATIVE_LABEL_METADATA_KEY,
NODE_PACKED_FEATURES_METADATA_KEY,
POSITIVE_LABEL_METADATA_KEY,
Expand Down Expand Up @@ -116,6 +117,7 @@ def __init__(self, *args, **kwargs) -> None:
self._sampling_error_sent: bool = False

self.dist_node_quantized_feature: Optional[DistFeature] = None
self.dist_edge_quantized_feature: Optional[DistFeature] = None
if (
self.collect_features
and data is not None
Expand All @@ -132,6 +134,20 @@ def __init__(self, *args, **kwargs) -> None:
rpc_router=self.rpc_router,
device=self.device,
)
if (
self.collect_features
and data is not None
and getattr(data, "edge_quantized_features", None) is not None
):
self.dist_edge_quantized_feature = DistFeature(
data.num_partitions,
data.partition_idx,
data.edge_quantized_features,
data.edge_pb,
local_only=False,
rpc_router=self.rpc_router,
device=self.device,
)

def _prepare_sample_loop_inputs(
self,
Expand Down Expand Up @@ -436,6 +452,8 @@ async def _collate_fn(
)
if self.dist_edge_feature is not None and self.with_edge:
for etype in self.edge_types:
if etype not in self.dist_edge_feature.local_feature:
continue
if self.edge_dir == "in":
eids = result_map.get(
f"{as_str(reverse_edge_type(etype))}.eids", None
Expand All @@ -451,6 +469,26 @@ async def _collate_fn(
futs[result_key] = wrap_torch_future(
self.dist_edge_feature.async_get(eids, etype)
)
if self.dist_edge_quantized_feature is not None and self.with_edge:
for etype in self.edge_types:
# Like node features, an edge partition book covers every
# edge type while a feature store may register only some.
if etype not in self.dist_edge_quantized_feature.local_feature:
continue
result_edge_type = (
reverse_edge_type(etype) if self.edge_dir == "in" else etype
)
eids = result_map.get(f"{as_str(result_edge_type)}.eids")
if eids is not None:
eids = eids.to(torch.long)
# GLT maps incoming wire edge types back to the dataset edge
# type during collation. Metadata bypasses that mapping, so its
# transport key must already match the final output store.
futs[f"#META.{EDGE_PACKED_FEATURES_METADATA_KEY}.{etype}"] = (
wrap_torch_future(
self.dist_edge_quantized_feature.async_get(eids, etype)
)
)
if output.batch is not None:
for ntype, batch in output.batch.items():
result_map[f"{as_str(ntype)}.batch"] = batch
Expand Down Expand Up @@ -490,6 +528,10 @@ async def _collate_fn(
futs["efeats"] = wrap_torch_future(
self.dist_edge_feature.async_get(eids)
)
if self.dist_edge_quantized_feature is not None:
futs[f"#META.{EDGE_PACKED_FEATURES_METADATA_KEY}"] = wrap_torch_future(
self.dist_edge_quantized_feature.async_get(result_map["eids"])
)
if output.batch is not None:
result_map["batch"] = output.batch

Expand Down
10 changes: 10 additions & 0 deletions gigl/distributed/dataset_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
SerializedGraphMetadata,
TFDatasetOptions,
load_torch_tensors_from_tf_record,
remove_sampling_weight_from_edge_quantization_metadata,
)
from gigl.common.logger import Logger
from gigl.common.utils.decorator import tf_on_cpu
Expand Down Expand Up @@ -194,6 +195,10 @@ def _load_and_build_partitioned_dataset(
partitioner.register_edge_features(
edge_features=loaded_graph_tensors.edge_features
)
if loaded_graph_tensors.edge_quantized_features is not None:
partitioner.register_edge_quantized_features(
edge_quantized_features=loaded_graph_tensors.edge_quantized_features
)
if loaded_graph_tensors.positive_label is not None:
partitioner.register_labels(
label_edge_index=loaded_graph_tensors.positive_label, is_positive=True
Expand All @@ -212,6 +217,7 @@ def _load_and_build_partitioned_dataset(
loaded_graph_tensors.node_quantized_features,
loaded_graph_tensors.edge_index,
loaded_graph_tensors.edge_features,
loaded_graph_tensors.edge_quantized_features,
loaded_graph_tensors.edge_weights,
loaded_graph_tensors.positive_label,
loaded_graph_tensors.negative_label,
Expand All @@ -227,6 +233,10 @@ def _load_and_build_partitioned_dataset(
world_size=world_size,
edge_dir=edge_dir,
node_quantization_metadata=serialized_graph_metadata.node_quantization_metadata,
edge_quantization_metadata=remove_sampling_weight_from_edge_quantization_metadata(
serialized_graph_metadata=serialized_graph_metadata,
weight_edge_feat_name=weight_edge_feat_name,
),
)

dataset.build(
Expand Down
Loading