chore: import upstream snapshot with attribution
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:20 +08:00
commit 593b94c120
1647 changed files with 382489 additions and 0 deletions
@@ -0,0 +1,348 @@
# Adding a New Feature Type to Ludwig
This guide walks through every file you need to touch when adding a brand-new feature type (e.g. a hypothetical `"widget"` type). Use `ludwig/features/binary_feature.py` and `ludwig/schema/features/binary_feature.py` as living reference implementations — they are among the simplest complete examples.
______________________________________________________________________
## Conceptual overview
Each feature type lives in two parallel places:
| Layer | Location | Purpose |
| ------------------ | ------------------------------------------ | ----------------------------------------------------------------------------- |
| **Schema** | `ludwig/schema/features/<type>_feature.py` | Pydantic-backed config classes; declares hyperparameters and their defaults |
| **Feature module** | `ludwig/features/<type>_feature.py` | PyTorch modules; implements preprocessing, encoding, decoding, postprocessing |
The schema classes are used for config validation and serialization. The feature module classes are instantiated at model-build time using those configs. Neither layer knows the other exists at import time — they are wired together through the feature registry.
______________________________________________________________________
## Step 1 Define the constant
Add the type string to `ludwig/constants.py`:
```python
WIDGET = "widget"
```
______________________________________________________________________
## Step 2 — Write the schema file
Create `ludwig/schema/features/widget_feature.py`. The minimal required structure is:
```python
from ludwig.constants import WIDGET, MODEL_ECD
from ludwig.schema import utils as schema_utils
from ludwig.schema.encoders.base import BaseEncoderConfig
from ludwig.schema.encoders.utils import EncoderDataclassField
from ludwig.schema.features.base import BaseInputFeatureConfig, BaseOutputFeatureConfig
from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig
from ludwig.schema.features.preprocessing.utils import PreprocessingDataclassField
from ludwig.schema.features.utils import (
ecd_defaults_config_registry,
ecd_input_config_registry,
ecd_output_config_registry,
input_mixin_registry,
output_mixin_registry,
)
from ludwig.schema.utils import LudwigBaseConfig
@input_mixin_registry.register(WIDGET)
class WidgetInputFeatureConfigMixin(LudwigBaseConfig):
preprocessing: BasePreprocessingConfig = PreprocessingDataclassField(feature_type=WIDGET)
class WidgetInputFeatureConfig(WidgetInputFeatureConfigMixin, BaseInputFeatureConfig):
type: str = schema_utils.ProtectedString(WIDGET)
encoder: BaseEncoderConfig = None
@ecd_input_config_registry.register(WIDGET)
class ECDWidgetInputFeatureConfig(WidgetInputFeatureConfig):
encoder: BaseEncoderConfig = EncoderDataclassField(
MODEL_ECD,
feature_type=WIDGET,
default="dense", # default encoder for this type
)
# For output features only:
@output_mixin_registry.register(WIDGET)
class WidgetOutputFeatureConfigMixin(LudwigBaseConfig):
# add loss, calibration, etc. fields here
pass
class WidgetOutputFeatureConfig(WidgetOutputFeatureConfigMixin, BaseOutputFeatureConfig):
type: str = schema_utils.ProtectedString(WIDGET)
default_validation_metric: str = "some_metric"
@ecd_output_config_registry.register(WIDGET)
class ECDWidgetOutputFeatureConfig(WidgetOutputFeatureConfig):
pass
```
**Key rules:**
- `type` must be a `ProtectedString` with your constant — this prevents accidental overwrite via user YAML.
- `@input_mixin_registry.register` / `@output_mixin_registry.register` make the preprocessing config available to `global_defaults` in Ludwig configs.
- `@ecd_input_config_registry.register` / `@ecd_output_config_registry.register` wire the schema into the ECD model config builder.
______________________________________________________________________
## Step 3 — Write the preprocessing config
Create `ludwig/schema/features/preprocessing/widget_feature_preprocessing.py` if your feature needs non-default preprocessing parameters, or register your type against an existing one (e.g. `number_feature` for scalars). For a new type, create the file:
```python
from ludwig.schema.features.preprocessing.base import BasePreprocessingConfig
from ludwig.schema.features.preprocessing.utils import register_preprocessor
from ludwig.constants import WIDGET
@register_preprocessor(WIDGET)
class WidgetPreprocessingConfig(BasePreprocessingConfig):
# add preprocessing hyperparameters here
pass
```
______________________________________________________________________
## Step 4 — Write the feature module
Create `ludwig/features/widget_feature.py`. The required classes are:
### Inner preprocessing module
```python
import torch
from ludwig.features.base_feature import BasePreprocessingModule, FeaturePreprocessingMixin, InputFeature, OutputFeature
class _WidgetPreprocessing(BasePreprocessingModule):
"""Runs inside the model graph during inference to preprocess raw input."""
def __init__(self, metadata: dict, preprocessing_config, is_input_feature: bool = True):
super().__init__()
# store everything needed to preprocess at inference time
def forward(self, v):
# v is the raw column value; return a tensor
raise NotImplementedError
```
### FeatureMixin (shared preprocessing logic)
`FeaturePreprocessingMixin` provides the Python-side preprocessing used during dataset preparation (not inside the model graph). You must implement `add_feature_data` and `get_preprocessing_module`:
```python
class WidgetFeatureMixin(FeaturePreprocessingMixin):
@staticmethod
def type():
return WIDGET
@staticmethod
def cast_column(column, backend):
"""Cast the raw DataFrame column to the expected dtype."""
return column
@staticmethod
def add_feature_data(
feature_config,
input_df,
proc_df,
metadata,
preprocessing_parameters,
backend,
skip_save_processed_input,
):
"""Populate proc_df[feature_config[PROC_COLUMN]] with preprocessed values."""
proc_df[feature_config[PROC_COLUMN]] = input_df[feature_config[COLUMN]].values
return proc_df
@staticmethod
def fill_missing_values(feature_config, input_df, backend):
"""Replace NaN/None with a fill value appropriate for this type."""
return input_df
@staticmethod
def feature_meta(column, preprocessing_parameters, backend):
"""Compute and return the training-set-level metadata dict for this feature."""
return {}
@staticmethod
def get_preprocessing_module(feature_config, metadata):
"""Return the _WidgetPreprocessing module for use during inference."""
return _WidgetPreprocessing(metadata, feature_config.preprocessing)
```
### InputFeature class
```python
from ludwig.schema.features.widget_feature import WidgetInputFeatureConfig
class WidgetInputFeature(WidgetFeatureMixin, InputFeature):
def __init__(self, input_feature_config: WidgetInputFeatureConfig, encoder_obj=None, **kwargs):
super().__init__(input_feature_config, **kwargs)
self._input_shape = torch.Size([1]) # set to actual encoded shape
if encoder_obj:
self.encoder_obj = encoder_obj
else:
self.encoder_obj = self.initialize_encoder(input_feature_config.encoder)
def forward(self, inputs, mask=None):
assert inputs.dtype == torch.float32
encoder_output = self.encoder_obj(inputs, mask=mask)
return {"encoder_output": encoder_output}
@property
def input_dtype(self):
return torch.float32
@property
def input_shape(self):
return self._input_shape
@property
def output_shape(self):
return self.encoder_obj.output_shape
@staticmethod
def update_config_with_metadata(feature_config, feature_metadata, *args, **kwargs):
pass
@staticmethod
def create_sample_input(batch_size=2):
return torch.zeros(batch_size, 1)
@staticmethod
def get_schema_cls():
return WidgetInputFeatureConfig
```
### OutputFeature class (only if this type can be a target)
```python
from ludwig.schema.features.widget_feature import WidgetOutputFeatureConfig
class WidgetOutputFeature(WidgetFeatureMixin, OutputFeature):
def __init__(self, output_feature_config: WidgetOutputFeatureConfig, output_features: dict, **kwargs):
super().__init__(output_feature_config, output_features, **kwargs)
self._input_shape = torch.Size([output_feature_config.input_size])
self.decoder_obj = self.initialize_decoder(output_feature_config.decoder)
self._setup_loss()
self._setup_metrics()
def logits(self, inputs, target=None):
return self.decoder_obj(inputs)
def create_predict_module(self):
return _WidgetPredict() # see PredictModule below
def get_prediction_set(self):
return {LOGITS, PREDICTIONS, PROBABILITIES}
@classmethod
def update_config_with_metadata(cls, feature_config, feature_metadata, *args, **kwargs):
feature_config.input_size = feature_metadata["input_size"]
@staticmethod
def get_schema_cls():
return WidgetOutputFeatureConfig
```
### PredictModule (for output features)
```python
from ludwig.features.base_feature import PredictModule
class _WidgetPredict(PredictModule):
def forward(self, inputs, feature_name):
logits = inputs[f"{feature_name}_{LOGITS}"]
predictions = (logits > 0.5).float()
return {PREDICTIONS: predictions, LOGITS: logits}
```
______________________________________________________________________
## Step 5 — Register in the feature registries
Open `ludwig/features/feature_registries.py` and add your classes to all relevant registry functions:
```python
# at the top — add import
from ludwig.features.widget_feature import WidgetFeatureMixin, WidgetInputFeature
# in get_base_type_registry(), inside the returned dict:
# WIDGET: WidgetFeatureMixin,
#
# in get_input_type_registry(), inside the returned dict:
# WIDGET: WidgetInputFeature,
#
# in get_output_type_registry() if applicable, inside the returned dict:
# WIDGET: WidgetOutputFeature,
```
The model builder uses `get_input_type_registry()` and `get_output_type_registry()` to instantiate feature objects from config at training time.
______________________________________________________________________
## Step 6 — Register the constant in constants.py (feature sets)
If the feature appears in `FEATURE_TYPES`, `INPUT_FEATURE_TYPES`, or similar sets, add `WIDGET` there too.
______________________________________________________________________
## Step 7 — Write tests
Create `tests/ludwig/features/test_widget_feature.py`. At minimum test:
1. `WidgetFeatureMixin.add_feature_data` — correct column values written to `proc_df`
1. `_WidgetPreprocessing.forward` — correct tensor shape for a known input
1. `WidgetInputFeature.forward` — correct output keys and shapes with a random input
1. Encoder round-trip via `create_sample_input`
```python
import torch
import pytest
from tests.integration_tests.utils import generate_data, run_api_test
def test_widget_preprocessing_forward():
meta = {}
module = _WidgetPreprocessing(meta, preprocessing_config=None)
out = module(torch.zeros(4))
assert out.shape == (4, 1)
```
______________________________________________________________________
## Checklist
- [ ] `ludwig/constants.py` — add `WIDGET = "widget"`
- [ ] `ludwig/schema/features/widget_feature.py` — schema classes + registry decorators
- [ ] `ludwig/schema/features/preprocessing/` — preprocessing config class (or reuse existing)
- [ ] `ludwig/features/widget_feature.py` — preprocessing module, mixin, input/output feature classes
- [ ] `ludwig/features/feature_registries.py` — add to `get_base_type_registry`, `get_input_type_registry`, optionally `get_output_type_registry`
- [ ] `tests/ludwig/features/test_widget_feature.py` — unit tests for preprocessing and forward pass
______________________________________________________________________
## Common pitfalls
**`proc_df[PROC_COLUMN]` vs `proc_df[COLUMN]`** — always write to `PROC_COLUMN` (the internal column name), not `COLUMN` (the raw user column name). They can differ when the user renames features.
**`get_preprocessing_module` vs `add_feature_data`** — `add_feature_data` runs in Python at dataset preparation time (CPU, pandas). `get_preprocessing_module` returns a `torch.nn.Module` that runs inside the model graph at inference time. Both must produce compatible representations.
**`input_shape` vs `output_shape`** — `InputFeature.input_shape` is the shape of the *raw preprocessed* tensor going into the encoder. `InputFeature.output_shape` is the encoder's output shape that feeds into the combiner. Return `self.encoder_obj.output_shape` for the latter.
**Registry order matters** — the registry in `feature_registries.py` is read at import time. If you import your feature class before `feature_registries.py` is loaded, the registry will be empty. The correct order is always: define constants → define schema → define feature → add to registry.
**Schema `type` field** — always use `schema_utils.ProtectedString(WIDGET)` not `str = WIDGET`. The protected string raises an error if a user tries to override it in their config YAML, which prevents subtle type mismatches.