Skip to content

Commit b3bdcff

Browse files
committed
add rules, workflows and skills
1 parent 589ced5 commit b3bdcff

6 files changed

Lines changed: 435 additions & 0 deletions

File tree

.agent/rules.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Project Rules for keras_cv_attention_models
2+
3+
## Code Style
4+
- Line length: 160 characters (`black -l 160`)
5+
- `__init__.py` files are excluded from black formatting
6+
- Use single-line multi-assignment for related vars: `self.a, self.b = a, b`
7+
8+
## Model Architecture Patterns
9+
- Model variant files (e.g., `vit5.py`) should be **thin wrappers** calling the base function (e.g., `Beit`)
10+
- Use `@register_model` decorator for every model variant function
11+
- Use `kwargs.pop("kwargs", None)` at the start of base model functions to handle nested kwargs
12+
- Model names in code use **snake_case** (e.g., `vit5_small_patch16`), class-style names for functions (e.g., `ViT5_Small_Patch16`)
13+
- Default `pretrained="imagenet"` (or appropriate tag) once weights are available
14+
15+
## PyTorch Backend (CRITICAL)
16+
- Any layer storing tensors as attributes (not via `add_weight`) **must** call `self.register_buffer()` in `__init__` with `if hasattr(self, "register_buffer")` guard
17+
- Prefer NumPy operations in `build()` for one-time tensor computation, then convert with `functional.convert_to_tensor()`
18+
- Always verify predictions match between TF and PyTorch backends after changes
19+
20+
## Documentation
21+
- Every model family needs: paper link, GitHub link, model table with Params/FLOPs/Input/Top1 Acc/Download
22+
- Document all parameters in `__class_tail_doc__` with model-specific defaults noted (e.g., "Default True for ViT-5, False for others")
23+
- Download links: `https://github.com/leondgarse/keras_cv_attention_models/releases/download/{sub_release}/{filename}.h5`
24+
25+
## Testing
26+
- Every model needs both `test_ModelFamily_defination` and `test_ModelFamily_predict` in `tests/test_models.py`
27+
28+
## Modifying Existing Code
29+
- **Preserve existing patterns**: When refactoring a layer, study the original implementation's design choices (e.g., `blocks_shape` for dimension-agnostic reshaping) before replacing them with new approaches. Reuse proven patterns rather than inventing alternatives.
30+
- **Use proper APIs, not workarounds**: For PyTorch backend, use `register_buffer()` — don't set internal flags like `use_layer_as_module` directly. Always prefer the idiomatic solution over hacking internals.
31+
- **Run existing tests first**: Before modifying a layer, run its existing test (`test_layers.py`) to understand the input/output contract. Don't change behavior without checking what the tests expect.
32+
- **`build()` should use numpy**: Computations in `build()` are one-time setup. Use `np.tile`, `np.sin`, `np.concatenate` etc., then convert the final result with `functional.convert_to_tensor()`. Don't use `functional.tile` or other framework ops for build-time constants.
33+
- **Consolidate related operations**: When a helper function (e.g., `_get_sin_cos`) always has the same post-processing (e.g., tiling by `num_heads`), fold that into the helper rather than leaving it in the caller.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
name: Weight Conversion
3+
description: Converting PyTorch model weights to Keras h5 format for keras_cv_attention_models
4+
---
5+
6+
# Weight Conversion Skill
7+
8+
Convert pretrained PyTorch weights to Keras h5 format.
9+
10+
The core task is **aligning the weight name order** between torch and keras models. `download_and_load.keras_reload_from_torch_model` is a convenience helper that automates this, but direct manual conversion is also fine.
11+
12+
## Pipeline Overview
13+
14+
1. **Weight collection** (`state_dict_stack_by_layer`): Groups torch `state_dict` entries by layer name (splitting on `.`), filtering via `skip_weights` and `unstack_weights`.
15+
2. **Name alignment** (`align_layer_names_multi_stage`): Reorders keras layer names to match torch weight order.
16+
3. **Weight transfer** (`keras_reload_stacked_state_dict`): Applies standard transforms (Conv2D/Dense transpose, etc.) plus custom `additional_transfer` overrides, then saves.
17+
18+
## Parameter Reference
19+
20+
### Weight Collection
21+
| Parameter | Purpose |
22+
|-----------|---------|
23+
| `skip_weights` | Weight name suffixes to drop (e.g., `["num_batches_tracked", "relative_position_index"]`) |
24+
| `unstack_weights` | Weights kept as individual entries instead of grouped with their layer (e.g., `["cls_token", "pos_embed", "gamma_1"]`) |
25+
26+
### Name Alignment (order matching)
27+
| Parameter | Purpose |
28+
|-----------|---------|
29+
| `tail_align_dict` | Reposition layers by tail name: `{tail_name: offset}`. Negative offset moves layer earlier. Can be scoped by stack: `{"stack3": {"attn_gamma": -6}}` |
30+
| `full_name_align_dict` | Reposition by exact name: value can be negative offset, absolute position, or another layer's name string |
31+
| `tail_split_position` | Where to split name into head/tail (default `2`). E.g., `1` → head=`stack1`, tail=`attn_gamma` |
32+
| `specific_match_func` | Function returning the complete ordered name list, bypassing all alignment logic. Use for complex cases where dicts can't express the mapping |
33+
34+
### Weight Transfer
35+
| Parameter | Purpose |
36+
|-----------|---------|
37+
| `additional_transfer` | Custom transforms: `{LayerClass: lambda ww: [...]}` or `{"name_suffix": lambda ww: [...]}`. Applied after default Conv2D/Dense transposes |
38+
39+
## Workflow
40+
41+
1. Create keras model with `pretrained=None, classifier_activation=None`
42+
2. Run with `do_convert=False` first to inspect both name lists
43+
3. Compare printed torch/keras weight lists — find misalignments
44+
4. Configure alignment parameters to fix ordering
45+
5. Run with `do_convert=True` — it predicts with both models and prints results
46+
6. Verify top prediction matches (usually `Egyptian_cat` for the cat test image)
47+
7. `md5sum output.h5` → add hash to `PRETRAINED_DICT`
48+
8. Upload to GitHub releases. Notify user if cannot upload directly.
49+
50+
## Troubleshooting
51+
52+
- **Shape mismatch**: Dense/Conv transposes are automatic; check if combined QKV needs `unstack_weights`
53+
- **Name ordering wrong**: Use `do_convert=False` to see lists side-by-side; adjust offsets or use `specific_match_func` for full control
54+
- **Predictions don't match**: Check `rescale_mode` in `add_pre_post_process()`, `classifier_activation`, or intermediate layer outputs

.agent/workflows/port-new-model.md

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
---
2+
description: How to port a new model to keras_cv_attention_models
3+
---
4+
5+
# Porting a New Model
6+
7+
This workflow covers adding a new model architecture (vision or LLM) to `keras_cv_attention_models`.
8+
9+
## 1. Research the Source Model
10+
11+
1. Read the paper and reference implementation (usually PyTorch from GitHub).
12+
2. Identify which **existing model family** it's closest to:
13+
- **Vision**: ViT-5 → `Beit`, EVA02 → `Beit`, DINOv2 → `Beit`, etc.
14+
- **LLM**: LLaMA3 → `LLaMA2`, etc.
15+
3. Note key architectural differences: new layers, attention mechanisms, normalization, positional encoding.
16+
4. Check if required building blocks already exist in `attention_layers/` (e.g., `RMSNorm`, `PositionalEncodingFourierRot`, `ClassToken`, `CausalMask`).
17+
18+
## 2. Implement New Components
19+
20+
1. If new layers are needed, add them to the appropriate file in `attention_layers/`.
21+
2. **Reuse existing layers** whenever possible — check `attention_layers/__init__.py` for available components.
22+
3. If modifying existing layers (e.g., adding parameters to `PositionalEncodingFourierRot`):
23+
- Ensure backward compatibility — existing models using the layer must still work.
24+
- Add new parameters with defaults matching the old behavior.
25+
- Update `get_config()` to include new parameters.
26+
27+
> [!IMPORTANT]
28+
> **PyTorch backend compatibility**: Any layer that stores tensors as plain attributes (not via `add_weight`) must call `self.register_buffer()` in `__init__` for PyTorch backend support. Without this, the layer becomes a no-op identity in the PyTorch computation graph. See the PyTorch backend workflow for details.
29+
30+
## 3. Create the Model Variant File
31+
32+
Models that share a base architecture should be **thin wrappers**:
33+
34+
**Vision model** (see `beit/vit5.py`, `beit/eva02.py`):
35+
```python
36+
from keras_cv_attention_models.beit.beit import Beit
37+
from keras_cv_attention_models.models import register_model
38+
39+
def ModelFamily(model_name="model_family", **kwargs):
40+
kwargs.pop("kwargs", None)
41+
return Beit(**locals(), **kwargs)
42+
43+
@register_model
44+
def ModelFamily_Small(input_shape=(224, 224, 3), num_classes=1000, activation="gelu",
45+
classifier_activation="softmax", pretrained="imagenet", **kwargs):
46+
embed_dim = 384
47+
num_heads = 6
48+
return ModelFamily(**locals(), model_name="model_family_small", **kwargs)
49+
```
50+
51+
**LLM model** (see `llama2/llama2.py`):
52+
```python
53+
from keras_cv_attention_models.llama2.llama2 import LLaMA2
54+
from keras_cv_attention_models.models import register_model
55+
56+
@register_model
57+
def LLaMA3_8B(max_block_size=8192, vocab_size=128256, include_top=True,
58+
activation="swish", pretrained=None, **kwargs):
59+
num_blocks = 32
60+
embedding_size = 4096
61+
num_heads = 32
62+
num_kv_heads = 8
63+
return LLaMA2(**locals(), model_name="llama3_8b", **kwargs)
64+
```
65+
66+
Key rules:
67+
- Use `@register_model` decorator for each variant.
68+
- Set `pretrained` default appropriately once weights are available.
69+
- Use `kwargs.pop("kwargs", None)` to handle nested kwargs.
70+
- Model names use **snake_case** (e.g., `vit5_small_patch16`, `llama2_42m`).
71+
72+
## 4. Register in `__init__.py`
73+
74+
1. Import the new model and variant functions.
75+
2. Add a `__head_doc__` string with paper link and GitHub link.
76+
3. Add a `__tail_doc__` for common parameters:
77+
- **Vision**: `input_shape`, `num_classes`, `classifier_activation`, `pretrained`, plus model-specific params.
78+
- **LLM**: `vocab_size`, `max_block_size`, `include_top`, `dropout`, `activation`, `pretrained`.
79+
4. Add model architecture table (Params, FLOPs, and model-specific columns).
80+
81+
## 5. Weight Conversion
82+
83+
1. Add entries to `PRETRAINED_DICT` in the base model file:
84+
```python
85+
# Vision: {model_name: {pretrained_tag: {resolution: md5}}}
86+
"vit5_small_patch16": {"imagenet": {224: "md5_hash_here"}},
87+
# LLM: {model_name: {pretrained_tag: md5}} (no resolution key)
88+
"llama2_42m": {"tiny_stories": "md5_hash_here"},
89+
```
90+
2. Convert weights using the appropriate method:
91+
- **Vision**: `download_and_load.keras_reload_from_torch_model()` or a model-specific helper.
92+
- **LLM**: `convert_huggingface_weights_to_h5()` or direct state_dict loading.
93+
3. After conversion, verify:
94+
```python
95+
# Vision
96+
mm = kecam.beit.ViT5_Small_Patch16(pretrained="path/to/converted.h5")
97+
pred = mm(mm.preprocess_input(kecam.test_images.cat()))
98+
print(mm.decode_predictions(pred)[0][0]) # ('n02124075', 'Egyptian_cat', 0.84...)
99+
100+
# LLM
101+
mm = kecam.llama2.LLaMA2_42M(pretrained="path/to/converted.h5")
102+
print(mm.run_prediction("A long time ago,", top_k=1, max_new_tokens=5))
103+
```
104+
4. Compute the md5 hash: `md5sum converted_model.h5`
105+
5. Upload to GitHub releases under the appropriate sub-release tag. Notice user if cannot upload directly.
106+
107+
## 6. Documentation
108+
109+
1. **Model-specific `README.md`** (e.g., `beit/README.md`, `llama2/README.md`):
110+
- Add paper reference in the Summary section.
111+
- Add model table with Params, FLOPs, and model-specific metrics.
112+
- Download links: `[filename.h5](https://github.com/leondgarse/keras_cv_attention_models/releases/download/{sub_release}/{filename}.h5)`
113+
2. **Root `README.md`**: Add entry to Table of Contents in alphabetical order.
114+
3. **`__init__.py`**: Ensure all parameters are documented with model-specific defaults noted.
115+
116+
## 7. Testing
117+
118+
Add tests in `tests/test_models.py`:
119+
120+
**Vision model**:
121+
```python
122+
def test_ModelFamily_defination():
123+
mm = keras_cv_attention_models.beit.ModelFamily_Small(pretrained=None)
124+
assert isinstance(mm, models.Model)
125+
mm = keras_cv_attention_models.beit.ModelFamily_Small(pretrained=None, num_classes=0)
126+
assert isinstance(mm, models.Model)
127+
128+
def test_ModelFamily_predict():
129+
mm = keras_cv_attention_models.beit.ModelFamily_Small(pretrained="imagenet")
130+
pred = mm(mm.preprocess_input(cat()))
131+
out = mm.decode_predictions(pred)[0][0]
132+
assert out[1] == "Egyptian_cat"
133+
```
134+
135+
**LLM model**:
136+
```python
137+
def test_LLM_run_prediction():
138+
mm = keras_cv_attention_models.llama2.LLaMA2_42M(pretrained="tiny_stories")
139+
generated = mm.run_prediction("A long time ago,", top_k=1, max_new_tokens=5)
140+
assert generated == " there was a little girl"
141+
```
142+
143+
If new layers were added, add layer tests in `tests/test_layers.py`.
144+
145+
## 8. Format and Verify
146+
147+
// turbo-all
148+
149+
1. Run black formatter:
150+
```sh
151+
find ./* -name "*.py" | grep -v __init__ | xargs -I {} black -l 160 {}
152+
```
153+
2. Run TF test suite:
154+
```sh
155+
CUDA_VISIBLE_DEVICES='-1' pytest -vv --durations=0 ./tests
156+
```
157+
3. Run PyTorch test suite:
158+
```sh
159+
CUDA_VISIBLE_DEVICES='-1' KECAM_BACKEND='torch' pytest -vv --durations=0 ./tests/test_models.py
160+
```
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
description: PyTorch backend compatibility rules and gotchas for keras_cv_attention_models
3+
---
4+
5+
# PyTorch Backend Compatibility
6+
7+
The `pytorch_backend/` provides a custom Keras-like API on top of PyTorch. It uses a graph-based execution model where layers build a `GraphNode` computation graph during model construction, then execute it in `Model.forward()`. **This has several critical gotchas.**
8+
9+
## Critical Rule: Layers with Non-Parameter Tensors
10+
11+
**Problem**: In the PyTorch backend's `Layer.forward()` (`pytorch_backend/layers.py`), when a `GraphNode` input is encountered, the node's `callable` is set based on `self.use_layer_as_module`:
12+
13+
```python
14+
if self.use_layer_as_module: # Layer has weights → callable = self (invokes call())
15+
cur_node.callable = self
16+
else: # No weights → callable = self.module (identity lambda!)
17+
cur_node.callable = self.module # ← self.module = lambda xx: xx
18+
```
19+
20+
`use_layer_as_module` is set to `True` only when `add_weight()` is called. **If a layer stores tensors as plain attributes without `add_weight()`, it becomes a silent no-op (identity function) in the PyTorch computation graph.**
21+
22+
**Solution**: Use `register_buffer()` in `__init__` for any tensors that are not trainable parameters:
23+
24+
```python
25+
def __init__(self, **kwargs):
26+
super().__init__(**kwargs)
27+
if hasattr(self, "register_buffer"): # PyTorch backend only
28+
self.register_buffer("my_tensor", None, persistent=False)
29+
# Initialize as None, assign actual value in build()
30+
```
31+
32+
This ensures:
33+
1. The layer is registered as a PyTorch submodule → `use_layer_as_module = True`
34+
2. Tensors are automatically moved to the correct device with `.to(device)`
35+
3. The layer's `call()` method is actually invoked during forward pass
36+
37+
> [!CAUTION]
38+
> This is a **silent** failure — the model will run without errors but produce wrong predictions. The only symptom is degraded accuracy.
39+
40+
## Diagnosing PyTorch Backend Issues
41+
42+
### Symptom: Prediction mismatch between TF and PyTorch backends
43+
44+
1. **Compare intermediate outputs** layer by layer:
45+
```python
46+
# Enable debug mode to see layer-by-layer execution
47+
mm.set_debug(True)
48+
```
49+
50+
2. **Check if a layer is identity** by inspecting node callables:
51+
```python
52+
for node in mm.forward_pipeline:
53+
if 'layer_name' in node.name:
54+
print(type(node.callable).__name__) # 'function' = identity lambda
55+
test_input = torch.randn(1, 197, 384)
56+
print(torch.allclose(test_input, node.callable(test_input))) # True = no-op!
57+
```
58+
59+
3. **Write a comparison script** that runs both backends on the same input and compares named layer outputs.
60+
61+
### Common Sources of Mismatch
62+
63+
| Issue | Cause | Fix |
64+
|-------|-------|-----|
65+
| Layer is no-op | No `add_weight()` call → identity callable | Use `register_buffer()` in `__init__` |
66+
| Device mismatch | `convert_to_tensor()` creates CPU tensors | Use `register_buffer()` for auto device placement |
67+
| Shape mismatch | `functional.reshape` with hardcoded dims | Use `functional.shape(x)` or `blocks_shape` pattern for dimension-agnostic code |
68+
| Split behavior | `math.ceil` in integer splits | Check `pytorch_backend/functional.py:split()` |
69+
70+
## Dimension-Agnostic Layer Design
71+
72+
Layers should support both 3D `[batch, tokens, channels]` and 4D `[batch, heads, tokens, channels]` inputs. Use the `blocks_shape` pattern:
73+
74+
```python
75+
def build(self, input_shape):
76+
num_tokens = input_shape[-2]
77+
self.blocks_shape = [*input_shape[1:-2], num_tokens] # Captures intermediate dims
78+
79+
def _process(self, inputs):
80+
# Use blocks_shape for reshape — preserves batch and any intermediate dims
81+
x = functional.reshape(inputs, [-1, *self.blocks_shape, self.channels // 2, 2])
82+
...
83+
return functional.reshape(result, (-1, *self.blocks_shape, self.channels))
84+
```
85+
86+
## Testing Both Backends
87+
88+
Always run tests on both backends:
89+
90+
```sh
91+
# TF backend (default)
92+
CUDA_VISIBLE_DEVICES='-1' pytest -vv --durations=0 ./tests
93+
94+
# PyTorch backend
95+
CUDA_VISIBLE_DEVICES='-1' KECAM_BACKEND='torch' pytest -vv --durations=0 ./tests/test_models.py
96+
```
97+
98+
## NumPy vs functional Operations in build()
99+
100+
Prefer **NumPy** operations in `build()` for tensors that are computed once and stored (e.g., positional encodings). This avoids backend-specific issues with `functional.tile()`, `functional.convert_to_tensor()`, etc.:
101+
102+
```python
103+
# Good: compute in numpy, convert once
104+
pos_sin = np.tile(np.sin(grid), [1, 1, num_heads])
105+
self.pos_sin = functional.convert_to_tensor(pos_sin, dtype=self.compute_dtype)
106+
107+
# Risky: functional.tile() may behave differently across backends
108+
self.pos_sin = functional.tile(functional.convert_to_tensor(np.sin(grid)), [1, 1, num_heads])
109+
```

.agent/workflows/run-tests.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
description: How to run tests and formatting for keras_cv_attention_models
3+
---
4+
5+
# Running Tests and Formatting
6+
7+
// turbo-all
8+
9+
## 1. Format Code with Black
10+
11+
```sh
12+
find ./* -name "*.py" | grep -v __init__ | xargs -I {} black -l 160 {}
13+
```
14+
15+
Note: `__init__.py` files are excluded from formatting.
16+
17+
## 2. Run TF Backend Tests
18+
19+
```sh
20+
CUDA_VISIBLE_DEVICES='-1' pytest -vv --durations=0 ./tests
21+
```
22+
23+
This runs all tests: `test_models.py`, `test_layers.py`, `test_models_tf.py`, `test_switch_to_deploy_tf.py`.
24+
25+
## 3. Run PyTorch Backend Tests
26+
27+
```sh
28+
CUDA_VISIBLE_DEVICES='-1' KECAM_BACKEND='torch' pytest -vv --durations=0 ./tests/test_models.py
29+
```
30+
31+
Only `test_models.py` is run for PyTorch backend.
32+
33+
## Notes
34+
35+
- These tests are also executed in GitHub Actions (`.github/workflows/publish-to-test-pypi.yml`), so running locally is not compulsory but recommended before pushing.
36+
- TF tests take ~14 minutes on CPU, PyTorch tests take ~2.5 minutes.
37+
- Use `CUDA_VISIBLE_DEVICES='-1'` to force CPU execution for reproducible results.
38+
- To run a single test: `pytest tests/test_models.py::test_ViT5_predict -xvs`

0 commit comments

Comments
 (0)