Instructions to use ZibinDong/ActionCodec2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ZibinDong/ActionCodec2 with Transformers:
# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ZibinDong/ActionCodec2", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Publish audited ActionCodec2 release files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +46 -96
- actioncodec2-0.1.1.tar.gz → actioncodec2-0.3.0.tar.gz +2 -2
- actioncodec2_kernels/__init__.py +0 -1
- actioncodec2_kernels/bpe.cpp +428 -0
- actioncodec2_kernels/common.hpp +139 -0
- actioncodec2_kernels/grammar.cpp +243 -126
- actioncodec2_kernels/inference.cpp +245 -0
- actioncodec2_kernels/module.cpp +63 -25
- actioncodec2_kernels/parallel.hpp +5 -1
- actioncodec2_kernels/physical.cpp +30 -86
- actioncodec2_kernels/preprocessing.cpp +281 -0
- actioncodec2_kernels/quaternion.hpp +1 -1
- actioncodec2_kernels/reconstruct.cpp +0 -146
- actioncodec2_kernels/repacking.hpp +437 -0
- actioncodec2_kernels/replay.cpp +160 -0
- actioncodec2_kernels/resample.cpp +58 -30
- actioncodec2_kernels/setbpe.cpp +63 -46
- actioncodec2_kernels/so3.cpp +62 -22
- actioncodec2_kernels/sparse.cpp +1308 -0
- actioncodec2_kernels/spatial.cpp +297 -0
- actioncodec2_kernels/temporal.cpp +799 -0
- actioncodec2_kernels/token_streams.hpp +48 -0
- config.json +4 -1
- processing_actioncodec2.py +30 -32
- pyproject.toml +0 -13
- requirements.txt +0 -6
- runtime/__init__.py +1 -1
- runtime/backend/__init__.py +0 -1
- runtime/backends/__init__.py +0 -0
- runtime/backends/actioncodec2/__init__.py +8 -0
- runtime/{physical → backends/actioncodec2}/_so3.py +33 -3
- runtime/{common → backends/actioncodec2}/_validation.py +4 -10
- runtime/backends/actioncodec2/assets/presets/dual_eef_abs.yaml +20 -0
- runtime/backends/actioncodec2/assets/presets/dual_eef_delta.yaml +20 -0
- runtime/backends/actioncodec2/assets/presets/dual_joint6_abs.yaml +18 -0
- runtime/backends/actioncodec2/assets/presets/dual_joint6_delta.yaml +16 -0
- runtime/backends/actioncodec2/assets/presets/dual_joint7_abs.yaml +18 -0
- runtime/backends/actioncodec2/assets/presets/dual_joint7_delta.yaml +16 -0
- runtime/backends/actioncodec2/assets/presets/single_eef_abs.yaml +14 -0
- runtime/backends/actioncodec2/assets/presets/single_eef_delta.yaml +14 -0
- runtime/backends/actioncodec2/assets/presets/single_joint6_abs.yaml +13 -0
- runtime/backends/actioncodec2/assets/presets/single_joint6_abs_no_gripper.yaml +12 -0
- runtime/backends/actioncodec2/assets/presets/single_joint6_delta.yaml +12 -0
- runtime/backends/actioncodec2/assets/presets/single_joint7_abs.yaml +13 -0
- runtime/backends/actioncodec2/assets/presets/single_joint7_delta.yaml +12 -0
- runtime/backends/actioncodec2/assets/profiles/eef.yaml +74 -0
- runtime/backends/actioncodec2/assets/profiles/eef_second_order.yaml +80 -0
- runtime/backends/actioncodec2/assets/profiles/joint.yaml +52 -0
- runtime/backends/actioncodec2/assets/profiles/joint_second_order.yaml +62 -0
- runtime/backends/actioncodec2/batch_semantics.py +269 -0
README.md
CHANGED
|
@@ -9,32 +9,16 @@ tags:
|
|
| 9 |
|
| 10 |
# ActionCodec2
|
| 11 |
|
| 12 |
-
|
| 13 |
-
|
|
|
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
- **Paper edition**: `paper-1st` and `paper-2nd` are the first- and second-order tokenizers
|
| 18 |
-
used in the paper. Both use **5 mm translation precision** and a **4,096-token budget
|
| 19 |
-
per profile**, with joint and EEF profiles.
|
| 20 |
-
- **Extended edition**: tokenizers retrained after additional cleaning and filtering of
|
| 21 |
-
the pretraining data. They offer first and second order, **1/5/10 mm translation
|
| 22 |
-
precision**, and **1k–16k token budgets per profile** for a wider range of precision
|
| 23 |
-
and vocabulary requirements. The 1 mm tier starts at 2k; its 2k variants contain
|
| 24 |
-
EEF profiles only.
|
| 25 |
-
|
| 26 |
-
Precision labels describe translation quantization settings. Rotation and joint settings
|
| 27 |
-
are specified separately in each profile's configuration. Paper and Extended variants
|
| 28 |
-
have separately trained vocabularies, including when their precision and budget match.
|
| 29 |
-
The six released VLA policies use the Paper edition and must keep their original variant.
|
| 30 |
|
| 31 |
## Tokenizer usage
|
| 32 |
|
| 33 |
-
Select a variant explicitly to load its configuration and fitted assets together:
|
| 34 |
-
|
| 35 |
```bash
|
| 36 |
-
|
| 37 |
-
pip install numpy scipy torch "transformers>=4.57,<6" huggingface-hub pyyaml
|
| 38 |
```
|
| 39 |
|
| 40 |
```python
|
|
@@ -42,50 +26,46 @@ import numpy as np
|
|
| 42 |
from transformers import AutoProcessor
|
| 43 |
|
| 44 |
codec = AutoProcessor.from_pretrained(
|
| 45 |
-
"ZibinDong/ActionCodec2", variant="paper-1st",
|
| 46 |
-
|
| 47 |
)
|
| 48 |
-
actions = np.zeros((
|
| 49 |
tokens = codec.encode(actions, fps=15)
|
| 50 |
-
|
| 51 |
-
codec.save_pretrained("./saved-tokenizer")
|
| 52 |
-
restored = AutoProcessor.from_pretrained(
|
| 53 |
-
"./saved-tokenizer", trust_remote_code=True, local_files_only=True,
|
| 54 |
-
)
|
| 55 |
```
|
| 56 |
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
```python
|
| 67 |
-
codec
|
| 68 |
-
|
| 69 |
-
revision="v0.1.1",
|
| 70 |
-
)
|
| 71 |
-
# Use a zero template only when the preceding motion is known to be zero.
|
| 72 |
-
boundary = codec.first_order_template(batch_size=1)
|
| 73 |
-
tokens = codec.encode(actions, fps=15, previous_first_order=boundary)
|
| 74 |
-
decoded = codec.decode(tokens, fps=15, previous_first_order=boundary)
|
| 75 |
-
boundary = codec.final_first_order(tokens, previous_first_order=boundary, executed_steps=None)
|
| 76 |
```
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
the physical input convention. Absolute layouts require `current_state` for reconstruction.
|
| 84 |
-
The 1mm/2k variants contain EEF profiles only.
|
| 85 |
|
| 86 |
## Available variants
|
| 87 |
|
| 88 |
-
| Variant | Edition | Order | Translation precision |
|
| 89 |
| --- | --- | --- | --- | --- | --- |
|
| 90 |
| `paper-1st` | Paper | 1 | 5mm | 4096 | joint, eef |
|
| 91 |
| `paper-2nd` | Paper | 2 | 5mm | 4096 | joint, eef |
|
|
@@ -117,52 +97,22 @@ The 1mm/2k variants contain EEF profiles only.
|
|
| 117 |
| `extended-2nd-10mm-4k` | Extended | 2 | 10mm | 4096 | joint, eef |
|
| 118 |
| `extended-2nd-10mm-8k` | Extended | 2 | 10mm | 8192 | joint, eef |
|
| 119 |
| `extended-2nd-10mm-16k` | Extended | 2 | 10mm | 16384 | joint, eef |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
asset hashes and its previous repository ID. Loading downloads shared files plus the
|
| 125 |
-
selected variant, and verifies its assets. `subfolder` is not a variant selector.
|
| 126 |
-
|
| 127 |
-
Saved processors contain only the selected assets and the complete portable runtime.
|
| 128 |
-
They load offline and preserve `artifact_manifest.json` with their asset hashes and origin.
|
| 129 |
-
Select a different variant by loading the catalog again.
|
| 130 |
-
|
| 131 |
-
## Native acceleration
|
| 132 |
-
|
| 133 |
-
Build the optional C++17 kernels once for all variants:
|
| 134 |
-
|
| 135 |
-
```bash
|
| 136 |
-
hf download ZibinDong/ActionCodec2 --revision v0.1.1 --include "setup.py" "pyproject.toml" "LICENSE" "actioncodec2_kernels/*" --local-dir ./actioncodec2-kernels
|
| 137 |
-
pip install ./actioncodec2-kernels
|
| 138 |
-
```
|
| 139 |
-
|
| 140 |
-
`ACTIONCODEC2_NUM_THREADS` controls the per-process thread budget.
|
| 141 |
-
`ACTIONCODEC2_NO_NATIVE=1` selects the Python reference implementation.
|
| 142 |
-
|
| 143 |
-
## VLA inference package
|
| 144 |
-
|
| 145 |
-
The versioned source archive `actioncodec2-0.1.1.tar.gz` contains policy inference code,
|
| 146 |
-
native kernels, training recipes, and evaluation scripts. Use **Python 3.12 or later**
|
| 147 |
-
and a C++17 compiler. Inference has been checked with Python 3.12, PyTorch 2.7.1,
|
| 148 |
-
and Transformers 5.14.1.
|
| 149 |
-
|
| 150 |
-
```bash
|
| 151 |
-
pip install "actioncodec2[cuda] @ https://hf-mirror.com/ZibinDong/ActionCodec2/resolve/v0.1.1/actioncodec2-0.1.1.tar.gz"
|
| 152 |
-
```
|
| 153 |
|
| 154 |
-
|
| 155 |
-
AC2 variant and text tokenizer; once downloaded, inference works offline without
|
| 156 |
-
another model or tokenizer repository. Each policy's README describes its inputs.
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
| DROID | [First order](https://hf-mirror.com/ZibinDong/G05_ActionCodec2-1st_DROID) | [Second order](https://hf-mirror.com/ZibinDong/G05_ActionCodec2-2nd_DROID) |
|
| 163 |
|
| 164 |
## License
|
| 165 |
|
| 166 |
-
|
| 167 |
-
also includes G0.5 components under `LICENSE-G0.5`. G0.5-derived policy weights use the
|
| 168 |
-
G0.5 Community License; see the individual policy cards and the archive's license files.
|
|
|
|
| 9 |
|
| 10 |
# ActionCodec2
|
| 11 |
|
| 12 |
+
- **Paper edition**: first- and second-order tokenizers used in the paper; 5 mm translation precision and 4,096 tokens per profile.
|
| 13 |
+
- **Extended edition**: retrained after additional pretraining-data cleaning and filtering, with more precision and vocabulary options.
|
| 14 |
+
- **Sparse version**: standard and high-dynamic first-order tokenizers, each with EEF and joint profiles and binary (`2bin`) or 21-level (`21bin`) grippers.
|
| 15 |
|
| 16 |
+
All 34 variants use one shared runtime. Precision labels describe translation quantization; rotation and joint limits are in each profile's configuration.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
## Tokenizer usage
|
| 19 |
|
|
|
|
|
|
|
| 20 |
```bash
|
| 21 |
+
pip install numpy scipy torch "transformers>=5.2,<6" huggingface-hub pyyaml
|
|
|
|
| 22 |
```
|
| 23 |
|
| 24 |
```python
|
|
|
|
| 26 |
from transformers import AutoProcessor
|
| 27 |
|
| 28 |
codec = AutoProcessor.from_pretrained(
|
| 29 |
+
"ZibinDong/ActionCodec2", variant="paper-1st",
|
| 30 |
+
action_space="single_eef_delta", trust_remote_code=True,
|
| 31 |
)
|
| 32 |
+
actions = np.zeros((1, 16, 7), dtype=np.float32)
|
| 33 |
tokens = codec.encode(actions, fps=15)
|
| 34 |
+
restored = codec.decode(tokens, fps=15)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
```
|
| 36 |
|
| 37 |
+
Use `variant="sparse-standard-2bin"` or `"sparse-high-dynamic-21bin"` for Sparse;
|
| 38 |
+
Sparse decoding requires `horizon=actions.shape[-2]` in source frames.
|
| 39 |
+
Each profile has its own token namespace; the budget is per profile, not the total routed vocabulary.
|
| 40 |
+
|
| 41 |
+
The EEF layout is `[x, y, z, rx, ry, rz, gripper]`, in metres and radians.
|
| 42 |
+
Delta rotations use `R_next = R_previous @ Exp(rotvec)`. Grippers are absolute open fractions (0 closed, 1 open).
|
| 43 |
+
Inputs are `(T,D)` or `(B,T,D)` and are resampled to 15 Hz. Reconstruction is lossy.
|
| 44 |
+
Absolute layouts also require `current_state`; second-order variants require
|
| 45 |
+
`previous_first_order` in canonical 15 Hz increments in both encode and decode.
|
| 46 |
+
Zero boundaries are valid only when the preceding motion is known to be zero.
|
| 47 |
+
Use `final_first_order` to carry reconstructed second-order boundaries between chunks.
|
| 48 |
|
| 49 |
+
`codec.print_action_spaces()` lists the layouts. Dual arms are interleaved by time;
|
| 50 |
+
BPE tokens never merge across arms. The historical `single_joint6_abs_continuous_gripper`
|
| 51 |
+
name aliases `single_joint6_abs_no_gripper`: six joint coordinates, no separate gripper slot.
|
| 52 |
+
|
| 53 |
+
## Train tokenizers
|
| 54 |
|
| 55 |
```python
|
| 56 |
+
codec.fit(episodes, fps=15)
|
| 57 |
+
codec.save_pretrained("./my-tokenizer")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
```
|
| 59 |
|
| 60 |
+
Cleaning is off by default, including VLA training and inference. Only tokenizer fitting
|
| 61 |
+
accepts `apply_preprocessing=True`, with a recorded or explicitly supplied `preprocessing`
|
| 62 |
+
configuration. It acts on continuous canonical trajectories before quantization.
|
| 63 |
+
Split recording discontinuities first; second-order cleaning needs `bootstrap_context=True`.
|
| 64 |
+
Saved cleaning settings do not enable filtering during encode or decode.
|
|
|
|
|
|
|
| 65 |
|
| 66 |
## Available variants
|
| 67 |
|
| 68 |
+
| Variant | Edition | Order | Translation precision | Budget / profile | Profiles |
|
| 69 |
| --- | --- | --- | --- | --- | --- |
|
| 70 |
| `paper-1st` | Paper | 1 | 5mm | 4096 | joint, eef |
|
| 71 |
| `paper-2nd` | Paper | 2 | 5mm | 4096 | joint, eef |
|
|
|
|
| 97 |
| `extended-2nd-10mm-4k` | Extended | 2 | 10mm | 4096 | joint, eef |
|
| 98 |
| `extended-2nd-10mm-8k` | Extended | 2 | 10mm | 8192 | joint, eef |
|
| 99 |
| `extended-2nd-10mm-16k` | Extended | 2 | 10mm | 16384 | joint, eef |
|
| 100 |
+
| `sparse-standard-2bin` | Sparse | 1 | 5mm | 4096 | eef, joint |
|
| 101 |
+
| `sparse-standard-21bin` | Sparse | 1 | 5mm | 4096 | eef, joint |
|
| 102 |
+
| `sparse-high-dynamic-2bin` | Sparse | 1 | 5mm | 4096 | eef, joint |
|
| 103 |
+
| `sparse-high-dynamic-21bin` | Sparse | 1 | 5mm | 4096 | eef, joint |
|
| 104 |
|
| 105 |
+
Pin `revision` for reproducibility. `variants.json` records asset hashes; a selected
|
| 106 |
+
`save_pretrained()` directory contains its assets and the shared portable runtime.
|
| 107 |
+
The optional C++17 kernels can be built with `pip install ./downloaded-tokenizer`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
+
## VLA policies
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
Installation, paper-policy evaluation and VLA training: [source repository](https://github.com/ZibinDong/actioncodec2).
|
| 112 |
+
The existing six AC2 policies retain their single-arm tokenizer behavior. The released Sparse
|
| 113 |
+
profiles have a 4,096-token budget; the current G0.5 Sparse adapter additionally needs an EOS
|
| 114 |
+
position and therefore requires a vocabulary of at most 4,095 tokens when training a new VLA.
|
|
|
|
| 115 |
|
| 116 |
## License
|
| 117 |
|
| 118 |
+
Tokenizer code and assets: Apache-2.0. G0.5 policy code and weights use their separate community license.
|
|
|
|
|
|
actioncodec2-0.1.1.tar.gz → actioncodec2-0.3.0.tar.gz
RENAMED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fdb76fb260435f45fba5a6572122eee3f3ceacb11480091f781e645ab60c0bec
|
| 3 |
+
size 503117
|
actioncodec2_kernels/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
"""Portable ActionCodec2 CPU kernels."""
|
|
|
|
|
|
actioncodec2_kernels/bpe.cpp
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Native BPE trainer -- the FAST-style ablation baseline.
|
| 2 |
+
//
|
| 3 |
+
// Set-BPE treats one control step as an unordered set of primitive cells and
|
| 4 |
+
// merges spatially before merging across time. Plain BPE instead serializes
|
| 5 |
+
// the quantized ``[T, D]`` cell matrix row-major into one symbol sequence and
|
| 6 |
+
// runs textbook byte-pair encoding on it, which is what FAST does after its
|
| 7 |
+
// DCT stage. Both trainers consume the identical quantized corpus,
|
| 8 |
+
// so a paired run isolates the tokenizer structure and nothing else.
|
| 9 |
+
//
|
| 10 |
+
// Correctness is identical to the Python reference trainer in
|
| 11 |
+
// ``actioncodec2/bpe_training.py``, including the tie-break (largest count,
|
| 12 |
+
// then lexicographically smallest ordered ``(left, right)`` pair) and the
|
| 13 |
+
// left-to-right application order, so the two emit the same merge list.
|
| 14 |
+
//
|
| 15 |
+
// Where the time goes, and what is done about it:
|
| 16 |
+
// * occurrences of every observed ordered pair are indexed once, so a merge
|
| 17 |
+
// touches only the positions that actually carry it;
|
| 18 |
+
// * the corpus is a doubly linked list over one flat symbol array, so
|
| 19 |
+
// collapsing a pair is O(1) and never rewrites the tail;
|
| 20 |
+
// * counters live in the same flat linear-probing table as the Set-BPE
|
| 21 |
+
// kernels (common.hpp), and the heap is lazy with per-merge deduplication
|
| 22 |
+
// of the keys a merge could have changed.
|
| 23 |
+
|
| 24 |
+
#include <pybind11/pybind11.h>
|
| 25 |
+
#include <pybind11/numpy.h>
|
| 26 |
+
#include <pybind11/stl.h>
|
| 27 |
+
|
| 28 |
+
#include <algorithm>
|
| 29 |
+
#include <cstdint>
|
| 30 |
+
#include <stdexcept>
|
| 31 |
+
#include <string>
|
| 32 |
+
#include <vector>
|
| 33 |
+
|
| 34 |
+
#include "common.hpp"
|
| 35 |
+
|
| 36 |
+
namespace py = pybind11;
|
| 37 |
+
|
| 38 |
+
namespace {
|
| 39 |
+
|
| 40 |
+
// BPE pairs are ordered, so the key keeps ``left`` in the high word. The
|
| 41 |
+
// heap tie-break in common.hpp prefers the smallest key, which is therefore the
|
| 42 |
+
// lexicographically smallest ``(left, right)`` -- the Python reference rule.
|
| 43 |
+
inline uint64_t ordered_key(int32_t left, int32_t right) {
|
| 44 |
+
return (static_cast<uint64_t>(static_cast<uint32_t>(left)) << 32) |
|
| 45 |
+
static_cast<uint64_t>(static_cast<uint32_t>(right));
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
inline int32_t key_left(uint64_t key) { return static_cast<int32_t>(key >> 32); }
|
| 49 |
+
inline int32_t key_right(uint64_t key) { return static_cast<int32_t>(key & 0xffffffffull); }
|
| 50 |
+
|
| 51 |
+
// Per-merge set of pair keys whose counters moved. A merge can only touch
|
| 52 |
+
// pairs incident to its two parents or its child, so the live population is
|
| 53 |
+
// bounded by a small multiple of the vocabulary; clearing is a generation bump
|
| 54 |
+
// rather than a table sweep.
|
| 55 |
+
class StampSet {
|
| 56 |
+
public:
|
| 57 |
+
explicit StampSet(size_t capacity_hint) {
|
| 58 |
+
size_t target = 1024;
|
| 59 |
+
while (target < capacity_hint * 4) target <<= 1;
|
| 60 |
+
keys_.assign(target, 0);
|
| 61 |
+
stamps_.assign(target, 0);
|
| 62 |
+
mask_ = target - 1;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
void reset() {
|
| 66 |
+
++generation_;
|
| 67 |
+
members_.clear();
|
| 68 |
+
if (generation_ != 0) return;
|
| 69 |
+
std::fill(stamps_.begin(), stamps_.end(), 0);
|
| 70 |
+
generation_ = 1;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
void insert(uint64_t key) {
|
| 74 |
+
size_t slot = mix(key) & mask_;
|
| 75 |
+
while (stamps_[slot] == generation_) {
|
| 76 |
+
if (keys_[slot] == key) return;
|
| 77 |
+
slot = (slot + 1) & mask_;
|
| 78 |
+
}
|
| 79 |
+
if ((members_.size() + 1) * 4 > (mask_ + 1) * 3) {
|
| 80 |
+
grow();
|
| 81 |
+
slot = mix(key) & mask_;
|
| 82 |
+
while (stamps_[slot] == generation_) {
|
| 83 |
+
if (keys_[slot] == key) return;
|
| 84 |
+
slot = (slot + 1) & mask_;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
keys_[slot] = key;
|
| 88 |
+
stamps_[slot] = generation_;
|
| 89 |
+
members_.push_back(key);
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
const std::vector<uint64_t>& members() const { return members_; }
|
| 93 |
+
|
| 94 |
+
private:
|
| 95 |
+
static uint64_t mix(uint64_t value) {
|
| 96 |
+
value ^= value >> 30;
|
| 97 |
+
value *= 0xbf58476d1ce4e5b9ull;
|
| 98 |
+
value ^= value >> 27;
|
| 99 |
+
value *= 0x94d049bb133111ebull;
|
| 100 |
+
return value ^ (value >> 31);
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
void grow() {
|
| 104 |
+
const size_t buckets = (mask_ + 1) << 1;
|
| 105 |
+
keys_.assign(buckets, 0);
|
| 106 |
+
stamps_.assign(buckets, 0);
|
| 107 |
+
mask_ = buckets - 1;
|
| 108 |
+
generation_ = 1;
|
| 109 |
+
for (const uint64_t key : members_) {
|
| 110 |
+
size_t slot = mix(key) & mask_;
|
| 111 |
+
while (stamps_[slot] == generation_) slot = (slot + 1) & mask_;
|
| 112 |
+
keys_[slot] = key;
|
| 113 |
+
stamps_[slot] = generation_;
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
std::vector<uint64_t> keys_;
|
| 118 |
+
std::vector<uint32_t> stamps_;
|
| 119 |
+
std::vector<uint64_t> members_;
|
| 120 |
+
size_t mask_ = 0;
|
| 121 |
+
uint32_t generation_ = 1;
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
// Occurrence index. ``slots`` maps a pair key to its 1-based row in
|
| 125 |
+
// ``lists``; a value of zero means "no row yet", which is exactly the absent
|
| 126 |
+
// value CounterMap already reports, so no separate presence bit is needed.
|
| 127 |
+
struct OccurrenceIndex {
|
| 128 |
+
ac2::CounterMap slots{1 << 16};
|
| 129 |
+
std::vector<std::vector<int32_t>> lists;
|
| 130 |
+
std::vector<uint64_t> keys;
|
| 131 |
+
int64_t stored = 0;
|
| 132 |
+
|
| 133 |
+
int64_t row_for(uint64_t key) {
|
| 134 |
+
const int64_t existing = slots.get(key);
|
| 135 |
+
if (existing > 0) return existing - 1;
|
| 136 |
+
const int64_t row = static_cast<int64_t>(lists.size());
|
| 137 |
+
lists.emplace_back();
|
| 138 |
+
keys.push_back(key);
|
| 139 |
+
slots.add(key, row + 1);
|
| 140 |
+
return row;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
void append(uint64_t key, int32_t position) {
|
| 144 |
+
lists[static_cast<size_t>(row_for(key))].push_back(position);
|
| 145 |
+
++stored;
|
| 146 |
+
}
|
| 147 |
+
};
|
| 148 |
+
|
| 149 |
+
struct Corpus {
|
| 150 |
+
std::vector<int32_t> symbol; // live token id per slot, -1 once absorbed
|
| 151 |
+
std::vector<int32_t> prev;
|
| 152 |
+
std::vector<int32_t> next;
|
| 153 |
+
int64_t actions = 0;
|
| 154 |
+
int64_t episodes = 0;
|
| 155 |
+
};
|
| 156 |
+
|
| 157 |
+
void ingest_chunk(Corpus& corpus, const int32_t* cells, int64_t rows, int32_t dimension,
|
| 158 |
+
const int32_t* offsets, const int32_t* bins, const int64_t* episode_offsets,
|
| 159 |
+
int64_t episode_count) {
|
| 160 |
+
for (int64_t episode = 0; episode < episode_count; ++episode) {
|
| 161 |
+
const int64_t start = episode_offsets[episode];
|
| 162 |
+
const int64_t stop = episode_offsets[episode + 1];
|
| 163 |
+
if (stop <= start || start < 0 || stop > rows) {
|
| 164 |
+
throw std::invalid_argument("BPE episode offsets must be increasing and in range");
|
| 165 |
+
}
|
| 166 |
+
const size_t first = corpus.symbol.size();
|
| 167 |
+
const int32_t* initial = cells + start * dimension;
|
| 168 |
+
for (int64_t row = start; row < stop; ++row) {
|
| 169 |
+
const int32_t* values = cells + row * dimension;
|
| 170 |
+
for (int32_t dim = 0; dim < dimension; ++dim) {
|
| 171 |
+
const int32_t value = values[dim];
|
| 172 |
+
if ((value >= 0) != (initial[dim] >= 0)) {
|
| 173 |
+
throw std::invalid_argument("active dimensions must remain constant within an episode");
|
| 174 |
+
}
|
| 175 |
+
if (value < 0) {
|
| 176 |
+
if (value != -1) throw std::invalid_argument("cells must contain -1 or a valid index");
|
| 177 |
+
continue;
|
| 178 |
+
}
|
| 179 |
+
if (value >= bins[dim]) throw std::invalid_argument("cell index exceeds its bin count");
|
| 180 |
+
corpus.symbol.push_back(offsets[dim] + value);
|
| 181 |
+
}
|
| 182 |
+
}
|
| 183 |
+
const size_t last = corpus.symbol.size();
|
| 184 |
+
if (last == first) throw std::invalid_argument("every control step needs an active coordinate");
|
| 185 |
+
if (last > static_cast<size_t>(INT32_MAX)) {
|
| 186 |
+
throw std::invalid_argument(
|
| 187 |
+
"BPE corpus exceeds 2^31 primitive cells; lower the fitting coverage");
|
| 188 |
+
}
|
| 189 |
+
corpus.prev.resize(last);
|
| 190 |
+
corpus.next.resize(last);
|
| 191 |
+
for (size_t index = first; index < last; ++index) {
|
| 192 |
+
corpus.prev[index] = index == first ? -1 : static_cast<int32_t>(index - 1);
|
| 193 |
+
corpus.next[index] = index + 1 == last ? -1 : static_cast<int32_t>(index + 1);
|
| 194 |
+
}
|
| 195 |
+
corpus.actions += stop - start;
|
| 196 |
+
++corpus.episodes;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
} // namespace
|
| 201 |
+
|
| 202 |
+
py::tuple train_bpe_native(py::iterable cell_chunks, const std::vector<int32_t>& bins,
|
| 203 |
+
int64_t vocab_size, int64_t max_token_cells, int threads) {
|
| 204 |
+
const int32_t dimension = static_cast<int32_t>(bins.size());
|
| 205 |
+
if (dimension < 1) throw std::invalid_argument("bins must not be empty");
|
| 206 |
+
std::vector<int32_t> offsets(dimension + 1, 0);
|
| 207 |
+
for (int32_t index = 0; index < dimension; ++index) {
|
| 208 |
+
if (bins[index] < 1) throw std::invalid_argument("every dimension needs at least one bin");
|
| 209 |
+
offsets[index + 1] = offsets[index] + bins[index];
|
| 210 |
+
}
|
| 211 |
+
const int32_t atom_count = offsets[dimension];
|
| 212 |
+
if (vocab_size < atom_count) {
|
| 213 |
+
throw std::invalid_argument("vocab_size is below the number of primitive atoms");
|
| 214 |
+
}
|
| 215 |
+
if (max_token_cells < 1) throw std::invalid_argument("max_token_cells must be positive");
|
| 216 |
+
|
| 217 |
+
// Greedy merge updates are serial. Preserve the requested option for the
|
| 218 |
+
// shared trainer API without changing the calling thread's OpenMP settings.
|
| 219 |
+
const int thread_count = 1;
|
| 220 |
+
|
| 221 |
+
Corpus corpus;
|
| 222 |
+
for (py::handle raw_chunk : cell_chunks) {
|
| 223 |
+
py::tuple chunk = py::cast<py::tuple>(raw_chunk);
|
| 224 |
+
if (chunk.size() != 2) throw std::invalid_argument("each chunk must be (cells, offsets)");
|
| 225 |
+
auto cells = py::cast<py::array_t<int32_t, py::array::c_style | py::array::forcecast>>(chunk[0]);
|
| 226 |
+
auto bounds =
|
| 227 |
+
py::cast<py::array_t<int64_t, py::array::c_style | py::array::forcecast>>(chunk[1]);
|
| 228 |
+
auto cells_view = cells.unchecked<2>();
|
| 229 |
+
auto bounds_view = bounds.unchecked<1>();
|
| 230 |
+
if (cells_view.shape(1) != dimension) {
|
| 231 |
+
throw std::invalid_argument("cells must have one column per bin count");
|
| 232 |
+
}
|
| 233 |
+
if (bounds_view.shape(0) < 2) throw std::invalid_argument("offsets must bound one episode");
|
| 234 |
+
{
|
| 235 |
+
py::gil_scoped_release release;
|
| 236 |
+
ingest_chunk(corpus, cells_view.data(0, 0), cells_view.shape(0), dimension, offsets.data(),
|
| 237 |
+
bins.data(), bounds_view.data(0), bounds_view.shape(0) - 1);
|
| 238 |
+
}
|
| 239 |
+
}
|
| 240 |
+
if (corpus.symbol.empty()) throw std::invalid_argument("BPE training needs one episode");
|
| 241 |
+
|
| 242 |
+
const int64_t initial_length = static_cast<int64_t>(corpus.symbol.size());
|
| 243 |
+
int64_t length = initial_length;
|
| 244 |
+
std::vector<int32_t>& symbol = corpus.symbol;
|
| 245 |
+
std::vector<int32_t>& prev = corpus.prev;
|
| 246 |
+
std::vector<int32_t>& next = corpus.next;
|
| 247 |
+
|
| 248 |
+
ac2::CounterMap counts(1 << 16);
|
| 249 |
+
OccurrenceIndex index;
|
| 250 |
+
std::vector<std::pair<int32_t, int32_t>> merges;
|
| 251 |
+
std::vector<int32_t> token_cells(static_cast<size_t>(atom_count), 1);
|
| 252 |
+
int64_t initial_pair_types = 0;
|
| 253 |
+
int64_t heap_pops = 0;
|
| 254 |
+
int64_t counter_updates = 0;
|
| 255 |
+
int64_t compactions = 0;
|
| 256 |
+
int64_t peak_stored = 0;
|
| 257 |
+
int64_t oversized_pairs = 0;
|
| 258 |
+
int32_t observed_cells = 1;
|
| 259 |
+
|
| 260 |
+
{
|
| 261 |
+
py::gil_scoped_release release;
|
| 262 |
+
|
| 263 |
+
// One pass builds ascending occurrence lists; the counter is their length.
|
| 264 |
+
for (int64_t position = 0; position < initial_length; ++position) {
|
| 265 |
+
const int32_t follower = next[position];
|
| 266 |
+
if (follower < 0) continue;
|
| 267 |
+
index.append(ordered_key(symbol[position], symbol[follower]),
|
| 268 |
+
static_cast<int32_t>(position));
|
| 269 |
+
}
|
| 270 |
+
for (size_t row = 0; row < index.lists.size(); ++row) {
|
| 271 |
+
counts.add(index.keys[row], static_cast<int64_t>(index.lists[row].size()));
|
| 272 |
+
}
|
| 273 |
+
initial_pair_types = static_cast<int64_t>(index.lists.size());
|
| 274 |
+
peak_stored = index.stored;
|
| 275 |
+
|
| 276 |
+
std::vector<ac2::HeapEntry> heap;
|
| 277 |
+
heap.reserve(index.lists.size());
|
| 278 |
+
counts.for_each([&](uint64_t key, int64_t value) { heap.push_back({value, key, 0}); });
|
| 279 |
+
std::make_heap(heap.begin(), heap.end(), ac2::HeapOrder{});
|
| 280 |
+
|
| 281 |
+
token_cells.reserve(static_cast<size_t>(vocab_size));
|
| 282 |
+
merges.reserve(static_cast<size_t>(vocab_size - atom_count));
|
| 283 |
+
|
| 284 |
+
ac2::CounterMap oversized(1 << 12);
|
| 285 |
+
StampSet touched(static_cast<size_t>(vocab_size));
|
| 286 |
+
std::vector<int32_t> occurrences;
|
| 287 |
+
|
| 288 |
+
while (static_cast<int64_t>(atom_count + merges.size()) < vocab_size && !heap.empty()) {
|
| 289 |
+
const ac2::HeapEntry top = ac2::heap_pop(heap);
|
| 290 |
+
++heap_pops;
|
| 291 |
+
const uint64_t key = top.key;
|
| 292 |
+
const int64_t frequency = counts.get(key);
|
| 293 |
+
if (frequency <= 0 || frequency != top.count) continue;
|
| 294 |
+
if (oversized.get(key) != 0) continue;
|
| 295 |
+
const int32_t left = key_left(key);
|
| 296 |
+
const int32_t right = key_right(key);
|
| 297 |
+
const int64_t span = static_cast<int64_t>(token_cells[left]) + token_cells[right];
|
| 298 |
+
if (span > max_token_cells) {
|
| 299 |
+
oversized.add(key, 1);
|
| 300 |
+
continue;
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
const int32_t child = atom_count + static_cast<int32_t>(merges.size());
|
| 304 |
+
token_cells.push_back(static_cast<int32_t>(span));
|
| 305 |
+
observed_cells = std::max(observed_cells, static_cast<int32_t>(span));
|
| 306 |
+
|
| 307 |
+
occurrences.clear();
|
| 308 |
+
occurrences.swap(index.lists[static_cast<size_t>(index.slots.get(key) - 1)]);
|
| 309 |
+
index.stored -= static_cast<int64_t>(occurrences.size());
|
| 310 |
+
// Left-to-right application is what makes overlapping runs deterministic;
|
| 311 |
+
// later appends leave the list unsorted, so restore the order first.
|
| 312 |
+
std::sort(occurrences.begin(), occurrences.end());
|
| 313 |
+
touched.reset();
|
| 314 |
+
touched.insert(key);
|
| 315 |
+
|
| 316 |
+
int64_t applied = 0;
|
| 317 |
+
for (const int32_t position : occurrences) {
|
| 318 |
+
if (symbol[position] != left) continue;
|
| 319 |
+
const int32_t follower = next[position];
|
| 320 |
+
if (follower < 0 || symbol[follower] != right) continue;
|
| 321 |
+
const int32_t before = prev[position];
|
| 322 |
+
const int32_t after = next[follower];
|
| 323 |
+
if (before >= 0) {
|
| 324 |
+
const uint64_t stale = ordered_key(symbol[before], left);
|
| 325 |
+
counts.add(stale, -1);
|
| 326 |
+
touched.insert(stale);
|
| 327 |
+
}
|
| 328 |
+
if (after >= 0) {
|
| 329 |
+
const uint64_t stale = ordered_key(right, symbol[after]);
|
| 330 |
+
counts.add(stale, -1);
|
| 331 |
+
touched.insert(stale);
|
| 332 |
+
}
|
| 333 |
+
symbol[position] = child;
|
| 334 |
+
symbol[follower] = -1;
|
| 335 |
+
next[position] = after;
|
| 336 |
+
if (after >= 0) prev[after] = position;
|
| 337 |
+
prev[follower] = -1;
|
| 338 |
+
next[follower] = -1;
|
| 339 |
+
if (before >= 0) {
|
| 340 |
+
const uint64_t fresh = ordered_key(symbol[before], child);
|
| 341 |
+
counts.add(fresh, 1);
|
| 342 |
+
index.append(fresh, before);
|
| 343 |
+
touched.insert(fresh);
|
| 344 |
+
}
|
| 345 |
+
if (after >= 0) {
|
| 346 |
+
const uint64_t fresh = ordered_key(child, symbol[after]);
|
| 347 |
+
counts.add(fresh, 1);
|
| 348 |
+
index.append(fresh, position);
|
| 349 |
+
touched.insert(fresh);
|
| 350 |
+
}
|
| 351 |
+
++applied;
|
| 352 |
+
}
|
| 353 |
+
counts.add(key, -applied);
|
| 354 |
+
counter_updates += 4 * applied;
|
| 355 |
+
length -= applied;
|
| 356 |
+
merges.emplace_back(left, right);
|
| 357 |
+
|
| 358 |
+
for (const uint64_t member : touched.members()) {
|
| 359 |
+
const int64_t value = counts.get(member);
|
| 360 |
+
if (value > 0) ac2::heap_push(heap, {value, member, 0});
|
| 361 |
+
}
|
| 362 |
+
peak_stored = std::max(peak_stored, index.stored);
|
| 363 |
+
|
| 364 |
+
// Superseded positions stay in the lists of pairs that never won. Sweep
|
| 365 |
+
// them out once they outnumber the live corpus, so memory tracks the
|
| 366 |
+
// corpus rather than the merge history.
|
| 367 |
+
if (index.stored > 3 * length) {
|
| 368 |
+
int64_t retained = 0;
|
| 369 |
+
for (size_t row = 0; row < index.lists.size(); ++row) {
|
| 370 |
+
auto& list = index.lists[row];
|
| 371 |
+
if (list.empty()) continue;
|
| 372 |
+
const int32_t expected_left = key_left(index.keys[row]);
|
| 373 |
+
const int32_t expected_right = key_right(index.keys[row]);
|
| 374 |
+
size_t write = 0;
|
| 375 |
+
for (const int32_t position : list) {
|
| 376 |
+
const int32_t follower = next[position];
|
| 377 |
+
if (symbol[position] != expected_left) continue;
|
| 378 |
+
if (follower < 0 || symbol[follower] != expected_right) continue;
|
| 379 |
+
list[write++] = position;
|
| 380 |
+
}
|
| 381 |
+
list.resize(write);
|
| 382 |
+
if (write * 2 < list.capacity()) list.shrink_to_fit();
|
| 383 |
+
retained += static_cast<int64_t>(write);
|
| 384 |
+
}
|
| 385 |
+
index.stored = retained;
|
| 386 |
+
++compactions;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
if (heap.size() > 4 * counts.size() + 65536) {
|
| 390 |
+
heap.clear();
|
| 391 |
+
counts.for_each([&](uint64_t member, int64_t value) { heap.push_back({value, member, 0}); });
|
| 392 |
+
std::make_heap(heap.begin(), heap.end(), ac2::HeapOrder{});
|
| 393 |
+
}
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
oversized_pairs = static_cast<int64_t>(oversized.live_entries());
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
py::list merge_list;
|
| 400 |
+
for (const auto& merge : merges) {
|
| 401 |
+
merge_list.append(py::make_tuple(merge.first, merge.second));
|
| 402 |
+
}
|
| 403 |
+
py::dict report;
|
| 404 |
+
report["episodes"] = corpus.episodes;
|
| 405 |
+
report["actions"] = corpus.actions;
|
| 406 |
+
report["atoms"] = static_cast<int64_t>(atom_count);
|
| 407 |
+
report["merges"] = static_cast<int64_t>(merges.size());
|
| 408 |
+
report["vocab_size"] = static_cast<int64_t>(atom_count + merges.size());
|
| 409 |
+
report["cells"] = initial_length;
|
| 410 |
+
report["tokens_per_action_initial"] =
|
| 411 |
+
static_cast<double>(initial_length) / static_cast<double>(corpus.actions);
|
| 412 |
+
report["tokens_per_action"] =
|
| 413 |
+
static_cast<double>(length) / static_cast<double>(corpus.actions);
|
| 414 |
+
report["compression"] = static_cast<double>(initial_length) / static_cast<double>(length);
|
| 415 |
+
report["max_token_cells"] = max_token_cells;
|
| 416 |
+
report["max_token_cells_observed"] = static_cast<int64_t>(observed_cells);
|
| 417 |
+
report["oversized_pairs_skipped"] = oversized_pairs;
|
| 418 |
+
report["initial_pair_types"] = initial_pair_types;
|
| 419 |
+
report["pair_types_peak"] = static_cast<int64_t>(index.lists.size());
|
| 420 |
+
report["occurrence_slots_peak"] = peak_stored;
|
| 421 |
+
report["occurrence_compactions"] = compactions;
|
| 422 |
+
report["heap_pops"] = heap_pops;
|
| 423 |
+
report["counter_updates"] = counter_updates;
|
| 424 |
+
report["threads"] = thread_count;
|
| 425 |
+
report["requested_threads"] = threads;
|
| 426 |
+
report["trainer"] = "native_sparse_exact";
|
| 427 |
+
return py::make_tuple(merge_list, report);
|
| 428 |
+
}
|
actioncodec2_kernels/common.hpp
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Shared primitives for the ActionCodec2 native kernels.
|
| 2 |
+
#pragma once
|
| 3 |
+
|
| 4 |
+
#include <cstdint>
|
| 5 |
+
#include <algorithm>
|
| 6 |
+
#include <utility>
|
| 7 |
+
#include <vector>
|
| 8 |
+
|
| 9 |
+
namespace ac2 {
|
| 10 |
+
|
| 11 |
+
class CounterMap {
|
| 12 |
+
public:
|
| 13 |
+
static constexpr uint64_t kEmpty = ~0ull;
|
| 14 |
+
|
| 15 |
+
explicit CounterMap(size_t capacity_hint = 1024) { reserve(capacity_hint); }
|
| 16 |
+
|
| 17 |
+
void reserve(size_t entries) {
|
| 18 |
+
size_t target = 16;
|
| 19 |
+
while (target * 3 < entries * 4) target <<= 1; // keep load factor <= 0.75
|
| 20 |
+
if (target <= mask_ + 1 && !keys_.empty()) return;
|
| 21 |
+
rehash(target);
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
size_t size() const { return size_; }
|
| 25 |
+
size_t buckets() const { return mask_ + 1; }
|
| 26 |
+
|
| 27 |
+
void clear() {
|
| 28 |
+
std::fill(keys_.begin(), keys_.end(), kEmpty);
|
| 29 |
+
size_ = 0;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
int64_t get(uint64_t key) const {
|
| 33 |
+
size_t slot = probe(key);
|
| 34 |
+
return keys_[slot] == key ? values_[slot] : 0;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
int64_t add(uint64_t key, int64_t delta) {
|
| 38 |
+
return add_with_previous(key, delta).second;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
// Update and expose both values with one hash-table probe. This is useful
|
| 42 |
+
// when a caller must distinguish a newly observed key from a stale one.
|
| 43 |
+
std::pair<int64_t, int64_t> add_with_previous(uint64_t key, int64_t delta) {
|
| 44 |
+
size_t slot = probe(key);
|
| 45 |
+
if (keys_[slot] != key) {
|
| 46 |
+
if ((size_ + 1) * 4 > (mask_ + 1) * 3) {
|
| 47 |
+
rehash((mask_ + 1) << 1);
|
| 48 |
+
slot = probe(key);
|
| 49 |
+
}
|
| 50 |
+
keys_[slot] = key;
|
| 51 |
+
values_[slot] = 0;
|
| 52 |
+
++size_;
|
| 53 |
+
}
|
| 54 |
+
const int64_t previous = values_[slot];
|
| 55 |
+
values_[slot] += delta;
|
| 56 |
+
return {previous, values_[slot]};
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
template <typename Fn>
|
| 60 |
+
void for_each(Fn&& fn) const {
|
| 61 |
+
for (size_t slot = 0; slot <= mask_; ++slot) {
|
| 62 |
+
if (keys_[slot] != kEmpty && values_[slot] != 0) fn(keys_[slot], values_[slot]);
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
size_t live_entries() const {
|
| 67 |
+
size_t live = 0;
|
| 68 |
+
for (size_t slot = 0; slot <= mask_; ++slot) {
|
| 69 |
+
if (keys_[slot] != kEmpty && values_[slot] != 0) ++live;
|
| 70 |
+
}
|
| 71 |
+
return live;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
private:
|
| 75 |
+
static uint64_t mix(uint64_t value) { // splitmix64 finalizer
|
| 76 |
+
value ^= value >> 30;
|
| 77 |
+
value *= 0xbf58476d1ce4e5b9ull;
|
| 78 |
+
value ^= value >> 27;
|
| 79 |
+
value *= 0x94d049bb133111ebull;
|
| 80 |
+
return value ^ (value >> 31);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
size_t probe(uint64_t key) const {
|
| 84 |
+
size_t slot = mix(key) & mask_;
|
| 85 |
+
while (keys_[slot] != kEmpty && keys_[slot] != key) slot = (slot + 1) & mask_;
|
| 86 |
+
return slot;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
void rehash(size_t buckets) {
|
| 90 |
+
std::vector<uint64_t> old_keys;
|
| 91 |
+
std::vector<int64_t> old_values;
|
| 92 |
+
old_keys.swap(keys_);
|
| 93 |
+
old_values.swap(values_);
|
| 94 |
+
keys_.assign(buckets, kEmpty);
|
| 95 |
+
values_.assign(buckets, 0);
|
| 96 |
+
mask_ = buckets - 1;
|
| 97 |
+
size_ = 0;
|
| 98 |
+
for (size_t slot = 0; slot < old_keys.size(); ++slot) {
|
| 99 |
+
if (old_keys[slot] != kEmpty && old_values[slot] != 0) {
|
| 100 |
+
size_t target = probe(old_keys[slot]);
|
| 101 |
+
keys_[target] = old_keys[slot];
|
| 102 |
+
values_[target] = old_values[slot];
|
| 103 |
+
++size_;
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
std::vector<uint64_t> keys_;
|
| 109 |
+
std::vector<int64_t> values_;
|
| 110 |
+
size_t mask_ = 0;
|
| 111 |
+
size_t size_ = 0;
|
| 112 |
+
};
|
| 113 |
+
|
| 114 |
+
struct HeapEntry {
|
| 115 |
+
int64_t count;
|
| 116 |
+
uint64_t key;
|
| 117 |
+
uint32_t version; // only shifted temporal self-rules use this
|
| 118 |
+
};
|
| 119 |
+
|
| 120 |
+
struct HeapOrder {
|
| 121 |
+
bool operator()(const HeapEntry& a, const HeapEntry& b) const {
|
| 122 |
+
if (a.count != b.count) return a.count < b.count; // larger count first
|
| 123 |
+
return a.key > b.key; // smaller key first
|
| 124 |
+
}
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
inline void heap_push(std::vector<HeapEntry>& heap, const HeapEntry& entry) {
|
| 128 |
+
heap.push_back(entry);
|
| 129 |
+
std::push_heap(heap.begin(), heap.end(), HeapOrder{});
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
inline HeapEntry heap_pop(std::vector<HeapEntry>& heap) {
|
| 133 |
+
std::pop_heap(heap.begin(), heap.end(), HeapOrder{});
|
| 134 |
+
HeapEntry top = heap.back();
|
| 135 |
+
heap.pop_back();
|
| 136 |
+
return top;
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
} // namespace ac2
|
actioncodec2_kernels/grammar.cpp
CHANGED
|
@@ -1,162 +1,279 @@
|
|
|
|
|
| 1 |
#include <pybind11/numpy.h>
|
| 2 |
#include <pybind11/pybind11.h>
|
| 3 |
#include <pybind11/stl.h>
|
| 4 |
-
|
| 5 |
#include <algorithm>
|
| 6 |
#include <cstdint>
|
| 7 |
-
#include <limits>
|
| 8 |
-
#include <map>
|
| 9 |
#include <memory>
|
| 10 |
#include <stdexcept>
|
| 11 |
#include <vector>
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
namespace py
|
| 14 |
namespace {
|
| 15 |
-
using State
|
| 16 |
-
|
| 17 |
-
class
|
| 18 |
-
protected:
|
| 19 |
-
int64_t horizon_, vocabulary_;
|
| 20 |
-
virtual void validate_state(const State&) const = 0;
|
| 21 |
-
virtual bool allowed(const State&, int64_t) const = 0;
|
| 22 |
-
virtual State apply(const State&, int64_t) const = 0;
|
| 23 |
public:
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
py::gil_scoped_release release;
|
| 35 |
-
for
|
| 36 |
return result;
|
| 37 |
}
|
| 38 |
};
|
| 39 |
-
|
| 40 |
-
class SetGrammar : public CoverageGrammar {
|
| 41 |
uint64_t full_;
|
| 42 |
-
std::vector<int64_t>
|
| 43 |
-
std::
|
| 44 |
-
std::
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
}
|
| 54 |
-
int64_t
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
return
|
| 58 |
-
}
|
| 59 |
-
bool allowed(const State& state,
|
| 60 |
-
if
|
| 61 |
-
const int64_t
|
| 62 |
-
if
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
while (!(missing & 1)) { missing >>= 1; ++root; }
|
| 66 |
-
if (roots_[token] != root) return false;
|
| 67 |
-
for (auto [offset, support] : patterns_[token]) {
|
| 68 |
-
if (offset >= horizon_ - start || (static_cast<uint64_t>(state[start + offset]) & support))
|
| 69 |
-
return false;
|
| 70 |
}
|
| 71 |
return true;
|
| 72 |
}
|
| 73 |
-
State
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
for
|
| 77 |
-
result[start + offset] |= static_cast<int64_t>(support);
|
| 78 |
return result;
|
| 79 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
public:
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
}
|
| 107 |
}
|
| 108 |
};
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
int64_t
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
if
|
| 117 |
-
throw std::invalid_argument("invalid plain-BPE grammar state");
|
| 118 |
}
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
}
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
}
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
}
|
|
|
|
| 145 |
|
| 146 |
void register_grammar(py::module_& module) {
|
| 147 |
-
py::class_<
|
| 148 |
-
.def("advance",
|
| 149 |
-
State next
|
| 150 |
-
if (next.empty()) return py::none();
|
| 151 |
-
py::tuple result(next.size());
|
| 152 |
-
for (size_t i = 0; i < next.size(); ++i) result[i] = next[i];
|
| 153 |
-
return result;
|
| 154 |
})
|
| 155 |
-
.def("mask",
|
| 156 |
-
py::class_<SetGrammar,
|
| 157 |
-
.def(py::init<int64_t,
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
}
|
|
|
|
| 1 |
+
// Coverage grammars and multi-component routing, including time-major streams.
|
| 2 |
#include <pybind11/numpy.h>
|
| 3 |
#include <pybind11/pybind11.h>
|
| 4 |
#include <pybind11/stl.h>
|
|
|
|
| 5 |
#include <algorithm>
|
| 6 |
#include <cstdint>
|
|
|
|
|
|
|
| 7 |
#include <memory>
|
| 8 |
#include <stdexcept>
|
| 9 |
#include <vector>
|
| 10 |
+
#include "parallel.hpp"
|
| 11 |
+
#include "token_streams.hpp"
|
| 12 |
|
| 13 |
+
namespace py=pybind11;
|
| 14 |
namespace {
|
| 15 |
+
using State=std::vector<int64_t>;
|
| 16 |
+
using States=std::vector<State>;
|
| 17 |
+
class Grammar {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
public:
|
| 19 |
+
int64_t horizon,vocabulary;
|
| 20 |
+
virtual ~Grammar()=default;
|
| 21 |
+
virtual State initial()const=0;
|
| 22 |
+
virtual State advance(const State&,int64_t)const=0;
|
| 23 |
+
virtual int64_t covered(const State&)const=0;
|
| 24 |
+
virtual int64_t next_dimension(const State&)const=0;
|
| 25 |
+
virtual bool allowed(const State&,int64_t)const=0;
|
| 26 |
+
virtual std::shared_ptr<Grammar> clone(int64_t)const=0;
|
| 27 |
+
py::array_t<bool> mask(const State& state)const {
|
| 28 |
+
py::array_t<bool> result(vocabulary);
|
| 29 |
py::gil_scoped_release release;
|
| 30 |
+
for(int64_t token=0;token<vocabulary;++token)result.mutable_data()[token]=allowed(state,token);
|
| 31 |
return result;
|
| 32 |
}
|
| 33 |
};
|
| 34 |
+
class SetGrammar:public Grammar {
|
|
|
|
| 35 |
uint64_t full_;
|
| 36 |
+
using Patterns=std::vector<std::vector<std::pair<int64_t,uint64_t>>>;
|
| 37 |
+
std::shared_ptr<const std::vector<int64_t>> roots_;
|
| 38 |
+
std::shared_ptr<const Patterns> patterns_;
|
| 39 |
+
std::shared_ptr<const std::vector<bool>> scope_;
|
| 40 |
+
public:
|
| 41 |
+
SetGrammar(int64_t steps,uint64_t full,std::vector<int64_t> roots,
|
| 42 |
+
std::vector<std::vector<std::pair<int64_t,uint64_t>>> patterns)
|
| 43 |
+
:full_(full),roots_(std::make_shared<const std::vector<int64_t>>(std::move(roots))),
|
| 44 |
+
patterns_(std::make_shared<const Patterns>(std::move(patterns))) {
|
| 45 |
+
if(steps<1 || !full || roots_->size()!=patterns_->size())throw std::invalid_argument("invalid grammar metadata");
|
| 46 |
+
horizon=steps;vocabulary=roots_->size();
|
| 47 |
+
std::vector<bool> scopes;
|
| 48 |
+
for(const auto& pattern:*patterns_) {
|
| 49 |
+
bool scope=true;
|
| 50 |
+
for(auto[offset,support]:pattern) {
|
| 51 |
+
if(offset<0)throw std::invalid_argument("negative grammar pattern offset");
|
| 52 |
+
scope=scope && !(support & ~full_);
|
| 53 |
+
}
|
| 54 |
+
scopes.push_back(scope);
|
| 55 |
}
|
| 56 |
+
scope_=std::make_shared<const std::vector<bool>>(std::move(scopes));
|
| 57 |
+
}
|
| 58 |
+
std::shared_ptr<Grammar> clone(int64_t steps)const override {
|
| 59 |
+
if(steps<1)throw std::invalid_argument("horizon must be positive");
|
| 60 |
+
auto result=std::make_shared<SetGrammar>(*this);result->horizon=steps;return result;
|
| 61 |
+
}
|
| 62 |
+
State initial()const override{return State(horizon,0);}
|
| 63 |
+
int64_t covered(const State& state)const override {
|
| 64 |
+
if(static_cast<int64_t>(state.size())!=horizon)throw std::invalid_argument("invalid grammar state");
|
| 65 |
+
for(int64_t i=0;i<horizon;++i)if(static_cast<uint64_t>(state[i])!=full_)return i;
|
| 66 |
+
return horizon;
|
| 67 |
}
|
| 68 |
+
int64_t next_dimension(const State& state)const override {
|
| 69 |
+
int64_t front=covered(state);if(front==horizon)return -1;
|
| 70 |
+
uint64_t missing=full_ & ~static_cast<uint64_t>(state[front]);
|
| 71 |
+
int64_t d=0;while(!(missing & 1)){missing>>=1;++d;}return d;
|
| 72 |
+
}
|
| 73 |
+
bool allowed(const State& state,int64_t token)const override {
|
| 74 |
+
if(token<0 || token>=vocabulary || !(*scope_)[token])return false;
|
| 75 |
+
const int64_t front=covered(state);
|
| 76 |
+
if(front==horizon || (*roots_)[token]!=next_dimension(state))return false;
|
| 77 |
+
for(auto[offset,support]:(*patterns_)[token]) {
|
| 78 |
+
if(front+offset>=horizon || (static_cast<uint64_t>(state[front+offset]) & support))return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
return true;
|
| 81 |
}
|
| 82 |
+
State advance(const State& state,int64_t token)const override {
|
| 83 |
+
if(!allowed(state,token))return {};
|
| 84 |
+
State result=state;int64_t front=covered(state);
|
| 85 |
+
for(auto[offset,support]:(*patterns_)[token])result[front+offset]|=static_cast<int64_t>(support);
|
|
|
|
| 86 |
return result;
|
| 87 |
}
|
| 88 |
+
};
|
| 89 |
+
class PlainGrammar:public Grammar {
|
| 90 |
+
std::shared_ptr<const std::vector<int64_t>> lengths_,dims_;
|
| 91 |
+
std::shared_ptr<const std::vector<bool>> phase_;
|
| 92 |
public:
|
| 93 |
+
PlainGrammar(int64_t steps,std::vector<int64_t> lengths,std::vector<int64_t> dims,
|
| 94 |
+
py::array_t<bool,py::array::c_style|py::array::forcecast> phase)
|
| 95 |
+
:lengths_(std::make_shared<const std::vector<int64_t>>(std::move(lengths))),
|
| 96 |
+
dims_(std::make_shared<const std::vector<int64_t>>(std::move(dims))) {
|
| 97 |
+
horizon=steps;vocabulary=lengths_->size();
|
| 98 |
+
if(steps<1 || dims_->empty() || phase.ndim()!=2 || phase.shape(0)!=static_cast<int64_t>(dims_->size()) || phase.shape(1)!=vocabulary)
|
| 99 |
+
throw std::invalid_argument("invalid plain-BPE grammar metadata");
|
| 100 |
+
for(auto value:*lengths_)if(value<1)throw std::invalid_argument("invalid token expansion");
|
| 101 |
+
phase_=std::make_shared<const std::vector<bool>>(phase.data(),phase.data()+phase.size());
|
| 102 |
+
}
|
| 103 |
+
std::shared_ptr<Grammar> clone(int64_t steps)const override {
|
| 104 |
+
if(steps<1)throw std::invalid_argument("horizon must be positive");
|
| 105 |
+
auto result=std::make_shared<PlainGrammar>(*this);result->horizon=steps;return result;
|
| 106 |
+
}
|
| 107 |
+
State initial()const override{return {0};}
|
| 108 |
+
int64_t covered(const State& state)const override {
|
| 109 |
+
if(state.size()!=1 || state[0]<0)throw std::invalid_argument("invalid grammar state");
|
| 110 |
+
return state[0]/dims_->size();
|
| 111 |
+
}
|
| 112 |
+
int64_t next_dimension(const State& state)const override{return (*dims_)[state[0]%dims_->size()];}
|
| 113 |
+
bool allowed(const State& state,int64_t token)const override {
|
| 114 |
+
return token>=0 && token<vocabulary && state[0]+(*lengths_)[token]<=horizon*static_cast<int64_t>(dims_->size()) && (*phase_)[(state[0]%dims_->size())*vocabulary+token];
|
| 115 |
+
}
|
| 116 |
+
State advance(const State& state,int64_t token)const override {
|
| 117 |
+
if(!allowed(state,token))return {};
|
| 118 |
+
return {state[0]+(*lengths_)[token]};
|
| 119 |
}
|
| 120 |
};
|
| 121 |
+
using Grammars=std::vector<std::shared_ptr<Grammar>>;
|
| 122 |
+
int64_t next_component(const Grammars& grammars,const States& states,bool time_major) {
|
| 123 |
+
int64_t selected=grammars.size(),minimum=INT64_MAX;
|
| 124 |
+
for(size_t i=0;i<grammars.size();++i) {
|
| 125 |
+
const int64_t front=grammars[i]->covered(states[i]);
|
| 126 |
+
if(front>=grammars[i]->horizon)continue;
|
| 127 |
+
if(!time_major)return i;
|
| 128 |
+
if(front<minimum){minimum=front;selected=i;}
|
|
|
|
| 129 |
}
|
| 130 |
+
return selected;
|
| 131 |
+
}
|
| 132 |
+
States initial(const Grammars& grammars) {
|
| 133 |
+
States states;for(const auto& grammar:grammars)states.push_back(grammar->initial());return states;
|
| 134 |
+
}
|
| 135 |
+
bool advance(const Grammars& grammars,const std::vector<int64_t>& offsets,States& states,int64_t index,int64_t token) {
|
| 136 |
+
if(index<0 || index>=static_cast<int64_t>(grammars.size()))return false;
|
| 137 |
+
State next=grammars[index]->advance(states[index],token-offsets[index]);
|
| 138 |
+
if(next.empty())return false;
|
| 139 |
+
states[index]=std::move(next);
|
| 140 |
+
return true;
|
| 141 |
+
}
|
| 142 |
+
py::tuple state_tuple(const State& state) {
|
| 143 |
+
py::tuple result(state.size());for(size_t i=0;i<state.size();++i)result[i]=state[i];return result;
|
| 144 |
+
}
|
| 145 |
+
py::object route_advance(const Grammars& grammars,const std::vector<int64_t>& offsets,
|
| 146 |
+
bool time_major,int64_t index,States states,int64_t token) {
|
| 147 |
+
if(states.size()!=grammars.size() || offsets.size()!=grammars.size())throw std::invalid_argument("invalid routed metadata");
|
| 148 |
+
if(!advance(grammars,offsets,states,index,token))return py::none();
|
| 149 |
+
py::tuple local(states.size());for(size_t i=0;i<states.size();++i)local[i]=state_tuple(states[i]);
|
| 150 |
+
return py::make_tuple(next_component(grammars,states,time_major),local);
|
| 151 |
+
}
|
| 152 |
+
std::vector<int64_t> interleave(const Grammars& grammars,const std::vector<int64_t>& offsets,
|
| 153 |
+
bool time_major,const std::vector<std::vector<int64_t>>& streams) {
|
| 154 |
+
if(streams.size()!=grammars.size() || offsets.size()!=grammars.size())throw std::invalid_argument("one stream per component is required");
|
| 155 |
+
States states=initial(grammars);std::vector<size_t> cursor(streams.size());std::vector<int64_t> output;
|
| 156 |
+
int64_t index;
|
| 157 |
+
while((index=next_component(grammars,states,time_major))<static_cast<int64_t>(grammars.size())) {
|
| 158 |
+
if(cursor[index]==streams[index].size())throw std::invalid_argument("component stream ends before complete coverage");
|
| 159 |
+
const int64_t token=streams[index][cursor[index]++]+offsets[index];
|
| 160 |
+
if(!advance(grammars,offsets,states,index,token))throw std::invalid_argument("invalid routed component stream");
|
| 161 |
+
output.push_back(token);
|
| 162 |
}
|
| 163 |
+
for(size_t i=0;i<cursor.size();++i)if(cursor[i]!=streams[i].size())throw std::invalid_argument("extra component tokens");
|
| 164 |
+
return output;
|
| 165 |
+
}
|
| 166 |
+
std::vector<std::vector<int64_t>> split(const Grammars& grammars,const std::vector<int64_t>& offsets,
|
| 167 |
+
bool time_major,const std::vector<int64_t>& tokens) {
|
| 168 |
+
if(offsets.size()!=grammars.size())throw std::invalid_argument("invalid routed offsets");
|
| 169 |
+
States states=initial(grammars);std::vector<std::vector<int64_t>> output(grammars.size());
|
| 170 |
+
for(int64_t token:tokens) {
|
| 171 |
+
int64_t index=next_component(grammars,states,time_major);
|
| 172 |
+
if(!advance(grammars,offsets,states,index,token))throw std::invalid_argument("invalid routed token coverage");
|
| 173 |
+
output[index].push_back(token-offsets[index]);
|
| 174 |
}
|
| 175 |
+
if(next_component(grammars,states,time_major)!=static_cast<int64_t>(grammars.size()))
|
| 176 |
+
throw std::invalid_argument("Incomplete action token stream");
|
| 177 |
+
return output;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
using Streams=std::vector<std::vector<int64_t>>;
|
| 181 |
+
using ComponentStreams=std::vector<Streams>;
|
| 182 |
+
|
| 183 |
+
Streams interleave_batch(const Grammars& grammars,const std::vector<int64_t>& offsets,
|
| 184 |
+
bool time_major,const ComponentStreams& streams) {
|
| 185 |
+
if(streams.size()!=grammars.size() || streams.empty() || streams.front().empty())
|
| 186 |
+
throw std::invalid_argument("one non-empty batch per component is required");
|
| 187 |
+
const int64_t batch=streams.front().size();
|
| 188 |
+
for(const auto& rows:streams)if(static_cast<int64_t>(rows.size())!=batch)
|
| 189 |
+
throw std::invalid_argument("component batches must have the same size");
|
| 190 |
+
Streams output(batch);
|
| 191 |
+
py::gil_scoped_release release;
|
| 192 |
+
ac2::parallel_for(batch,0,[&](int64_t b) {
|
| 193 |
+
Streams local;for(const auto& rows:streams)local.push_back(rows[b]);
|
| 194 |
+
output[b]=interleave(grammars,offsets,time_major,local);
|
| 195 |
+
});
|
| 196 |
+
return output;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
py::tuple decode_batch(const Grammars& prototypes,const std::vector<int64_t>& offsets,
|
| 200 |
+
bool time_major,const py::iterable& streams,
|
| 201 |
+
py::array_t<int64_t,py::array::c_style|py::array::forcecast> counts,
|
| 202 |
+
int64_t cells_per_step) {
|
| 203 |
+
if(prototypes.empty() || counts.ndim()!=1 || cells_per_step<1)
|
| 204 |
+
throw std::invalid_argument("invalid routed batch metadata");
|
| 205 |
+
const auto rows=ac2::checked_token_streams(streams,counts.size(),true);
|
| 206 |
+
int64_t steps=-1;
|
| 207 |
+
ComponentStreams output(prototypes.size(),Streams(rows.size()));
|
| 208 |
+
{
|
| 209 |
+
py::gil_scoped_release release;
|
| 210 |
+
for(const auto& row:rows) {
|
| 211 |
+
int64_t cells=0;
|
| 212 |
+
for(int64_t token:row) {
|
| 213 |
+
if(counts.data()[token]<1)throw std::invalid_argument("token is outside the routed profile vocabularies");
|
| 214 |
+
cells+=counts.data()[token];
|
| 215 |
+
}
|
| 216 |
+
if(cells%cells_per_step || cells/cells_per_step<1)
|
| 217 |
+
throw std::invalid_argument("Incomplete action token stream: " +
|
| 218 |
+
std::to_string(cells_per_step-cells%cells_per_step) +
|
| 219 |
+
" more coordinate values are needed to finish a step. "
|
| 220 |
+
"Constrain generation with codec.grammar(H, fps=...).next_token_mask(prefix) "
|
| 221 |
+
"and check is_complete(tokens) for the intended H before decoding. "
|
| 222 |
+
"Use codec.batch_decode(..., errors='return') to isolate invalid rows; "
|
| 223 |
+
"do not pad truncated tokens with zeros.");
|
| 224 |
+
if(steps>=0 && steps!=cells/cells_per_step)
|
| 225 |
+
throw std::invalid_argument("all token streams must decode to the same length; use codec.batch_decode(...) for independent variable-length outputs");
|
| 226 |
+
steps=cells/cells_per_step;
|
| 227 |
}
|
| 228 |
+
Grammars grammars;for(const auto& prototype:prototypes)grammars.push_back(prototype->clone(steps));
|
| 229 |
+
ac2::parallel_for(rows.size(),0,[&](int64_t b) {
|
| 230 |
+
auto local=split(grammars,offsets,time_major,rows[b]);
|
| 231 |
+
for(size_t i=0;i<local.size();++i)output[i][b]=std::move(local[i]);
|
| 232 |
+
});
|
| 233 |
}
|
| 234 |
+
return py::make_tuple(steps,output);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
std::vector<int64_t> complete(const Grammars& grammars,const std::vector<int64_t>& offsets,
|
| 238 |
+
bool time_major,std::vector<int64_t> tokens,int64_t steps,
|
| 239 |
+
const std::vector<std::vector<int64_t>>& zero_atoms) {
|
| 240 |
+
if(grammars.empty() || offsets.size()!=grammars.size() || zero_atoms.size()!=grammars.size() ||
|
| 241 |
+
steps<1 || steps>grammars.front()->horizon)throw std::invalid_argument("invalid prefix completion metadata");
|
| 242 |
+
py::gil_scoped_release release;
|
| 243 |
+
States states=initial(grammars);
|
| 244 |
+
for(int64_t token:tokens) {
|
| 245 |
+
int64_t index=next_component(grammars,states,time_major);
|
| 246 |
+
if(!advance(grammars,offsets,states,index,token))throw std::invalid_argument("invalid routed token prefix");
|
| 247 |
+
}
|
| 248 |
+
for(size_t i=0;i<grammars.size();++i)if(grammars[i]->covered(states[i])<steps)
|
| 249 |
+
throw std::invalid_argument("action prefix does not cover the requested frames");
|
| 250 |
+
int64_t index;
|
| 251 |
+
while((index=next_component(grammars,states,time_major))<static_cast<int64_t>(grammars.size())) {
|
| 252 |
+
int64_t dimension=grammars[index]->next_dimension(states[index]);
|
| 253 |
+
if(dimension<0 || dimension>=static_cast<int64_t>(zero_atoms[index].size()))
|
| 254 |
+
throw std::invalid_argument("missing zero-motion primitive atom");
|
| 255 |
+
const int64_t token=offsets[index]+zero_atoms[index][dimension];
|
| 256 |
+
if(!advance(grammars,offsets,states,index,token))throw std::invalid_argument("invalid zero-motion suffix");
|
| 257 |
+
tokens.push_back(token);
|
| 258 |
+
}
|
| 259 |
+
return tokens;
|
| 260 |
}
|
| 261 |
+
} // namespace
|
| 262 |
|
| 263 |
void register_grammar(py::module_& module) {
|
| 264 |
+
py::class_<Grammar,std::shared_ptr<Grammar>>(module,"CoverageGrammar")
|
| 265 |
+
.def("advance",[](const Grammar& g,const State& s,int64_t token)->py::object {
|
| 266 |
+
State next=g.advance(s,token);return next.empty()?py::object(py::none()):py::object(state_tuple(next));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
})
|
| 268 |
+
.def("mask",&Grammar::mask).def("covered",&Grammar::covered).def("clone",&Grammar::clone);
|
| 269 |
+
py::class_<SetGrammar,Grammar,std::shared_ptr<SetGrammar>>(module,"SetGrammar")
|
| 270 |
+
.def(py::init<int64_t,uint64_t,std::vector<int64_t>,std::vector<std::vector<std::pair<int64_t,uint64_t>>>>());
|
| 271 |
+
py::class_<PlainGrammar,Grammar,std::shared_ptr<PlainGrammar>>(module,"PlainGrammar")
|
| 272 |
+
.def(py::init<int64_t,std::vector<int64_t>,std::vector<int64_t>,py::array_t<bool,py::array::c_style|py::array::forcecast>>());
|
| 273 |
+
module.def("route_advance",&route_advance);
|
| 274 |
+
module.def("route_interleave",&interleave,py::call_guard<py::gil_scoped_release>());
|
| 275 |
+
module.def("route_split",&split,py::call_guard<py::gil_scoped_release>());
|
| 276 |
+
module.def("route_interleave_batch",&interleave_batch);
|
| 277 |
+
module.def("route_decode_batch",&decode_batch);
|
| 278 |
+
module.def("route_complete",&complete);
|
| 279 |
}
|
actioncodec2_kernels/inference.cpp
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Lossless plain-BPE inference and additive physical reconstruction.
|
| 2 |
+
#include <pybind11/numpy.h>
|
| 3 |
+
#include <pybind11/pybind11.h>
|
| 4 |
+
#include <pybind11/stl.h>
|
| 5 |
+
#include <algorithm>
|
| 6 |
+
#include <cstdint>
|
| 7 |
+
#include <map>
|
| 8 |
+
#include <limits>
|
| 9 |
+
#include <queue>
|
| 10 |
+
#include <stdexcept>
|
| 11 |
+
#include <vector>
|
| 12 |
+
#include "parallel.hpp"
|
| 13 |
+
#include "quaternion.hpp"
|
| 14 |
+
|
| 15 |
+
namespace py = pybind11;
|
| 16 |
+
namespace {
|
| 17 |
+
using I64 = py::array_t<int64_t, py::array::c_style | py::array::forcecast>;
|
| 18 |
+
using F64 = py::array_t<double, py::array::c_style | py::array::forcecast>;
|
| 19 |
+
using F32 = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
F64 quaternion_map(const F64& values, const std::string& operation) {
|
| 24 |
+
const bool vector_in = operation == "from_rotvec" || operation == "from_euler";
|
| 25 |
+
const int input_width=vector_in?3:4;
|
| 26 |
+
const bool vector_out=operation == "to_rotvec" || operation == "to_euler";
|
| 27 |
+
const int width=vector_out?3:4;
|
| 28 |
+
if(values.ndim()!=2 || values.shape(1)!=input_width)
|
| 29 |
+
throw std::invalid_argument("invalid quaternion operation shape");
|
| 30 |
+
F64 output({values.shape(0),static_cast<int64_t>(width)});
|
| 31 |
+
py::gil_scoped_release release;
|
| 32 |
+
for(int64_t i=0;i<values.shape(0);++i) {
|
| 33 |
+
const double* v=values.data()+i*input_width;
|
| 34 |
+
ac2q::Q q=vector_in?(operation=="from_rotvec"?ac2q::from_rotvec(v):ac2q::from_euler(v))
|
| 35 |
+
:ac2q::Q{v[0],v[1],v[2],v[3]};
|
| 36 |
+
double* out=output.mutable_data()+i*width;
|
| 37 |
+
if(operation=="to_rotvec")ac2q::to_rotvec(q,out);
|
| 38 |
+
else if(operation=="to_euler")ac2q::to_euler(q,out);
|
| 39 |
+
else {
|
| 40 |
+
if(operation=="normalize")q=ac2q::normalize(q);
|
| 41 |
+
else if(operation=="conjugate")q=ac2q::conjugate(q);
|
| 42 |
+
else if(!vector_in)throw std::invalid_argument("unknown quaternion operation");
|
| 43 |
+
std::copy(q.begin(),q.end(),out);
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
return output;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
F64 quaternion_multiply(const F64& first, const F64& second) {
|
| 50 |
+
if(first.ndim()!=2 || first.shape(1)!=4 || second.ndim()!=2 ||
|
| 51 |
+
second.shape(1)!=4 || first.shape(0)!=second.shape(0))
|
| 52 |
+
throw std::invalid_argument("invalid quaternion product shape");
|
| 53 |
+
F64 output({first.shape(0),int64_t{4}});
|
| 54 |
+
py::gil_scoped_release release;
|
| 55 |
+
for(int64_t i=0;i<first.shape(0);++i) {
|
| 56 |
+
auto* a=first.data()+4*i;auto* b=second.data()+4*i;
|
| 57 |
+
auto q=ac2q::multiply({a[0],a[1],a[2],a[3]},{b[0],b[1],b[2],b[3]});
|
| 58 |
+
std::copy(q.begin(),q.end(),output.mutable_data()+4*i);
|
| 59 |
+
}
|
| 60 |
+
return output;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
F32 so3_reconstruct(const F64& table, const I64& cells, const F32& previous,
|
| 64 |
+
const F32& scale, int input_order, int primitive_order,
|
| 65 |
+
bool left, bool rotation_vector, double radius) {
|
| 66 |
+
if(cells.ndim()!=3 || cells.shape(2)!=3 || table.ndim()!=2 || table.shape(0)!=3 ||
|
| 67 |
+
previous.ndim()!=2 || previous.shape(0)!=cells.shape(0) || previous.shape(1)!=3 || scale.size()!=3)
|
| 68 |
+
throw std::invalid_argument("invalid SO(3) reconstruction shapes");
|
| 69 |
+
const int64_t batch=cells.shape(0),steps=cells.shape(1);
|
| 70 |
+
F32 output({batch,steps,int64_t{3}});
|
| 71 |
+
py::gil_scoped_release release;
|
| 72 |
+
ac2::parallel_for(batch,0,[&](int64_t b) {
|
| 73 |
+
double boundary[3];
|
| 74 |
+
for(int d=0;d<3;++d)boundary[d]=static_cast<double>(previous.data()[3*b+d])*scale.data()[d];
|
| 75 |
+
auto increment=rotation_vector?ac2q::from_rotvec(boundary):ac2q::from_euler(boundary);
|
| 76 |
+
ac2q::Q state{0,0,0,1};
|
| 77 |
+
float accumulated[3]={0,0,0};
|
| 78 |
+
auto apply=[&](ac2q::Q a,ac2q::Q v){return left?ac2q::multiply(v,a):ac2q::multiply(a,v);};
|
| 79 |
+
for(int64_t t=0;t<steps;++t) {
|
| 80 |
+
double v[3],represented[3];
|
| 81 |
+
for(int d=0;d<3;++d) {
|
| 82 |
+
const int64_t cell=cells.data()[(b*steps+t)*3+d];
|
| 83 |
+
if(cell<0 || cell>=table.shape(1))throw std::invalid_argument("invalid primitive cell");
|
| 84 |
+
v[d]=table.data()[d*table.shape(1)+cell];
|
| 85 |
+
}
|
| 86 |
+
if(primitive_order==2) {
|
| 87 |
+
const double norm=std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
|
| 88 |
+
const double factor=std::min(1.,radius/std::max(norm,std::numeric_limits<double>::min()));
|
| 89 |
+
for(double& x:v)x*=factor;
|
| 90 |
+
increment=apply(increment,ac2q::from_rotvec(v));
|
| 91 |
+
state=apply(state,increment);
|
| 92 |
+
auto q=input_order==1?increment:state;
|
| 93 |
+
if(rotation_vector)ac2q::to_rotvec(q,represented);else ac2q::to_euler(q,represented);
|
| 94 |
+
} else if(input_order==1) {
|
| 95 |
+
if(rotation_vector)std::copy(v,v+3,represented);
|
| 96 |
+
else {for(double& x:v)x=static_cast<float>(x);ac2q::to_euler(ac2q::from_rotvec(v),represented);}
|
| 97 |
+
} else {
|
| 98 |
+
state=apply(state,ac2q::from_rotvec(v));
|
| 99 |
+
if(rotation_vector)ac2q::to_rotvec(state,represented);else ac2q::to_euler(state,represented);
|
| 100 |
+
}
|
| 101 |
+
for(int d=0;d<3;++d) {
|
| 102 |
+
float value=static_cast<float>(represented[d]/scale.data()[d]);
|
| 103 |
+
if(primitive_order==1 && input_order==1) {
|
| 104 |
+
const float next=accumulated[d]+value;value=next-accumulated[d];accumulated[d]=next;
|
| 105 |
+
}
|
| 106 |
+
output.mutable_data()[(b*steps+t)*3+d]=value;
|
| 107 |
+
}
|
| 108 |
+
}
|
| 109 |
+
});
|
| 110 |
+
return output;
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
F32 additive_reconstruct(const F64& table, const I64& cells, const F32& previous,
|
| 114 |
+
const F32& scale, int input_order, int primitive_order) {
|
| 115 |
+
if (cells.ndim() != 3 || table.ndim() != 2 || scale.ndim() != 1 ||
|
| 116 |
+
cells.shape(2) != table.shape(0) || scale.size() != cells.shape(2) ||
|
| 117 |
+
previous.ndim() != 2 || previous.shape(0) != cells.shape(0) || previous.shape(1) != cells.shape(2))
|
| 118 |
+
throw std::invalid_argument("invalid additive reconstruction shapes");
|
| 119 |
+
if (primitive_order != 2 || input_order < 0 || input_order > 2)
|
| 120 |
+
throw std::invalid_argument("invalid differential order");
|
| 121 |
+
const int64_t batch = cells.shape(0), steps = cells.shape(1), width = cells.shape(2);
|
| 122 |
+
F32 output({batch, steps, width});
|
| 123 |
+
py::gil_scoped_release release;
|
| 124 |
+
ac2::parallel_for(batch, 0, [&](int64_t b) {
|
| 125 |
+
for (int64_t d = 0; d < width; ++d) {
|
| 126 |
+
double change_sum = 0, position = 0;
|
| 127 |
+
const float boundary = previous.data()[b * width + d] * scale.data()[d];
|
| 128 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 129 |
+
const int64_t index = (b * steps + t) * width + d, cell = cells.data()[index];
|
| 130 |
+
if (cell < 0 || cell >= table.shape(1)) throw std::invalid_argument("invalid cell");
|
| 131 |
+
const double primitive = table.data()[d * table.shape(1) + cell];
|
| 132 |
+
change_sum += primitive;
|
| 133 |
+
const double velocity = change_sum + boundary;
|
| 134 |
+
position += velocity;
|
| 135 |
+
const double value = input_order == 2 ? primitive : input_order == 1 ? velocity : position;
|
| 136 |
+
output.mutable_data()[index] = static_cast<float>(value / scale.data()[d]);
|
| 137 |
+
}
|
| 138 |
+
}
|
| 139 |
+
});
|
| 140 |
+
return output;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
I64 choose_cells(const F32& table,const I64& bins,const F32& request,
|
| 144 |
+
const F32& direction,const F32& limits,bool smooth) {
|
| 145 |
+
if(table.ndim()!=2 || request.ndim()!=2 || direction.ndim()!=2 ||
|
| 146 |
+
direction.shape(0)!=request.shape(0) || direction.shape(1)!=request.shape(1) ||
|
| 147 |
+
table.shape(0)!=request.shape(1) || bins.size()!=request.shape(1) || limits.size()!=request.shape(1))
|
| 148 |
+
throw std::invalid_argument("invalid primitive selection shapes");
|
| 149 |
+
I64 output({request.shape(0),request.shape(1)});
|
| 150 |
+
py::gil_scoped_release release;
|
| 151 |
+
const int64_t width=request.shape(1);
|
| 152 |
+
for(int64_t i=0;i<request.shape(0);++i)for(int64_t d=0;d<width;++d) {
|
| 153 |
+
int64_t nearest=0,feasible=-1,preferred=-1;
|
| 154 |
+
float best=INFINITY,best_feasible=INFINITY,best_preferred=INFINITY;
|
| 155 |
+
const float q=request.data()[i*width+d],sign=direction.data()[i*width+d];
|
| 156 |
+
for(int64_t j=0;j<bins.data()[d];++j) {
|
| 157 |
+
float p=table.data()[d*table.shape(1)+j],distance=std::abs(q-p);
|
| 158 |
+
if(distance<best){best=distance;nearest=j;}
|
| 159 |
+
if(distance<=limits.data()[d]+8*std::numeric_limits<float>::epsilon()) {
|
| 160 |
+
if(distance<best_feasible){best_feasible=distance;feasible=j;}
|
| 161 |
+
if((p>0?1.F:p<0?-1.F:0.F)*sign>=0 && distance<best_preferred) {
|
| 162 |
+
best_preferred=distance;preferred=j;
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
output.mutable_data()[i*width+d]=smooth?(preferred>=0?preferred:feasible>=0?feasible:nearest):nearest;
|
| 167 |
+
}
|
| 168 |
+
return output;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
F32 additive_decode(const F32& table,const I64& cells,const F32& scale,
|
| 172 |
+
const F32& lower,const F32& upper,int input_order,int primitive_order) {
|
| 173 |
+
if(cells.ndim()!=3 || table.ndim()!=2 || table.shape(0)!=cells.shape(2) ||
|
| 174 |
+
scale.size()!=cells.shape(2) || lower.size()!=cells.shape(2) || upper.size()!=cells.shape(2) ||
|
| 175 |
+
primitive_order<0 || primitive_order>1 || input_order<0 || input_order>2)
|
| 176 |
+
throw std::invalid_argument("invalid additive decoding metadata");
|
| 177 |
+
const int64_t batch=cells.shape(0),steps=cells.shape(1),width=cells.shape(2);
|
| 178 |
+
F32 output({batch,steps,width});
|
| 179 |
+
py::gil_scoped_release release;
|
| 180 |
+
ac2::parallel_for(batch,0,[&](int64_t b) {
|
| 181 |
+
for(int64_t d=0;d<width;++d) {
|
| 182 |
+
float position=0;
|
| 183 |
+
for(int64_t t=0;t<steps;++t) {
|
| 184 |
+
const int64_t index=(b*steps+t)*width+d,cell=cells.data()[index];
|
| 185 |
+
if(cell<0 || cell>=table.shape(1))throw std::invalid_argument("invalid primitive cell");
|
| 186 |
+
const float picked=table.data()[d*table.shape(1)+cell];
|
| 187 |
+
position=primitive_order?std::clamp(position+picked,lower.data()[d],upper.data()[d]):picked;
|
| 188 |
+
output.mutable_data()[index]=position;
|
| 189 |
+
}
|
| 190 |
+
for(int order=0;order<input_order;++order)for(int64_t t=steps-1;t>0;--t)
|
| 191 |
+
output.mutable_data()[(b*steps+t)*width+d]-=output.data()[(b*steps+t-1)*width+d];
|
| 192 |
+
for(int64_t t=0;t<steps;++t)output.mutable_data()[(b*steps+t)*width+d]/=scale.data()[d];
|
| 193 |
+
}
|
| 194 |
+
});
|
| 195 |
+
return output;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
I64 spatial_expand(const std::vector<int64_t>& tokens,const I64& expansions,
|
| 199 |
+
const I64& supports,uint64_t full) {
|
| 200 |
+
if(expansions.ndim()!=2 || supports.ndim()!=1 || expansions.shape(0)!=supports.size())
|
| 201 |
+
throw std::invalid_argument("invalid spatial metadata");
|
| 202 |
+
const int64_t width=expansions.shape(1);I64 output(width);
|
| 203 |
+
std::fill(output.mutable_data(),output.mutable_data()+width,-1);
|
| 204 |
+
uint64_t covered=0;
|
| 205 |
+
py::gil_scoped_release release;
|
| 206 |
+
for(int64_t token:tokens) {
|
| 207 |
+
if(token<0 || token>=supports.size())throw std::invalid_argument("token is outside the vocabulary");
|
| 208 |
+
const uint64_t support=supports.data()[token];
|
| 209 |
+
if(covered & support)throw std::invalid_argument("token supports overlap");
|
| 210 |
+
covered|=support;
|
| 211 |
+
for(int64_t d=0;d<width;++d) {
|
| 212 |
+
const int64_t value=expansions.data()[token*width+d];
|
| 213 |
+
if(value>=0)output.mutable_data()[d]=value;
|
| 214 |
+
}
|
| 215 |
+
}
|
| 216 |
+
if(covered!=full)throw std::invalid_argument("token set does not exactly cover the requested active dimensions");
|
| 217 |
+
return output;
|
| 218 |
+
}
|
| 219 |
+
F32 additive_transform(const F32& values,int order,bool integrate) {
|
| 220 |
+
if(values.ndim()!=3 || order<0 || order>2)throw std::invalid_argument("expected [B,T,D] and order 0..2");
|
| 221 |
+
const int64_t batch=values.shape(0),steps=values.shape(1),width=values.shape(2);
|
| 222 |
+
F32 output({batch,steps,width});std::copy(values.data(),values.data()+values.size(),output.mutable_data());
|
| 223 |
+
py::gil_scoped_release release;
|
| 224 |
+
for(int level=0;level<order;++level)for(int64_t b=0;b<batch;++b)for(int64_t d=0;d<width;++d) {
|
| 225 |
+
if(integrate) {
|
| 226 |
+
float sum=0;
|
| 227 |
+
for(int64_t t=0;t<steps;++t) {const int64_t i=(b*steps+t)*width+d;sum+=output.data()[i];output.mutable_data()[i]=sum;}
|
| 228 |
+
} else {
|
| 229 |
+
for(int64_t t=steps-1;t>0;--t)output.mutable_data()[(b*steps+t)*width+d]-=output.data()[(b*steps+t-1)*width+d];
|
| 230 |
+
}
|
| 231 |
+
}
|
| 232 |
+
return output;
|
| 233 |
+
}
|
| 234 |
+
} // namespace
|
| 235 |
+
|
| 236 |
+
void register_inference(py::module_& module) {
|
| 237 |
+
module.def("additive_reconstruct", &additive_reconstruct);
|
| 238 |
+
module.def("so3_reconstruct", &so3_reconstruct);
|
| 239 |
+
module.def("quaternion_map", &quaternion_map);
|
| 240 |
+
module.def("quaternion_multiply", &quaternion_multiply);
|
| 241 |
+
module.def("choose_cells", &choose_cells);
|
| 242 |
+
module.def("additive_decode", &additive_decode);
|
| 243 |
+
module.def("spatial_expand", &spatial_expand);
|
| 244 |
+
module.def("additive_transform", &additive_transform);
|
| 245 |
+
}
|
actioncodec2_kernels/module.cpp
CHANGED
|
@@ -1,13 +1,25 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
#include <pybind11/pybind11.h>
|
|
|
|
|
|
|
| 3 |
|
| 4 |
#include <cstdint>
|
| 5 |
-
#include <
|
| 6 |
-
|
| 7 |
-
#include "parallel.hpp"
|
| 8 |
|
| 9 |
namespace py = pybind11;
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
py::array_t<int32_t> so3_second_order_encode_batch(
|
| 12 |
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 13 |
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
|
@@ -22,6 +34,15 @@ py::array_t<int32_t> additive_second_order_encode_batch(
|
|
| 22 |
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 23 |
const py::array_t<float, py::array::c_style | py::array::forcecast>&, bool, int);
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
py::array_t<float> physical_decode_batch(
|
| 26 |
const py::array_t<float>&, const py::array_t<int32_t>&, const py::array_t<uint8_t>&,
|
| 27 |
const py::array_t<float>&, const py::array_t<float>&,
|
|
@@ -41,30 +62,24 @@ py::array_t<int32_t> so3_encode_batch(
|
|
| 41 |
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 42 |
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
| 43 |
const py::array_t<float, py::array::c_style | py::array::forcecast>&, int32_t, bool, bool,
|
| 44 |
-
double,
|
| 45 |
|
| 46 |
-
|
| 47 |
-
void register_resample(py::module_& module);
|
| 48 |
-
void register_reconstruction(py::module_& module);
|
| 49 |
-
void register_grammar(py::module_& module);
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
register_resample(module);
|
| 55 |
-
|
|
|
|
|
|
|
| 56 |
register_grammar(module);
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
[](int threads) {
|
| 60 |
-
if (threads < 0) throw std::invalid_argument("thread budget must be non-negative");
|
| 61 |
-
ac2::thread_budget().store(threads);
|
| 62 |
-
},
|
| 63 |
-
py::arg("threads"),
|
| 64 |
-
"Cap threads per call for this process; 0 uses every CPU it may run on.");
|
| 65 |
-
module.def("thread_budget", [] { return ac2::thread_budget().load(); });
|
| 66 |
-
module.def("plan_threads", &ac2::plan_threads, py::arg("count"), py::arg("requested"),
|
| 67 |
-
py::arg("seconds_each"), "Threads a call would use for ``count`` items.");
|
| 68 |
module.def("so3_second_order_encode_batch", &so3_second_order_encode_batch,
|
| 69 |
py::arg("table"), py::arg("bins"), py::arg("quaternions"),
|
| 70 |
py::arg("previous_first_order"), py::arg("input_order"), py::arg("left"),
|
|
@@ -73,16 +88,39 @@ PYBIND11_MODULE(_actioncodec2_native, module) {
|
|
| 73 |
py::arg("table"), py::arg("bins"), py::arg("positions"),
|
| 74 |
py::arg("previous_first_order"), py::arg("epsilon"), py::arg("smooth"),
|
| 75 |
py::arg("threads") = 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
module.def("physical_decode_batch", &physical_decode_batch, py::arg("table"),
|
| 77 |
py::arg("bins"), py::arg("binary"), py::arg("lower"), py::arg("upper"),
|
| 78 |
py::arg("cells"), py::arg("initial"), py::arg("initial_velocity"),
|
| 79 |
py::arg("order"), py::arg("threads") = 0);
|
|
|
|
| 80 |
module.def("physical_encode_batch", &physical_encode_batch, py::arg("table"), py::arg("bins"),
|
| 81 |
py::arg("binary"), py::arg("lower"), py::arg("upper"), py::arg("positions"),
|
| 82 |
py::arg("initial"), py::arg("initial_velocity"), py::arg("order"),
|
| 83 |
py::arg("threads") = 0, py::arg("epsilon") = py::array_t<float>(),
|
| 84 |
py::arg("smooth") = false);
|
|
|
|
| 85 |
module.def("so3_encode_batch", &so3_encode_batch, py::arg("table"), py::arg("bins"),
|
| 86 |
py::arg("actions"), py::arg("input_order"), py::arg("rotation_vector"),
|
| 87 |
-
py::arg("left"), py::arg("limit"), py::arg("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
}
|
|
|
|
| 1 |
+
// pybind11 entry point for the ActionCodec2 native kernels.
|
| 2 |
+
//
|
| 3 |
+
// Every function here has an exactly equivalent Python implementation in the
|
| 4 |
+
// package. The native module is a pure accelerator: if it is not built, the
|
| 5 |
+
// package still works, and the test suite asserts the two agree bit for bit.
|
| 6 |
+
|
| 7 |
#include <pybind11/pybind11.h>
|
| 8 |
+
#include <pybind11/numpy.h>
|
| 9 |
+
#include <pybind11/stl.h>
|
| 10 |
|
| 11 |
#include <cstdint>
|
| 12 |
+
#include <vector>
|
|
|
|
|
|
|
| 13 |
|
| 14 |
namespace py = pybind11;
|
| 15 |
|
| 16 |
+
void register_resample(py::module_&);
|
| 17 |
+
void register_setbpe(py::module_&);
|
| 18 |
+
void register_inference(py::module_&);
|
| 19 |
+
void register_preprocessing(py::module_&);
|
| 20 |
+
void register_grammar(py::module_&);
|
| 21 |
+
void register_sparse(py::module_&);
|
| 22 |
+
|
| 23 |
py::array_t<int32_t> so3_second_order_encode_batch(
|
| 24 |
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 25 |
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
|
|
|
| 34 |
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 35 |
const py::array_t<float, py::array::c_style | py::array::forcecast>&, bool, int);
|
| 36 |
|
| 37 |
+
py::tuple train_spatial_native(py::array_t<int32_t, py::array::c_style | py::array::forcecast>,
|
| 38 |
+
py::array_t<int64_t, py::array::c_style | py::array::forcecast>,
|
| 39 |
+
const std::vector<int32_t>&, int64_t, int);
|
| 40 |
+
|
| 41 |
+
py::tuple train_temporal_native(py::iterable, int64_t,
|
| 42 |
+
py::array_t<uint64_t, py::array::c_style | py::array::forcecast>,
|
| 43 |
+
py::array_t<int32_t, py::array::c_style | py::array::forcecast>,
|
| 44 |
+
int32_t, int32_t, int64_t, int64_t, int);
|
| 45 |
+
|
| 46 |
py::array_t<float> physical_decode_batch(
|
| 47 |
const py::array_t<float>&, const py::array_t<int32_t>&, const py::array_t<uint8_t>&,
|
| 48 |
const py::array_t<float>&, const py::array_t<float>&,
|
|
|
|
| 62 |
const py::array_t<double, py::array::c_style | py::array::forcecast>&,
|
| 63 |
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
| 64 |
const py::array_t<float, py::array::c_style | py::array::forcecast>&, int32_t, bool, bool,
|
| 65 |
+
double, int32_t, int);
|
| 66 |
|
| 67 |
+
py::tuple train_bpe_native(py::iterable, const std::vector<int32_t>&, int64_t, int64_t, int);
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
+
py::tuple spatial_encode_instances(
|
| 70 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&,
|
| 71 |
+
const py::array_t<int64_t, py::array::c_style | py::array::forcecast>&,
|
| 72 |
+
const std::vector<int32_t>&,
|
| 73 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>&, int);
|
| 74 |
+
|
| 75 |
+
PYBIND11_MODULE(_native, module) {
|
| 76 |
register_resample(module);
|
| 77 |
+
register_setbpe(module);
|
| 78 |
+
register_inference(module);
|
| 79 |
+
register_preprocessing(module);
|
| 80 |
register_grammar(module);
|
| 81 |
+
register_sparse(module);
|
| 82 |
+
module.doc() = "ActionCodec2 native kernels (Set-BPE training and batched physical stage)";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
module.def("so3_second_order_encode_batch", &so3_second_order_encode_batch,
|
| 84 |
py::arg("table"), py::arg("bins"), py::arg("quaternions"),
|
| 85 |
py::arg("previous_first_order"), py::arg("input_order"), py::arg("left"),
|
|
|
|
| 88 |
py::arg("table"), py::arg("bins"), py::arg("positions"),
|
| 89 |
py::arg("previous_first_order"), py::arg("epsilon"), py::arg("smooth"),
|
| 90 |
py::arg("threads") = 0);
|
| 91 |
+
|
| 92 |
+
module.def("train_spatial", &train_spatial_native, py::arg("codes"), py::arg("weights"),
|
| 93 |
+
py::arg("bins"), py::arg("vocab_size"), py::arg("threads") = 0,
|
| 94 |
+
"Exact greedy spatial Set-BPE; returns (merges, report).");
|
| 95 |
+
|
| 96 |
+
module.def("train_temporal", &train_temporal_native, py::arg("instance_tokens"),
|
| 97 |
+
py::arg("total_anchors"), py::arg("spatial_support"),
|
| 98 |
+
py::arg("spatial_anchor"), py::arg("dimension"), py::arg("lookahead"),
|
| 99 |
+
py::arg("vocab_size"), py::arg("action_count"), py::arg("threads") = 0,
|
| 100 |
+
"Exact greedy rooted temporal Set-BPE; returns (merges, report).");
|
| 101 |
+
|
| 102 |
+
module.def("train_bpe", &train_bpe_native, py::arg("cell_chunks"), py::arg("bins"),
|
| 103 |
+
py::arg("vocab_size"), py::arg("max_token_cells"), py::arg("threads") = 0,
|
| 104 |
+
"Exact greedy BPE over the row-major flattened cell stream; returns (merges, report).");
|
| 105 |
+
|
| 106 |
module.def("physical_decode_batch", &physical_decode_batch, py::arg("table"),
|
| 107 |
py::arg("bins"), py::arg("binary"), py::arg("lower"), py::arg("upper"),
|
| 108 |
py::arg("cells"), py::arg("initial"), py::arg("initial_velocity"),
|
| 109 |
py::arg("order"), py::arg("threads") = 0);
|
| 110 |
+
|
| 111 |
module.def("physical_encode_batch", &physical_encode_batch, py::arg("table"), py::arg("bins"),
|
| 112 |
py::arg("binary"), py::arg("lower"), py::arg("upper"), py::arg("positions"),
|
| 113 |
py::arg("initial"), py::arg("initial_velocity"), py::arg("order"),
|
| 114 |
py::arg("threads") = 0, py::arg("epsilon") = py::array_t<float>(),
|
| 115 |
py::arg("smooth") = false);
|
| 116 |
+
|
| 117 |
module.def("so3_encode_batch", &so3_encode_batch, py::arg("table"), py::arg("bins"),
|
| 118 |
py::arg("actions"), py::arg("input_order"), py::arg("rotation_vector"),
|
| 119 |
+
py::arg("left"), py::arg("limit"), py::arg("selection"), py::arg("threads") = 0,
|
| 120 |
+
"Batched exact SO(3) error-feedback encoding; selection 0 nearest, 1 smooth, "
|
| 121 |
+
"2 momentum_lazy.");
|
| 122 |
+
|
| 123 |
+
module.def("spatial_encode_instances", &spatial_encode_instances, py::arg("codes"),
|
| 124 |
+
py::arg("row_anchors"), py::arg("bins"), py::arg("merges"),
|
| 125 |
+
py::arg("threads") = 0);
|
| 126 |
}
|
actioncodec2_kernels/parallel.hpp
CHANGED
|
@@ -16,6 +16,7 @@
|
|
| 16 |
|
| 17 |
namespace ac2 {
|
| 18 |
|
|
|
|
| 19 |
inline std::atomic<int>& thread_budget() {
|
| 20 |
static std::atomic<int> value{0};
|
| 21 |
return value;
|
|
@@ -32,8 +33,11 @@ inline int available_cpus() {
|
|
| 32 |
return std::max(1, static_cast<int>(std::thread::hardware_concurrency()));
|
| 33 |
}
|
| 34 |
|
|
|
|
| 35 |
constexpr double kThreadStartSeconds = 30e-6;
|
| 36 |
|
|
|
|
|
|
|
| 37 |
inline int64_t plan_threads(int64_t count, int requested, double seconds_each) {
|
| 38 |
const int budget = thread_budget().load();
|
| 39 |
int64_t threads = budget > 0 ? budget : available_cpus();
|
|
@@ -85,4 +89,4 @@ void parallel_for(int64_t count, int threads, Body&& body) {
|
|
| 85 |
if (failure) std::rethrow_exception(failure);
|
| 86 |
}
|
| 87 |
|
| 88 |
-
}
|
|
|
|
| 16 |
|
| 17 |
namespace ac2 {
|
| 18 |
|
| 19 |
+
// Process-wide cap on threads per call; 0 means every CPU the process may use.
|
| 20 |
inline std::atomic<int>& thread_budget() {
|
| 21 |
static std::atomic<int> value{0};
|
| 22 |
return value;
|
|
|
|
| 33 |
return std::max(1, static_cast<int>(std::thread::hardware_concurrency()));
|
| 34 |
}
|
| 35 |
|
| 36 |
+
// Measured cost of starting and joining one thread (seconds).
|
| 37 |
constexpr double kThreadStartSeconds = 30e-6;
|
| 38 |
|
| 39 |
+
// Threads for ``count`` iterations of ``seconds_each``: at most ``requested``
|
| 40 |
+
// when positive, at most the process budget, and at most ``sqrt(W / s)``.
|
| 41 |
inline int64_t plan_threads(int64_t count, int requested, double seconds_each) {
|
| 42 |
const int budget = thread_budget().load();
|
| 43 |
int64_t threads = budget > 0 ? budget : available_cpus();
|
|
|
|
| 89 |
if (failure) std::rethrow_exception(failure);
|
| 90 |
}
|
| 91 |
|
| 92 |
+
} // namespace ac2
|
actioncodec2_kernels/physical.cpp
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
#include <pybind11/pybind11.h>
|
| 2 |
#include <pybind11/numpy.h>
|
| 3 |
#include <pybind11/stl.h>
|
|
@@ -6,7 +7,6 @@
|
|
| 6 |
#include <cfloat>
|
| 7 |
#include <cmath>
|
| 8 |
#include <cstdint>
|
| 9 |
-
#include <limits>
|
| 10 |
#include <vector>
|
| 11 |
|
| 12 |
#include "parallel.hpp"
|
|
@@ -33,7 +33,7 @@ inline int32_t pick_second_order_axis(const SecondOrderAxis& axis, double reques
|
|
| 33 |
const int32_t right = std::min(insertion, axis.count - 1);
|
| 34 |
int32_t nearest = std::fabs(request - values[left]) <= std::fabs(request - values[right])
|
| 35 |
? left : right;
|
| 36 |
-
|
| 37 |
while (nearest > 0 && std::fabs(request - values[nearest - 1]) == std::fabs(request - values[nearest]))
|
| 38 |
--nearest;
|
| 39 |
if constexpr (!Smooth) return nearest;
|
|
@@ -43,7 +43,7 @@ inline int32_t pick_second_order_axis(const SecondOrderAxis& axis, double reques
|
|
| 43 |
const double sign = primitive > 0 ? 1 : primitive < 0 ? -1 : 0;
|
| 44 |
return sign * direction >= 0 && std::fabs(request - primitive) <= epsilon ? preferred : nearest;
|
| 45 |
}
|
| 46 |
-
|
| 47 |
int32_t nearest = 0, feasible = -1, preferred = -1;
|
| 48 |
double nearest_distance = INFINITY, feasible_distance = INFINITY, preferred_distance = INFINITY;
|
| 49 |
for (int32_t k = 0; k < axis.count; ++k) {
|
|
@@ -62,7 +62,7 @@ inline int32_t pick_second_order_axis(const SecondOrderAxis& axis, double reques
|
|
| 62 |
return preferred >= 0 ? preferred : feasible >= 0 ? feasible : nearest;
|
| 63 |
}
|
| 64 |
|
| 65 |
-
}
|
| 66 |
|
| 67 |
py::array_t<int32_t> additive_second_order_encode_batch(
|
| 68 |
const py::array_t<float, py::array::c_style | py::array::forcecast>& table,
|
|
@@ -124,16 +124,17 @@ namespace {
|
|
| 124 |
|
| 125 |
struct Layout {
|
| 126 |
int32_t dimension;
|
| 127 |
-
int32_t width;
|
| 128 |
-
const float* table;
|
| 129 |
-
const int32_t* bins;
|
| 130 |
-
const uint8_t* binary;
|
| 131 |
const float* lower;
|
| 132 |
const float* upper;
|
| 133 |
-
const float* epsilon;
|
| 134 |
-
bool smooth;
|
| 135 |
};
|
| 136 |
|
|
|
|
| 137 |
constexpr float kSourceTolerance = 8.0F * FLT_EPSILON;
|
| 138 |
|
| 139 |
inline int32_t nearest(const Layout& layout, int32_t axis, float request) {
|
|
@@ -175,7 +176,7 @@ inline int32_t select_smooth(const Layout& layout, int32_t axis, float request,
|
|
| 175 |
feasible_distance = distance;
|
| 176 |
feasible_index = index;
|
| 177 |
}
|
| 178 |
-
|
| 179 |
const float sign = values[index] > 0.0F ? 1.0F : (values[index] < 0.0F ? -1.0F : 0.0F);
|
| 180 |
if (!(sign * direction >= 0.0F)) continue;
|
| 181 |
if (!any_preferred || distance < preferred_distance) {
|
|
@@ -193,7 +194,7 @@ void encode_first_order(const Layout& layout, const float* positions, int64_t st
|
|
| 193 |
const float* initial, int32_t* cells) {
|
| 194 |
const int32_t dimension = layout.dimension;
|
| 195 |
std::vector<float> state(initial, initial + dimension);
|
| 196 |
-
|
| 197 |
std::vector<float> direction(dimension, 0.0F);
|
| 198 |
for (int64_t step = 0; step < steps; ++step) {
|
| 199 |
const float* target = positions + step * dimension;
|
|
@@ -297,47 +298,6 @@ Layout make_layout(const py::array_t<float>& table, const py::array_t<int32_t>&
|
|
| 297 |
const py::array_t<uint8_t>& binary, const py::array_t<float>& lower,
|
| 298 |
const py::array_t<float>& upper, const py::array_t<float>& epsilon,
|
| 299 |
bool smooth) {
|
| 300 |
-
if (table.ndim() != 2 || table.shape(0) < 1 || table.shape(1) < 1 ||
|
| 301 |
-
table.shape(0) > std::numeric_limits<int32_t>::max() ||
|
| 302 |
-
table.shape(1) > std::numeric_limits<int32_t>::max()) {
|
| 303 |
-
throw std::invalid_argument("table must have non-empty shape [D, K]");
|
| 304 |
-
}
|
| 305 |
-
for (const py::array* array : {static_cast<const py::array*>(&table),
|
| 306 |
-
static_cast<const py::array*>(&bins),
|
| 307 |
-
static_cast<const py::array*>(&binary),
|
| 308 |
-
static_cast<const py::array*>(&lower),
|
| 309 |
-
static_cast<const py::array*>(&upper),
|
| 310 |
-
static_cast<const py::array*>(&epsilon)}) {
|
| 311 |
-
if (!(array->flags() & py::array::c_style)) {
|
| 312 |
-
throw std::invalid_argument("physical layout arrays must be C-contiguous");
|
| 313 |
-
}
|
| 314 |
-
}
|
| 315 |
-
const auto dimension = table.shape(0);
|
| 316 |
-
if (bins.ndim() != 1 || binary.ndim() != 1 || lower.ndim() != 1 || upper.ndim() != 1 ||
|
| 317 |
-
bins.size() != dimension || binary.size() != dimension ||
|
| 318 |
-
lower.size() != dimension || upper.size() != dimension) {
|
| 319 |
-
throw std::invalid_argument("layout vectors must have shape [D]");
|
| 320 |
-
}
|
| 321 |
-
if ((epsilon.size() && (epsilon.ndim() != 1 || epsilon.size() != dimension)) ||
|
| 322 |
-
(smooth && !epsilon.size())) {
|
| 323 |
-
throw std::invalid_argument("smooth selection needs epsilon with shape [D]");
|
| 324 |
-
}
|
| 325 |
-
for (py::ssize_t axis = 0; axis < dimension; ++axis) {
|
| 326 |
-
const int32_t count = bins.data()[axis];
|
| 327 |
-
if (count < 1 || count > table.shape(1) || binary.data()[axis] > 1 ||
|
| 328 |
-
!(lower.data()[axis] <= upper.data()[axis])) {
|
| 329 |
-
throw std::invalid_argument("invalid physical layout bounds or primitive count");
|
| 330 |
-
}
|
| 331 |
-
if (epsilon.size() &&
|
| 332 |
-
!(std::isfinite(epsilon.data()[axis]) && epsilon.data()[axis] >= 0)) {
|
| 333 |
-
throw std::invalid_argument("epsilon must be finite and non-negative");
|
| 334 |
-
}
|
| 335 |
-
for (int32_t cell = 0; cell < count; ++cell) {
|
| 336 |
-
if (!std::isfinite(table.data()[axis * table.shape(1) + cell])) {
|
| 337 |
-
throw std::invalid_argument("active primitives must be finite");
|
| 338 |
-
}
|
| 339 |
-
}
|
| 340 |
-
}
|
| 341 |
Layout layout;
|
| 342 |
layout.dimension = static_cast<int32_t>(table.shape(0));
|
| 343 |
layout.width = static_cast<int32_t>(table.shape(1));
|
|
@@ -348,6 +308,12 @@ Layout make_layout(const py::array_t<float>& table, const py::array_t<int32_t>&
|
|
| 348 |
layout.upper = upper.data();
|
| 349 |
layout.epsilon = epsilon.size() == 0 ? nullptr : epsilon.data();
|
| 350 |
layout.smooth = smooth && layout.epsilon != nullptr;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 351 |
return layout;
|
| 352 |
}
|
| 353 |
|
|
@@ -355,7 +321,7 @@ using Array2D = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
|
| 355 |
using Array3D = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 356 |
using Cells3D = py::array_t<int32_t, py::array::c_style | py::array::forcecast>;
|
| 357 |
|
| 358 |
-
}
|
| 359 |
|
| 360 |
py::array_t<float> physical_decode_batch(const py::array_t<float>& table,
|
| 361 |
const py::array_t<int32_t>& bins,
|
|
@@ -365,29 +331,21 @@ py::array_t<float> physical_decode_batch(const py::array_t<float>& table,
|
|
| 365 |
const Array2D& initial,
|
| 366 |
const Array2D& initial_velocity, int32_t order,
|
| 367 |
int threads) {
|
| 368 |
-
|
| 369 |
-
make_layout(table, bins, binary, lower, upper, py::array_t<float>(), false);
|
| 370 |
-
if (cells.ndim() != 3 || cells.shape(1) < 1 || cells.shape(2) != layout.dimension || initial.ndim() != 2 ||
|
| 371 |
initial_velocity.ndim() != 2 || initial.shape(0) != cells.shape(0) ||
|
| 372 |
initial_velocity.shape(0) != cells.shape(0) || initial.shape(1) != cells.shape(2) ||
|
| 373 |
initial_velocity.shape(1) != cells.shape(2) || (order != 1 && order != 2)) {
|
| 374 |
throw std::invalid_argument("invalid physical decode batch shapes or order");
|
| 375 |
}
|
|
|
|
|
|
|
| 376 |
const int64_t batch = cells.shape(0);
|
| 377 |
const int64_t steps = cells.shape(1);
|
| 378 |
const int64_t dimension = layout.dimension;
|
| 379 |
-
for (py::ssize_t index = 0; index < initial.size(); ++index) {
|
| 380 |
-
if (!std::isfinite(initial.data()[index]) || !std::isfinite(initial_velocity.data()[index])) {
|
| 381 |
-
throw std::invalid_argument("initial state and velocity must be finite");
|
| 382 |
-
}
|
| 383 |
-
}
|
| 384 |
-
for (py::ssize_t index = 0; index < cells.size(); ++index) {
|
| 385 |
-
const int32_t cell = cells.data()[index];
|
| 386 |
-
if (cell < 0 || cell >= layout.bins[index % dimension]) {
|
| 387 |
-
throw std::invalid_argument("cell index is outside its primitive table");
|
| 388 |
-
}
|
| 389 |
-
}
|
| 390 |
py::array_t<float> positions({batch, steps, dimension});
|
|
|
|
|
|
|
|
|
|
| 391 |
{
|
| 392 |
py::gil_scoped_release release;
|
| 393 |
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
|
@@ -404,6 +362,7 @@ py::array_t<float> physical_decode_batch(const py::array_t<float>& table,
|
|
| 404 |
return positions;
|
| 405 |
}
|
| 406 |
|
|
|
|
| 407 |
py::array_t<int32_t> physical_encode_batch(const py::array_t<float>& table,
|
| 408 |
const py::array_t<int32_t>& bins,
|
| 409 |
const py::array_t<uint8_t>& binary,
|
|
@@ -414,24 +373,6 @@ py::array_t<int32_t> physical_encode_batch(const py::array_t<float>& table,
|
|
| 414 |
int threads, const py::array_t<float>& epsilon,
|
| 415 |
bool smooth) {
|
| 416 |
const Layout layout = make_layout(table, bins, binary, lower, upper, epsilon, smooth);
|
| 417 |
-
if (positions.ndim() != 3 || positions.shape(1) < 1 ||
|
| 418 |
-
positions.shape(2) != layout.dimension || initial.ndim() != 2 ||
|
| 419 |
-
initial_velocity.ndim() != 2 || initial.shape(0) != positions.shape(0) ||
|
| 420 |
-
initial_velocity.shape(0) != positions.shape(0) ||
|
| 421 |
-
initial.shape(1) != layout.dimension || initial_velocity.shape(1) != layout.dimension ||
|
| 422 |
-
(order != 1 && order != 2)) {
|
| 423 |
-
throw std::invalid_argument("invalid physical encode batch shapes or order");
|
| 424 |
-
}
|
| 425 |
-
for (py::ssize_t index = 0; index < positions.size(); ++index) {
|
| 426 |
-
if (!std::isfinite(positions.data()[index])) {
|
| 427 |
-
throw std::invalid_argument("positions must be finite");
|
| 428 |
-
}
|
| 429 |
-
}
|
| 430 |
-
for (py::ssize_t index = 0; index < initial.size(); ++index) {
|
| 431 |
-
if (!std::isfinite(initial.data()[index]) || !std::isfinite(initial_velocity.data()[index])) {
|
| 432 |
-
throw std::invalid_argument("initial state and velocity must be finite");
|
| 433 |
-
}
|
| 434 |
-
}
|
| 435 |
const int64_t batch = positions.shape(0);
|
| 436 |
const int64_t steps = positions.shape(1);
|
| 437 |
const int64_t dimension = layout.dimension;
|
|
@@ -440,6 +381,9 @@ py::array_t<int32_t> physical_encode_batch(const py::array_t<float>& table,
|
|
| 440 |
const float* starts = initial.data();
|
| 441 |
const float* velocities = initial_velocity.data();
|
| 442 |
int32_t* output = cells.mutable_data();
|
|
|
|
|
|
|
|
|
|
| 443 |
{
|
| 444 |
py::gil_scoped_release release;
|
| 445 |
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
|
|
|
| 1 |
+
|
| 2 |
#include <pybind11/pybind11.h>
|
| 3 |
#include <pybind11/numpy.h>
|
| 4 |
#include <pybind11/stl.h>
|
|
|
|
| 7 |
#include <cfloat>
|
| 8 |
#include <cmath>
|
| 9 |
#include <cstdint>
|
|
|
|
| 10 |
#include <vector>
|
| 11 |
|
| 12 |
#include "parallel.hpp"
|
|
|
|
| 33 |
const int32_t right = std::min(insertion, axis.count - 1);
|
| 34 |
int32_t nearest = std::fabs(request - values[left]) <= std::fabs(request - values[right])
|
| 35 |
? left : right;
|
| 36 |
+
// Rounded distance plateaus must keep the original first-cell tie-break.
|
| 37 |
while (nearest > 0 && std::fabs(request - values[nearest - 1]) == std::fabs(request - values[nearest]))
|
| 38 |
--nearest;
|
| 39 |
if constexpr (!Smooth) return nearest;
|
|
|
|
| 43 |
const double sign = primitive > 0 ? 1 : primitive < 0 ? -1 : 0;
|
| 44 |
return sign * direction >= 0 && std::fabs(request - primitive) <= epsilon ? preferred : nearest;
|
| 45 |
}
|
| 46 |
+
// Small tables are faster to scan; arbitrary stored order also uses this path.
|
| 47 |
int32_t nearest = 0, feasible = -1, preferred = -1;
|
| 48 |
double nearest_distance = INFINITY, feasible_distance = INFINITY, preferred_distance = INFINITY;
|
| 49 |
for (int32_t k = 0; k < axis.count; ++k) {
|
|
|
|
| 62 |
return preferred >= 0 ? preferred : feasible >= 0 ? feasible : nearest;
|
| 63 |
}
|
| 64 |
|
| 65 |
+
} // namespace
|
| 66 |
|
| 67 |
py::array_t<int32_t> additive_second_order_encode_batch(
|
| 68 |
const py::array_t<float, py::array::c_style | py::array::forcecast>& table,
|
|
|
|
| 124 |
|
| 125 |
struct Layout {
|
| 126 |
int32_t dimension;
|
| 127 |
+
int32_t width; // padded primitive-table stride
|
| 128 |
+
const float* table; // [dimension * width], +inf padding
|
| 129 |
+
const int32_t* bins; // [dimension]
|
| 130 |
+
const uint8_t* binary; // [dimension], 1 for absolute two-valued axes
|
| 131 |
const float* lower;
|
| 132 |
const float* upper;
|
| 133 |
+
const float* epsilon; // [dimension]; null unless `smooth` is set
|
| 134 |
+
bool smooth; // pick by FirstOrderCodec._select_smooth
|
| 135 |
};
|
| 136 |
|
| 137 |
+
// Matches PhysicalCodecBase.SOURCE_TOLERANCE.
|
| 138 |
constexpr float kSourceTolerance = 8.0F * FLT_EPSILON;
|
| 139 |
|
| 140 |
inline int32_t nearest(const Layout& layout, int32_t axis, float request) {
|
|
|
|
| 176 |
feasible_distance = distance;
|
| 177 |
feasible_index = index;
|
| 178 |
}
|
| 179 |
+
// np.sign(value) * direction >= 0: a zero primitive never reverses.
|
| 180 |
const float sign = values[index] > 0.0F ? 1.0F : (values[index] < 0.0F ? -1.0F : 0.0F);
|
| 181 |
if (!(sign * direction >= 0.0F)) continue;
|
| 182 |
if (!any_preferred || distance < preferred_distance) {
|
|
|
|
| 194 |
const float* initial, int32_t* cells) {
|
| 195 |
const int32_t dimension = layout.dimension;
|
| 196 |
std::vector<float> state(initial, initial + dimension);
|
| 197 |
+
// Sign of the last non-zero primitive each coordinate emitted.
|
| 198 |
std::vector<float> direction(dimension, 0.0F);
|
| 199 |
for (int64_t step = 0; step < steps; ++step) {
|
| 200 |
const float* target = positions + step * dimension;
|
|
|
|
| 298 |
const py::array_t<uint8_t>& binary, const py::array_t<float>& lower,
|
| 299 |
const py::array_t<float>& upper, const py::array_t<float>& epsilon,
|
| 300 |
bool smooth) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
Layout layout;
|
| 302 |
layout.dimension = static_cast<int32_t>(table.shape(0));
|
| 303 |
layout.width = static_cast<int32_t>(table.shape(1));
|
|
|
|
| 308 |
layout.upper = upper.data();
|
| 309 |
layout.epsilon = epsilon.size() == 0 ? nullptr : epsilon.data();
|
| 310 |
layout.smooth = smooth && layout.epsilon != nullptr;
|
| 311 |
+
if (smooth && layout.epsilon == nullptr) {
|
| 312 |
+
throw std::invalid_argument("smooth selection needs a per-coordinate epsilon");
|
| 313 |
+
}
|
| 314 |
+
if (layout.epsilon != nullptr && epsilon.size() != layout.dimension) {
|
| 315 |
+
throw std::invalid_argument("epsilon must have one entry per coordinate");
|
| 316 |
+
}
|
| 317 |
return layout;
|
| 318 |
}
|
| 319 |
|
|
|
|
| 321 |
using Array3D = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 322 |
using Cells3D = py::array_t<int32_t, py::array::c_style | py::array::forcecast>;
|
| 323 |
|
| 324 |
+
} // namespace
|
| 325 |
|
| 326 |
py::array_t<float> physical_decode_batch(const py::array_t<float>& table,
|
| 327 |
const py::array_t<int32_t>& bins,
|
|
|
|
| 331 |
const Array2D& initial,
|
| 332 |
const Array2D& initial_velocity, int32_t order,
|
| 333 |
int threads) {
|
| 334 |
+
if (cells.ndim() != 3 || cells.shape(2) != table.shape(0) || initial.ndim() != 2 ||
|
|
|
|
|
|
|
| 335 |
initial_velocity.ndim() != 2 || initial.shape(0) != cells.shape(0) ||
|
| 336 |
initial_velocity.shape(0) != cells.shape(0) || initial.shape(1) != cells.shape(2) ||
|
| 337 |
initial_velocity.shape(1) != cells.shape(2) || (order != 1 && order != 2)) {
|
| 338 |
throw std::invalid_argument("invalid physical decode batch shapes or order");
|
| 339 |
}
|
| 340 |
+
const Layout layout =
|
| 341 |
+
make_layout(table, bins, binary, lower, upper, py::array_t<float>(), false);
|
| 342 |
const int64_t batch = cells.shape(0);
|
| 343 |
const int64_t steps = cells.shape(1);
|
| 344 |
const int64_t dimension = layout.dimension;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
py::array_t<float> positions({batch, steps, dimension});
|
| 346 |
+
#ifndef _OPENMP
|
| 347 |
+
(void)threads;
|
| 348 |
+
#endif
|
| 349 |
{
|
| 350 |
py::gil_scoped_release release;
|
| 351 |
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
|
|
|
| 362 |
return positions;
|
| 363 |
}
|
| 364 |
|
| 365 |
+
// One thread per chunk. ``positions`` is [B, T, D] and ``initial`` is [B, D].
|
| 366 |
py::array_t<int32_t> physical_encode_batch(const py::array_t<float>& table,
|
| 367 |
const py::array_t<int32_t>& bins,
|
| 368 |
const py::array_t<uint8_t>& binary,
|
|
|
|
| 373 |
int threads, const py::array_t<float>& epsilon,
|
| 374 |
bool smooth) {
|
| 375 |
const Layout layout = make_layout(table, bins, binary, lower, upper, epsilon, smooth);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
const int64_t batch = positions.shape(0);
|
| 377 |
const int64_t steps = positions.shape(1);
|
| 378 |
const int64_t dimension = layout.dimension;
|
|
|
|
| 381 |
const float* starts = initial.data();
|
| 382 |
const float* velocities = initial_velocity.data();
|
| 383 |
int32_t* output = cells.mutable_data();
|
| 384 |
+
#ifndef _OPENMP
|
| 385 |
+
(void)threads;
|
| 386 |
+
#endif
|
| 387 |
{
|
| 388 |
py::gil_scoped_release release;
|
| 389 |
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
actioncodec2_kernels/preprocessing.cpp
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Continuous-segment preprocessing kernels. Policy and statistics stay in Python.
|
| 2 |
+
#include <pybind11/numpy.h>
|
| 3 |
+
#include <pybind11/pybind11.h>
|
| 4 |
+
#include <pybind11/stl.h>
|
| 5 |
+
#include <algorithm>
|
| 6 |
+
#include <cmath>
|
| 7 |
+
#include <map>
|
| 8 |
+
#include <mutex>
|
| 9 |
+
#include <numeric>
|
| 10 |
+
#include <vector>
|
| 11 |
+
#include "quaternion.hpp"
|
| 12 |
+
|
| 13 |
+
namespace py = pybind11;
|
| 14 |
+
namespace {
|
| 15 |
+
using D64=py::array_t<double,py::array::c_style|py::array::forcecast>;
|
| 16 |
+
using F32=py::array_t<float,py::array::c_style|py::array::forcecast>;
|
| 17 |
+
|
| 18 |
+
double step_norm(const double* a,const double* b,int64_t width,bool single) {
|
| 19 |
+
if(single) {
|
| 20 |
+
float sum=0;
|
| 21 |
+
for(int64_t d=0;d<width;++d) {const float v=static_cast<float>(b[d])-static_cast<float>(a[d]);sum+=v*v;}
|
| 22 |
+
return std::sqrt(sum);
|
| 23 |
+
}
|
| 24 |
+
double sum=0;
|
| 25 |
+
for(int64_t d=0;d<width;++d){double v=b[d]-a[d];sum+=v*v;}
|
| 26 |
+
return std::sqrt(sum);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
D64 step_norms(const D64& values,bool single) {
|
| 30 |
+
if(values.ndim()!=2)throw std::invalid_argument("expected [T,D] values");
|
| 31 |
+
const int64_t steps=std::max<int64_t>(0,values.shape(0)-1),width=values.shape(1);
|
| 32 |
+
D64 output(steps);
|
| 33 |
+
py::gil_scoped_release release;
|
| 34 |
+
for(int64_t t=0;t<steps;++t)output.mutable_data()[t]=step_norm(values.data()+t*width,values.data()+(t+1)*width,width,single);
|
| 35 |
+
return output;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
std::pair<int64_t,int64_t> boundary_trim(const D64& values,double tau,int64_t idle,
|
| 39 |
+
int64_t context,bool legacy,bool single) {
|
| 40 |
+
if(values.ndim()!=2 || context<1)throw std::invalid_argument("invalid trimming input");
|
| 41 |
+
int64_t length=values.shape(0),width=values.shape(1);
|
| 42 |
+
if(length<2)return {0,length};
|
| 43 |
+
py::gil_scoped_release release;
|
| 44 |
+
int64_t first=-1,last=-1;
|
| 45 |
+
for(int64_t t=0;t<length-1;++t) {
|
| 46 |
+
if(step_norm(values.data()+t*width,values.data()+(t+1)*width,width,single)>tau) {
|
| 47 |
+
if(first<0)first=t;
|
| 48 |
+
last=t;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
if(first<0)return legacy?std::make_pair(length,length):std::make_pair(int64_t{0},length);
|
| 52 |
+
const int64_t trailing=length-2-last;
|
| 53 |
+
int64_t start=first>=idle?(legacy?first+1:std::max<int64_t>(0,first-context+1)):0;
|
| 54 |
+
int64_t stop=trailing>=idle?last+2:length;
|
| 55 |
+
return {start,legacy?std::max(start,stop):std::max(start+1,stop)};
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
py::dict trim_deviation(const D64& values,int64_t start,int64_t stop,
|
| 59 |
+
const std::string& profile,int64_t gripper) {
|
| 60 |
+
if(values.ndim()!=2 || start<0 || start>=stop || stop>values.shape(0))
|
| 61 |
+
throw std::invalid_argument("trim slice must retain at least one row");
|
| 62 |
+
const int64_t width=values.shape(1),length=values.shape(0);
|
| 63 |
+
std::vector<int64_t> motion;
|
| 64 |
+
for(int64_t d=0;d<width;++d)if(d!=gripper)motion.push_back(d);
|
| 65 |
+
if(profile=="eef" && motion.size()<6)throw std::invalid_argument("EEF needs six motion columns");
|
| 66 |
+
std::vector<double> maximum(motion.size(),0);
|
| 67 |
+
double translation=0,rotation=0;bool changed=false;
|
| 68 |
+
{
|
| 69 |
+
py::gil_scoped_release release;
|
| 70 |
+
for(int side=0;side<2;++side) {
|
| 71 |
+
int64_t begin=side?stop:0,end=side?length:start,anchor=side?stop-1:start;
|
| 72 |
+
const double* a=values.data()+anchor*width;
|
| 73 |
+
for(int64_t t=begin;t<end;++t) {
|
| 74 |
+
const double* v=values.data()+t*width;
|
| 75 |
+
for(size_t d=0;d<motion.size();++d)maximum[d]=std::max(maximum[d],std::abs(v[motion[d]]-a[motion[d]]));
|
| 76 |
+
if(gripper>=0)changed=changed || v[gripper]!=a[gripper];
|
| 77 |
+
if(profile=="eef") {
|
| 78 |
+
double distance=0,ra[3],rv[3],relative[3];
|
| 79 |
+
for(int d=0;d<3;++d) {
|
| 80 |
+
double delta=v[motion[d]]-a[motion[d]];distance+=delta*delta;
|
| 81 |
+
ra[d]=a[motion[d+3]];rv[d]=v[motion[d+3]];
|
| 82 |
+
}
|
| 83 |
+
translation=std::max(translation,std::sqrt(distance));
|
| 84 |
+
ac2q::to_rotvec(ac2q::multiply(ac2q::conjugate(ac2q::from_rotvec(ra)),ac2q::from_rotvec(rv)),relative);
|
| 85 |
+
rotation=std::max(rotation,std::sqrt(relative[0]*relative[0]+relative[1]*relative[1]+relative[2]*relative[2]));
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
}
|
| 90 |
+
py::dict result;
|
| 91 |
+
result["motion_max_error"]=maximum;result["translation_max_error_m"]=translation;
|
| 92 |
+
result["rotation_max_error_rad"]=rotation;result["gripper_changed"]=changed;
|
| 93 |
+
return result;
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
// Small normalized Vandermonde least-squares systems, solved in extended precision.
|
| 97 |
+
std::vector<double> polynomial_weights(int window,int degree) {
|
| 98 |
+
const int p=degree+1;
|
| 99 |
+
std::vector<long double> basis(window*p),matrix(p*2*p,0);
|
| 100 |
+
for(int i=0;i<window;++i) {
|
| 101 |
+
long double x=2.L*i/(window-1)-1.L,v=1;
|
| 102 |
+
for(int d=0;d<p;++d){basis[i*p+d]=v;v*=x;}
|
| 103 |
+
}
|
| 104 |
+
for(int a=0;a<p;++a) {
|
| 105 |
+
for(int b=0;b<p;++b)for(int i=0;i<window;++i)matrix[a*2*p+b]+=basis[i*p+a]*basis[i*p+b];
|
| 106 |
+
matrix[a*2*p+p+a]=1;
|
| 107 |
+
}
|
| 108 |
+
for(int a=0;a<p;++a) {
|
| 109 |
+
int pivot=a;
|
| 110 |
+
for(int b=a+1;b<p;++b)if(std::abs(matrix[b*2*p+a])>std::abs(matrix[pivot*2*p+a]))pivot=b;
|
| 111 |
+
if(std::abs(matrix[pivot*2*p+a])<1e-25L)throw std::invalid_argument("singular polynomial fit");
|
| 112 |
+
for(int j=0;j<2*p;++j)std::swap(matrix[a*2*p+j],matrix[pivot*2*p+j]);
|
| 113 |
+
long double scale=matrix[a*2*p+a];
|
| 114 |
+
for(int j=0;j<2*p;++j)matrix[a*2*p+j]/=scale;
|
| 115 |
+
for(int b=0;b<p;++b)if(b!=a) {
|
| 116 |
+
const long double factor=matrix[b*2*p+a];
|
| 117 |
+
for(int j=0;j<2*p;++j)matrix[b*2*p+j]-=factor*matrix[a*2*p+j];
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
std::vector<double> weights(window*window);
|
| 121 |
+
for(int target=0;target<window;++target)for(int i=0;i<window;++i) {
|
| 122 |
+
long double w=0;
|
| 123 |
+
for(int a=0;a<p;++a)for(int b=0;b<p;++b)w+=basis[target*p+a]*matrix[a*2*p+p+b]*basis[i*p+b];
|
| 124 |
+
weights[target*window+i]=static_cast<double>(w);
|
| 125 |
+
}
|
| 126 |
+
return weights;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
F32 savgol(const D64& values,int window,int degree) {
|
| 130 |
+
if(values.ndim()!=2 || window<3 || window%2!=1 || degree<0 || degree>=window || window>values.shape(0))
|
| 131 |
+
throw std::invalid_argument("invalid Savitzky-Golay shape or polynomial order");
|
| 132 |
+
std::vector<double> weights;
|
| 133 |
+
{
|
| 134 |
+
static std::mutex mutex;
|
| 135 |
+
static std::map<std::pair<int,int>,std::vector<double>> cache;
|
| 136 |
+
std::lock_guard<std::mutex> lock(mutex);
|
| 137 |
+
auto key=std::make_pair(window,degree);auto it=cache.find(key);
|
| 138 |
+
if(it==cache.end())it=cache.emplace(key,polynomial_weights(window,degree)).first;
|
| 139 |
+
weights=it->second;
|
| 140 |
+
}
|
| 141 |
+
const int64_t length=values.shape(0),width=values.shape(1);
|
| 142 |
+
F32 output({length,width});
|
| 143 |
+
py::gil_scoped_release release;
|
| 144 |
+
for(int64_t t=0;t<length;++t) {
|
| 145 |
+
int64_t begin=std::clamp<int64_t>(t-window/2,0,length-window),target=t-begin;
|
| 146 |
+
for(int64_t d=0;d<width;++d) {
|
| 147 |
+
double value=0;
|
| 148 |
+
for(int i=0;i<window;++i)value+=weights[target*window+i]*values.data()[(begin+i)*width+d];
|
| 149 |
+
output.mutable_data()[t*width+d]=static_cast<float>(value);
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
return output;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
D64 median(const D64& values,int window,bool nearest) {
|
| 156 |
+
if(values.ndim()!=1 || window<1 || window%2!=1)throw std::invalid_argument("invalid median filter input");
|
| 157 |
+
const int64_t length=values.size();D64 output(length);
|
| 158 |
+
py::gil_scoped_release release;
|
| 159 |
+
std::vector<double> sample(window);
|
| 160 |
+
for(int64_t i=0;i<length;++i) {
|
| 161 |
+
for(int j=0;j<window;++j) {
|
| 162 |
+
int64_t k=i+j-window/2;
|
| 163 |
+
sample[j]=(k<0 || k>=length)?(nearest?values.data()[std::clamp<int64_t>(k,0,length-1)]:0):values.data()[k];
|
| 164 |
+
}
|
| 165 |
+
std::nth_element(sample.begin(),sample.begin()+window/2,sample.end());
|
| 166 |
+
output.mutable_data()[i]=sample[window/2];
|
| 167 |
+
}
|
| 168 |
+
return output;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
F32 ease(const D64& values,const D64& limits) {
|
| 172 |
+
if(values.ndim()!=2 || limits.ndim()!=1 || limits.size()!=values.shape(1))
|
| 173 |
+
throw std::invalid_argument("invalid easing shape");
|
| 174 |
+
int64_t length=values.shape(0),width=values.shape(1);F32 output({length,width});
|
| 175 |
+
py::gil_scoped_release release;
|
| 176 |
+
for(int64_t d=0;d<width;++d) {
|
| 177 |
+
if(length==0)continue;
|
| 178 |
+
double debt=0,sum=0,limit=limits.data()[d];
|
| 179 |
+
output.mutable_data()[d]=static_cast<float>(values.data()[d]);
|
| 180 |
+
for(int64_t t=1;t<length;++t) {
|
| 181 |
+
double step=values.data()[t*width+d]-values.data()[(t-1)*width+d],candidate=step+debt;
|
| 182 |
+
if(std::abs(candidate)<=limit){step=candidate;debt=0;}
|
| 183 |
+
else {step=(candidate>0?1:-1)*limit;debt=candidate-step;}
|
| 184 |
+
sum+=step;output.mutable_data()[t*width+d]=static_cast<float>(values.data()[d]+sum);
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
return output;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
F32 gripper(const D64& values,const std::string& operation,double first,double second) {
|
| 191 |
+
if(values.ndim()!=2 || values.shape(1)!=1)throw std::invalid_argument("gripper action must be [T,1]");
|
| 192 |
+
F32 output({values.shape(0),int64_t{1}});
|
| 193 |
+
py::gil_scoped_release release;
|
| 194 |
+
for(int64_t i=0;i<values.size();++i) {
|
| 195 |
+
float v=static_cast<float>(values.data()[i]);
|
| 196 |
+
if(!std::isfinite(v))throw std::invalid_argument("gripper fraction must be finite");
|
| 197 |
+
if(operation=="normalize") {
|
| 198 |
+
const float span=static_cast<float>(second-first);
|
| 199 |
+
output.mutable_data()[i]=std::clamp((v-static_cast<float>(first))/span,0.F,1.F);
|
| 200 |
+
} else if(operation=="binary")output.mutable_data()[i]=v>=static_cast<float>(first)?1.F:0.F;
|
| 201 |
+
else if(operation=="eleven")output.mutable_data()[i]=std::nearbyint(v*10.F)/10.F;
|
| 202 |
+
else throw std::invalid_argument("invalid gripper operation");
|
| 203 |
+
}
|
| 204 |
+
return output;
|
| 205 |
+
}
|
| 206 |
+
std::vector<double> max_change(const D64& values,const D64& reference,
|
| 207 |
+
const std::vector<int64_t>& columns,bool single) {
|
| 208 |
+
if(values.ndim()!=2 || reference.ndim()!=2 || values.shape(0)!=reference.shape(0) || values.shape(1)!=reference.shape(1))
|
| 209 |
+
throw std::invalid_argument("mismatched change metric shapes");
|
| 210 |
+
std::vector<double> result(columns.size(),0);const int64_t width=values.shape(1);
|
| 211 |
+
py::gil_scoped_release release;
|
| 212 |
+
for(size_t i=0;i<columns.size();++i) {
|
| 213 |
+
const int64_t d=(columns[i]%width+width)%width;
|
| 214 |
+
for(int64_t t=0;t<values.shape(0);++t) {
|
| 215 |
+
double change=single?std::abs(static_cast<float>(values.data()[t*width+d])-static_cast<float>(reference.data()[t*width+d]))
|
| 216 |
+
:std::abs(values.data()[t*width+d]-reference.data()[t*width+d]);
|
| 217 |
+
result[i]=std::max(result[i],change);
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
return result;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
int64_t overspeed_steps(const D64& values,const D64& limits,bool single) {
|
| 224 |
+
if(values.ndim()!=2 || limits.size()!=values.shape(1))throw std::invalid_argument("invalid overspeed metric shapes");
|
| 225 |
+
int64_t count=0,width=values.shape(1);py::gil_scoped_release release;
|
| 226 |
+
for(int64_t t=1;t<values.shape(0);++t)for(int64_t d=0;d<width;++d) {
|
| 227 |
+
double delta=single?static_cast<float>(values.data()[t*width+d])-static_cast<float>(values.data()[(t-1)*width+d])
|
| 228 |
+
:values.data()[t*width+d]-values.data()[(t-1)*width+d];
|
| 229 |
+
if(std::abs(delta)>limits.data()[d]){++count;break;}
|
| 230 |
+
}
|
| 231 |
+
return count;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
F32 wrap_angles(const D64& values) {
|
| 235 |
+
std::vector<py::ssize_t> shape(values.shape(),values.shape()+values.ndim());F32 output(shape);
|
| 236 |
+
const double pi=std::acos(-1.);py::gil_scoped_release release;
|
| 237 |
+
for(py::ssize_t i=0;i<values.size();++i) {
|
| 238 |
+
double remainder=std::fmod(pi-values.data()[i],2*pi);if(remainder<0)remainder+=2*pi;
|
| 239 |
+
output.mutable_data()[i]=static_cast<float>(pi-remainder);
|
| 240 |
+
}
|
| 241 |
+
return output;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
template<class T>
|
| 245 |
+
void unwrap_column(const double* values,double* output,int64_t length,int64_t width,int64_t d) {
|
| 246 |
+
if(!length)return;
|
| 247 |
+
const T pi=static_cast<T>(std::acos(-1.)),period=static_cast<T>(2*std::acos(-1.));
|
| 248 |
+
T sum=0;output[d]=values[d];
|
| 249 |
+
for(int64_t t=1;t<length;++t) {
|
| 250 |
+
const T delta=static_cast<T>(values[t*width+d])-static_cast<T>(values[(t-1)*width+d]);
|
| 251 |
+
T reduced=std::fmod(static_cast<T>(delta+pi),period);if(reduced<0)reduced+=period;
|
| 252 |
+
reduced-=pi;if(reduced==-pi && delta>0)reduced=pi;
|
| 253 |
+
const T correction=std::abs(delta)<pi?0:reduced-delta;sum+=correction;
|
| 254 |
+
output[t*width+d]=static_cast<T>(static_cast<T>(values[t*width+d])+sum);
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
D64 unwrap_angles(const D64& values,bool single) {
|
| 258 |
+
if(values.ndim()!=2)throw std::invalid_argument("unwrap expects [T,D]");
|
| 259 |
+
int64_t length=values.shape(0),width=values.shape(1);D64 output({length,width});
|
| 260 |
+
py::gil_scoped_release release;
|
| 261 |
+
for(int64_t d=0;d<width;++d) {
|
| 262 |
+
if(single)unwrap_column<float>(values.data(),output.mutable_data(),length,width,d);
|
| 263 |
+
else unwrap_column<double>(values.data(),output.mutable_data(),length,width,d);
|
| 264 |
+
}
|
| 265 |
+
return output;
|
| 266 |
+
}
|
| 267 |
+
} // namespace
|
| 268 |
+
|
| 269 |
+
void register_preprocessing(py::module_& module) {
|
| 270 |
+
module.def("step_norms",&step_norms);
|
| 271 |
+
module.def("boundary_trim",&boundary_trim);
|
| 272 |
+
module.def("trim_deviation",&trim_deviation);
|
| 273 |
+
module.def("savgol_smooth",&savgol);
|
| 274 |
+
module.def("median_filter",&median);
|
| 275 |
+
module.def("ease_overspeed",&ease);
|
| 276 |
+
module.def("gripper_transform",&gripper);
|
| 277 |
+
module.def("max_change",&max_change);
|
| 278 |
+
module.def("overspeed_steps",&overspeed_steps);
|
| 279 |
+
module.def("wrap_angles",&wrap_angles);
|
| 280 |
+
module.def("unwrap_angles",&unwrap_angles);
|
| 281 |
+
}
|
actioncodec2_kernels/quaternion.hpp
CHANGED
|
@@ -43,4 +43,4 @@ inline void to_euler(Q q, double* out) {
|
|
| 43 |
out[1]=std::asin(std::clamp(2*(w*y-z*x),-1.,1.));
|
| 44 |
out[2]=std::atan2(2*(w*z+x*y),1-2*(y*y+z*z));
|
| 45 |
}
|
| 46 |
-
}
|
|
|
|
| 43 |
out[1]=std::asin(std::clamp(2*(w*y-z*x),-1.,1.));
|
| 44 |
out[2]=std::atan2(2*(w*z+x*y),1-2*(y*y+z*z));
|
| 45 |
}
|
| 46 |
+
} // namespace ac2q
|
actioncodec2_kernels/reconstruct.cpp
DELETED
|
@@ -1,146 +0,0 @@
|
|
| 1 |
-
#include <pybind11/numpy.h>
|
| 2 |
-
#include <pybind11/pybind11.h>
|
| 3 |
-
|
| 4 |
-
#include <algorithm>
|
| 5 |
-
#include <cmath>
|
| 6 |
-
#include <cstdint>
|
| 7 |
-
#include <limits>
|
| 8 |
-
#include <stdexcept>
|
| 9 |
-
|
| 10 |
-
#include "parallel.hpp"
|
| 11 |
-
#include "quaternion.hpp"
|
| 12 |
-
|
| 13 |
-
namespace py = pybind11;
|
| 14 |
-
namespace {
|
| 15 |
-
using I64 = py::array_t<int64_t, py::array::c_style | py::array::forcecast>;
|
| 16 |
-
using I32 = py::array_t<int32_t, py::array::c_style | py::array::forcecast>;
|
| 17 |
-
using F64 = py::array_t<double, py::array::c_style | py::array::forcecast>;
|
| 18 |
-
using F32 = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 19 |
-
|
| 20 |
-
void validate_reconstruction(const F64& table, const I32& bins, const I64& cells,
|
| 21 |
-
const F32& previous, const F32& scale) {
|
| 22 |
-
if (table.ndim() != 2 || table.shape(0) < 1 || table.shape(1) < 1 ||
|
| 23 |
-
bins.ndim() != 1 || bins.size() != table.shape(0) ||
|
| 24 |
-
cells.ndim() != 3 || cells.shape(0) < 1 || cells.shape(1) < 1 ||
|
| 25 |
-
cells.shape(2) != table.shape(0) || previous.ndim() != 2 ||
|
| 26 |
-
previous.shape(0) != cells.shape(0) || previous.shape(1) != cells.shape(2) ||
|
| 27 |
-
scale.ndim() != 1 || scale.size() != cells.shape(2)) {
|
| 28 |
-
throw std::invalid_argument("invalid reconstruction shapes");
|
| 29 |
-
}
|
| 30 |
-
for (py::ssize_t d = 0; d < bins.size(); ++d) {
|
| 31 |
-
if (bins.data()[d] < 1 || bins.data()[d] > table.shape(1))
|
| 32 |
-
throw std::invalid_argument("primitive count exceeds table width");
|
| 33 |
-
if (!std::isfinite(scale.data()[d]) || scale.data()[d] <= 0)
|
| 34 |
-
throw std::invalid_argument("scale must be positive and finite");
|
| 35 |
-
for (int32_t k = 0; k < bins.data()[d]; ++k) {
|
| 36 |
-
if (!std::isfinite(table.data()[d * table.shape(1) + k]))
|
| 37 |
-
throw std::invalid_argument("active primitives must be finite");
|
| 38 |
-
}
|
| 39 |
-
}
|
| 40 |
-
for (py::ssize_t i = 0; i < previous.size(); ++i) {
|
| 41 |
-
if (!std::isfinite(previous.data()[i]))
|
| 42 |
-
throw std::invalid_argument("previous first-order values must be finite");
|
| 43 |
-
}
|
| 44 |
-
for (py::ssize_t i = 0; i < cells.size(); ++i) {
|
| 45 |
-
if (cells.data()[i] < 0 || cells.data()[i] >= bins.data()[i % bins.size()])
|
| 46 |
-
throw std::invalid_argument("cell index is outside its primitive table");
|
| 47 |
-
}
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
F32 so3_reconstruct(const F64& table, const I32& bins, const I64& cells,
|
| 51 |
-
const F32& previous, const F32& scale, int input_order,
|
| 52 |
-
int primitive_order, bool left, bool rotation_vector, double radius) {
|
| 53 |
-
validate_reconstruction(table, bins, cells, previous, scale);
|
| 54 |
-
if (table.shape(0) != 3 || (input_order != 0 && input_order != 1) ||
|
| 55 |
-
(primitive_order != 1 && primitive_order != 2) ||
|
| 56 |
-
std::isnan(radius) || radius <= 0)
|
| 57 |
-
throw std::invalid_argument("invalid SO(3) reconstruction order or radius");
|
| 58 |
-
const int64_t batch = cells.shape(0), steps = cells.shape(1);
|
| 59 |
-
F32 output({batch, steps, int64_t{3}});
|
| 60 |
-
auto* target = output.mutable_data();
|
| 61 |
-
py::gil_scoped_release release;
|
| 62 |
-
ac2::parallel_for(batch, 0, [&](int64_t b) {
|
| 63 |
-
double boundary[3];
|
| 64 |
-
for (int d = 0; d < 3; ++d)
|
| 65 |
-
boundary[d] = static_cast<double>(previous.data()[3 * b + d]) * scale.data()[d];
|
| 66 |
-
auto increment = rotation_vector ? ac2q::from_rotvec(boundary) : ac2q::from_euler(boundary);
|
| 67 |
-
ac2q::Q state{0, 0, 0, 1};
|
| 68 |
-
float accumulated[3] = {0, 0, 0};
|
| 69 |
-
auto apply = [&](ac2q::Q a, ac2q::Q v) {
|
| 70 |
-
return left ? ac2q::multiply(v, a) : ac2q::multiply(a, v);
|
| 71 |
-
};
|
| 72 |
-
for (int64_t t = 0; t < steps; ++t) {
|
| 73 |
-
double v[3], represented[3];
|
| 74 |
-
for (int d = 0; d < 3; ++d)
|
| 75 |
-
v[d] = table.data()[d * table.shape(1) + cells.data()[(b * steps + t) * 3 + d]];
|
| 76 |
-
if (primitive_order == 2) {
|
| 77 |
-
const double norm = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
| 78 |
-
const double factor = std::min(1., radius / std::max(norm, std::numeric_limits<double>::min()));
|
| 79 |
-
for (double& x : v) x *= factor;
|
| 80 |
-
increment = apply(increment, ac2q::from_rotvec(v));
|
| 81 |
-
state = apply(state, increment);
|
| 82 |
-
auto q = input_order == 1 ? increment : state;
|
| 83 |
-
if (rotation_vector) ac2q::to_rotvec(q, represented);
|
| 84 |
-
else ac2q::to_euler(q, represented);
|
| 85 |
-
} else if (input_order == 1) {
|
| 86 |
-
if (rotation_vector) std::copy(v, v + 3, represented);
|
| 87 |
-
else {
|
| 88 |
-
for (double& x : v) x = static_cast<float>(x);
|
| 89 |
-
ac2q::to_euler(ac2q::from_rotvec(v), represented);
|
| 90 |
-
}
|
| 91 |
-
} else {
|
| 92 |
-
state = apply(state, ac2q::from_rotvec(v));
|
| 93 |
-
if (rotation_vector) ac2q::to_rotvec(state, represented);
|
| 94 |
-
else ac2q::to_euler(state, represented);
|
| 95 |
-
}
|
| 96 |
-
for (int d = 0; d < 3; ++d) {
|
| 97 |
-
float value = static_cast<float>(represented[d] / scale.data()[d]);
|
| 98 |
-
if (primitive_order == 1 && input_order == 1) {
|
| 99 |
-
const float next = accumulated[d] + value;
|
| 100 |
-
value = next - accumulated[d];
|
| 101 |
-
accumulated[d] = next;
|
| 102 |
-
}
|
| 103 |
-
target[(b * steps + t) * 3 + d] = value;
|
| 104 |
-
}
|
| 105 |
-
}
|
| 106 |
-
});
|
| 107 |
-
return output;
|
| 108 |
-
}
|
| 109 |
-
|
| 110 |
-
F32 additive_reconstruct(const F64& table, const I32& bins, const I64& cells,
|
| 111 |
-
const F32& previous, const F32& scale, int input_order) {
|
| 112 |
-
validate_reconstruction(table, bins, cells, previous, scale);
|
| 113 |
-
if (input_order < 0 || input_order > 2)
|
| 114 |
-
throw std::invalid_argument("invalid additive reconstruction order");
|
| 115 |
-
const int64_t batch = cells.shape(0), steps = cells.shape(1), width = cells.shape(2);
|
| 116 |
-
F32 output({batch, steps, width});
|
| 117 |
-
auto* target = output.mutable_data();
|
| 118 |
-
py::gil_scoped_release release;
|
| 119 |
-
ac2::parallel_for(batch, 0, [&](int64_t b) {
|
| 120 |
-
for (int64_t d = 0; d < width; ++d) {
|
| 121 |
-
double change_sum = 0, position = 0;
|
| 122 |
-
const float boundary = previous.data()[b * width + d] * scale.data()[d];
|
| 123 |
-
for (int64_t t = 0; t < steps; ++t) {
|
| 124 |
-
const int64_t index = (b * steps + t) * width + d;
|
| 125 |
-
const double primitive = table.data()[d * table.shape(1) + cells.data()[index]];
|
| 126 |
-
change_sum += primitive;
|
| 127 |
-
const double velocity = change_sum + boundary;
|
| 128 |
-
position += velocity;
|
| 129 |
-
const double value = input_order == 2 ? primitive : input_order == 1 ? velocity : position;
|
| 130 |
-
target[index] = static_cast<float>(value / scale.data()[d]);
|
| 131 |
-
}
|
| 132 |
-
}
|
| 133 |
-
});
|
| 134 |
-
return output;
|
| 135 |
-
}
|
| 136 |
-
}
|
| 137 |
-
|
| 138 |
-
void register_reconstruction(py::module_& module) {
|
| 139 |
-
module.def("so3_reconstruct", &so3_reconstruct, py::arg("table"), py::arg("bins"),
|
| 140 |
-
py::arg("cells"), py::arg("previous_first_order"), py::arg("scale"),
|
| 141 |
-
py::arg("input_order"), py::arg("primitive_order"), py::arg("left"),
|
| 142 |
-
py::arg("rotation_vector"), py::arg("radius"));
|
| 143 |
-
module.def("additive_reconstruct", &additive_reconstruct, py::arg("table"), py::arg("bins"),
|
| 144 |
-
py::arg("cells"), py::arg("previous_first_order"), py::arg("scale"),
|
| 145 |
-
py::arg("input_order"));
|
| 146 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
actioncodec2_kernels/repacking.hpp
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Optional lossless fixed-vocabulary packing for the native Set-BPE encoder.
|
| 2 |
+
#pragma once
|
| 3 |
+
#include <algorithm>
|
| 4 |
+
#include <array>
|
| 5 |
+
#include <cstdint>
|
| 6 |
+
#include <functional>
|
| 7 |
+
#include <stdexcept>
|
| 8 |
+
#include <tuple>
|
| 9 |
+
#include <unordered_map>
|
| 10 |
+
#include <utility>
|
| 11 |
+
#include <vector>
|
| 12 |
+
|
| 13 |
+
namespace ac2pack {
|
| 14 |
+
using Instance = std::tuple<int64_t, int64_t, int64_t>;
|
| 15 |
+
using Cover = std::array<uint64_t, 4>;
|
| 16 |
+
|
| 17 |
+
struct CoverHash {
|
| 18 |
+
size_t operator()(const Cover& value) const {
|
| 19 |
+
size_t hash = 0;
|
| 20 |
+
for (uint64_t word : value) hash ^= std::hash<uint64_t>{}(word) +
|
| 21 |
+
0x9e3779b97f4a7c15ULL + (hash << 6) + (hash >> 2);
|
| 22 |
+
return hash;
|
| 23 |
+
}
|
| 24 |
+
};
|
| 25 |
+
|
| 26 |
+
struct Placement {
|
| 27 |
+
Cover cells{};
|
| 28 |
+
int64_t token;
|
| 29 |
+
int anchor, count;
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
struct Requirement { int offset; uint64_t mask, key; };
|
| 33 |
+
using MatchCache = std::array<std::unordered_map<uint64_t, std::vector<int64_t>>, 8>;
|
| 34 |
+
|
| 35 |
+
inline int first_free(const Cover& cover) {
|
| 36 |
+
for (int word = 0; word < 4; ++word)
|
| 37 |
+
if (~cover[word]) return word * 64 + __builtin_ctzll(~cover[word]);
|
| 38 |
+
return 256;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
inline int free_count(const Cover& cover) {
|
| 42 |
+
int count = 0;
|
| 43 |
+
for (uint64_t word : cover) count += __builtin_popcountll(~word);
|
| 44 |
+
return count;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
inline bool overlaps(const Cover& a, const Cover& b) {
|
| 48 |
+
for (int word = 0; word < 4; ++word) if (a[word] & b[word]) return true;
|
| 49 |
+
return false;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
inline Cover unite(Cover a, const Cover& b) {
|
| 53 |
+
for (int word = 0; word < 4; ++word) a[word] |= b[word];
|
| 54 |
+
return a;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
class Search {
|
| 58 |
+
public:
|
| 59 |
+
Search(const std::vector<int64_t>& bins, const std::vector<uint64_t>& support,
|
| 60 |
+
const std::vector<int64_t>& expansion,
|
| 61 |
+
const std::vector<std::vector<std::pair<int64_t, int64_t>>>& patterns,
|
| 62 |
+
const std::vector<int64_t>& roots)
|
| 63 |
+
: bins_(bins), width_(bins.size()), spatial_size_(support.size()),
|
| 64 |
+
support_(support), patterns_(patterns) {
|
| 65 |
+
if (width_ > 8) throw std::invalid_argument("packing search supports at most eight slots");
|
| 66 |
+
for (int64_t count : bins)
|
| 67 |
+
if (count > 255) throw std::invalid_argument("packing search supports at most 255 bins per slot");
|
| 68 |
+
keys_.resize(spatial_size_);
|
| 69 |
+
anchors_.resize(patterns.size());
|
| 70 |
+
spans_.resize(patterns.size(), 1);
|
| 71 |
+
counts_.resize(patterns.size());
|
| 72 |
+
for (int64_t id = 0; id < spatial_size_; ++id) {
|
| 73 |
+
for (int d = 0; d < width_; ++d) {
|
| 74 |
+
const int64_t value = expansion[id * width_ + d];
|
| 75 |
+
if (value >= 0) keys_[id] |= uint64_t(value + 1) << (8 * d);
|
| 76 |
+
}
|
| 77 |
+
lookup_.emplace(keys_[id], id);
|
| 78 |
+
}
|
| 79 |
+
temporal_by_root_.resize(spatial_size_);
|
| 80 |
+
for (size_t id = 0; id < patterns.size(); ++id) {
|
| 81 |
+
anchors_[id] = __builtin_ctzll(support[roots[id]]);
|
| 82 |
+
int count = 0;
|
| 83 |
+
for (auto [time, sid] : patterns[id]) {
|
| 84 |
+
count += __builtin_popcountll(support[sid]);
|
| 85 |
+
spans_[id] = std::max(spans_[id], int(time + 1));
|
| 86 |
+
}
|
| 87 |
+
max_cells_ = std::max(max_cells_, count);
|
| 88 |
+
counts_[id] = count;
|
| 89 |
+
if (id >= static_cast<size_t>(spatial_size_))
|
| 90 |
+
temporal_by_root_[lookup_.at(keys_[roots[id]])].push_back(id);
|
| 91 |
+
}
|
| 92 |
+
byte_masks_.resize(1 << width_);
|
| 93 |
+
for (int mask = 1; mask < (1 << width_); ++mask) {
|
| 94 |
+
const int d = __builtin_ctz(mask);
|
| 95 |
+
byte_masks_[mask] = byte_masks_[mask ^ (1 << d)] | (uint64_t{255} << (8 * d));
|
| 96 |
+
}
|
| 97 |
+
prepare_spatial_trie();
|
| 98 |
+
prepare_patterns();
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
std::vector<int64_t> apply(const int64_t* cells, int64_t steps,
|
| 102 |
+
const std::vector<int64_t>& original, bool spatial, bool temporal, int window, int beam) const {
|
| 103 |
+
if (!temporal) return spatial ? repack(cells, steps, original) : original;
|
| 104 |
+
auto result = local_repack(cells, steps, original, window, beam);
|
| 105 |
+
return spatial ? repack(cells, steps, result) : result;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
private:
|
| 109 |
+
struct TrieNode { std::vector<int32_t> children; int64_t token = -1; };
|
| 110 |
+
|
| 111 |
+
int32_t trie_node(int dimension) {
|
| 112 |
+
const auto index = static_cast<int32_t>(trie_.size());
|
| 113 |
+
trie_.emplace_back();
|
| 114 |
+
if (dimension < width_) trie_.back().children.assign(bins_[dimension] + 1, -1);
|
| 115 |
+
return index;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
void prepare_spatial_trie() {
|
| 119 |
+
for (int d = 0; d < width_; ++d) trie_roots_[d].assign(bins_[d] + 1, -1);
|
| 120 |
+
for (int64_t sid = 0; sid < spatial_size_; ++sid) {
|
| 121 |
+
if (lookup_.at(keys_[sid]) != sid) continue;
|
| 122 |
+
const int root = anchors_[sid], value = (keys_[sid] >> (8 * root)) & 255;
|
| 123 |
+
int32_t node = trie_roots_[root][value];
|
| 124 |
+
if (node < 0) {
|
| 125 |
+
node = trie_node(root + 1);
|
| 126 |
+
trie_roots_[root][value] = node;
|
| 127 |
+
}
|
| 128 |
+
for (int d = root + 1; d < width_; ++d) {
|
| 129 |
+
const int cell = (keys_[sid] >> (8 * d)) & 255;
|
| 130 |
+
int32_t child = trie_[node].children[cell];
|
| 131 |
+
if (child < 0) {
|
| 132 |
+
child = trie_node(d + 1);
|
| 133 |
+
trie_[node].children[cell] = child;
|
| 134 |
+
}
|
| 135 |
+
node = child;
|
| 136 |
+
}
|
| 137 |
+
trie_[node].token = sid;
|
| 138 |
+
}
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
void query_spatial_trie(int32_t node, int dimension, uint64_t row,
|
| 142 |
+
std::vector<int64_t>& result) const {
|
| 143 |
+
if (node < 0) return;
|
| 144 |
+
if (dimension == width_) { result.push_back(trie_[node].token); return; }
|
| 145 |
+
query_spatial_trie(trie_[node].children[0], dimension + 1, row, result);
|
| 146 |
+
const int value = (row >> (8 * dimension)) & 255;
|
| 147 |
+
if (value) query_spatial_trie(trie_[node].children[value], dimension + 1, row, result);
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
void prepare_patterns() {
|
| 151 |
+
relative_.resize(patterns_.size());
|
| 152 |
+
requirements_.resize(patterns_.size());
|
| 153 |
+
for (size_t token = 0; token < patterns_.size(); ++token) {
|
| 154 |
+
if (spans_[token] * width_ > 256) continue;
|
| 155 |
+
std::vector<Requirement> rows(spans_[token], Requirement{0, 0, 0});
|
| 156 |
+
for (auto [offset, sid] : patterns_[token]) {
|
| 157 |
+
rows[offset].offset = offset;
|
| 158 |
+
rows[offset].mask |= byte_masks_[support_[sid]];
|
| 159 |
+
rows[offset].key |= keys_[sid];
|
| 160 |
+
uint64_t support = support_[sid];
|
| 161 |
+
while (support) {
|
| 162 |
+
const int d = __builtin_ctzll(support); support &= support - 1;
|
| 163 |
+
const int bit = offset * width_ + d;
|
| 164 |
+
relative_[token][bit / 64] |= uint64_t{1} << (bit % 64);
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
for (const auto& row : rows) if (row.mask) requirements_[token].push_back(row);
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
uint64_t row_key(const int64_t* row) const {
|
| 172 |
+
uint64_t key = 0;
|
| 173 |
+
for (int d = 0; d < width_; ++d)
|
| 174 |
+
if (row[d] >= 0) key |= uint64_t(row[d] + 1) << (8 * d);
|
| 175 |
+
return key;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
std::vector<int64_t> matching_spatial(uint64_t row, int dim) const {
|
| 179 |
+
std::vector<int64_t> result;
|
| 180 |
+
const int value = (row >> (8 * dim)) & 255;
|
| 181 |
+
if (value) query_spatial_trie(trie_roots_[dim][value], dim + 1, row, result);
|
| 182 |
+
return result;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
bool placement(int64_t token, int anchor, const std::vector<uint64_t>& rows,
|
| 186 |
+
const Cover& fixed, Placement& out) const {
|
| 187 |
+
if (anchor + spans_[token] > static_cast<int>(rows.size())) return false;
|
| 188 |
+
for (const auto& req : requirements_[token])
|
| 189 |
+
if ((rows[anchor + req.offset] & req.mask) != req.key) return false;
|
| 190 |
+
out = Placement{}; out.token = token; out.anchor = anchor; out.count = counts_[token];
|
| 191 |
+
const int start = anchor * width_, word = start / 64, shift = start % 64;
|
| 192 |
+
const int words = (spans_[token] * width_ + 63) / 64;
|
| 193 |
+
for (int i = 0; i < words && word + i < 4; ++i) {
|
| 194 |
+
out.cells[word + i] |= relative_[token][i] << shift;
|
| 195 |
+
if (shift && word + i + 1 < 4)
|
| 196 |
+
out.cells[word + i + 1] |= relative_[token][i] >> (64 - shift);
|
| 197 |
+
}
|
| 198 |
+
return !overlaps(out.cells, fixed);
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
std::vector<Placement> candidates(
|
| 202 |
+
int root, const std::vector<uint64_t>& rows, const Cover& fixed,
|
| 203 |
+
MatchCache& cache) const {
|
| 204 |
+
std::vector<Placement> result;
|
| 205 |
+
const int time = root / width_, dim = root % width_;
|
| 206 |
+
auto& table = cache[dim];
|
| 207 |
+
auto found = table.find(rows[time]);
|
| 208 |
+
if (found == table.end())
|
| 209 |
+
found = table.emplace(rows[time], matching_spatial(rows[time], dim)).first;
|
| 210 |
+
for (int64_t sid : found->second) {
|
| 211 |
+
Placement offer;
|
| 212 |
+
if (placement(sid, time, rows, fixed, offer)) result.push_back(offer);
|
| 213 |
+
for (int64_t token : temporal_by_root_[sid])
|
| 214 |
+
if (placement(token, time, rows, fixed, offer)) result.push_back(offer);
|
| 215 |
+
}
|
| 216 |
+
std::sort(result.begin(), result.end(), [](const Placement& a, const Placement& b) {
|
| 217 |
+
return std::make_pair(-a.count, a.token) < std::make_pair(-b.count, b.token);
|
| 218 |
+
});
|
| 219 |
+
return result;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
using Options = std::function<const std::vector<Placement>&(int)>;
|
| 223 |
+
|
| 224 |
+
std::vector<Instance> greedy_cover(const Options& options, Cover occupied, int bound) const {
|
| 225 |
+
std::vector<Instance> result;
|
| 226 |
+
while (first_free(occupied) != 256 && static_cast<int>(result.size()) + 1 < bound) {
|
| 227 |
+
const auto& offers = options(first_free(occupied));
|
| 228 |
+
auto choice = std::find_if(offers.begin(), offers.end(), [&](const Placement& offer) {
|
| 229 |
+
return !overlaps(occupied, offer.cells);
|
| 230 |
+
});
|
| 231 |
+
if (choice == offers.end()) return {};
|
| 232 |
+
result.emplace_back(choice->anchor, anchors_[choice->token], choice->token);
|
| 233 |
+
occupied = unite(occupied, choice->cells);
|
| 234 |
+
}
|
| 235 |
+
return first_free(occupied) == 256 ? result : std::vector<Instance>{};
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
std::vector<Instance> beam_cover(const Options& options,
|
| 239 |
+
const Cover& fixed, int bound, int beam) const {
|
| 240 |
+
if (beam == 1) return greedy_cover(options, fixed, bound);
|
| 241 |
+
struct Node { Cover covered; int parent; int64_t token; int anchor, remaining, root; };
|
| 242 |
+
std::vector<Node> arena{{fixed, -1, -1, -1, free_count(fixed), first_free(fixed)}};
|
| 243 |
+
arena.reserve(beam * bound + 1);
|
| 244 |
+
std::vector<int> frontier{0};
|
| 245 |
+
frontier.reserve(beam);
|
| 246 |
+
std::array<Node, 64> next;
|
| 247 |
+
auto better = [](const Node& a, const Node& b) {
|
| 248 |
+
if (a.remaining != b.remaining) return a.remaining < b.remaining;
|
| 249 |
+
return a.root != b.root ? a.root > b.root : a.covered < b.covered;
|
| 250 |
+
};
|
| 251 |
+
for (int depth = 0; depth + 1 < bound && !frontier.empty(); ++depth) {
|
| 252 |
+
int keep = 0;
|
| 253 |
+
for (int index : frontier) {
|
| 254 |
+
for (const auto& offer : options(arena[index].root)) {
|
| 255 |
+
if (overlaps(arena[index].covered, offer.cells)) continue;
|
| 256 |
+
Cover covered = unite(arena[index].covered, offer.cells);
|
| 257 |
+
const int remaining = arena[index].remaining - offer.count;
|
| 258 |
+
if (!remaining) {
|
| 259 |
+
std::vector<Instance> result{{offer.anchor, anchors_[offer.token], offer.token}};
|
| 260 |
+
for (int p = index; arena[p].parent >= 0; p = arena[p].parent)
|
| 261 |
+
result.emplace_back(arena[p].anchor, anchors_[arena[p].token], arena[p].token);
|
| 262 |
+
std::reverse(result.begin(), result.end());
|
| 263 |
+
return result;
|
| 264 |
+
}
|
| 265 |
+
if (keep == beam && remaining > next[keep - 1].remaining) continue;
|
| 266 |
+
Node node{covered, index, offer.token, offer.anchor, remaining, first_free(covered)};
|
| 267 |
+
if (keep == beam && !better(node, next[keep - 1])) continue;
|
| 268 |
+
bool duplicate = false;
|
| 269 |
+
for (int i = 0; i < keep; ++i) if (covered == next[i].covered) { duplicate = true; break; }
|
| 270 |
+
if (duplicate) continue;
|
| 271 |
+
int position = keep < beam ? keep++ : keep - 1;
|
| 272 |
+
while (position && better(node, next[position - 1])) {
|
| 273 |
+
next[position] = next[position - 1]; --position;
|
| 274 |
+
}
|
| 275 |
+
next[position] = node;
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
frontier.clear();
|
| 279 |
+
for (int i = 0; i < keep; ++i) {
|
| 280 |
+
frontier.push_back(arena.size());
|
| 281 |
+
arena.push_back(std::move(next[i]));
|
| 282 |
+
}
|
| 283 |
+
}
|
| 284 |
+
return {};
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
std::vector<int64_t> local_repack(const int64_t* cells, int64_t steps,
|
| 288 |
+
const std::vector<int64_t>& stream, int window, int beam) const {
|
| 289 |
+
uint64_t full = 0;
|
| 290 |
+
for (int d = 0; d < width_; ++d) if (cells[d] >= 0) full |= uint64_t{1} << d;
|
| 291 |
+
std::vector<uint64_t> coverage(steps, 0), fixed(steps, 0);
|
| 292 |
+
std::vector<std::vector<Instance>> blocks((steps + window - 1) / window);
|
| 293 |
+
std::vector<Instance> result;
|
| 294 |
+
int64_t front = 0;
|
| 295 |
+
for (int64_t token : stream) {
|
| 296 |
+
while (front < steps && coverage[front] == full) ++front;
|
| 297 |
+
const bool crossing = front / window != (front + spans_[token] - 1) / window;
|
| 298 |
+
Instance instance{front, anchors_[token], token};
|
| 299 |
+
if (crossing) result.push_back(instance);
|
| 300 |
+
else blocks[front / window].push_back(instance);
|
| 301 |
+
for (auto [offset, sid] : patterns_[token]) {
|
| 302 |
+
coverage[front + offset] |= support_[sid];
|
| 303 |
+
if (crossing) fixed[front + offset] |= support_[sid];
|
| 304 |
+
}
|
| 305 |
+
}
|
| 306 |
+
MatchCache cache;
|
| 307 |
+
for (size_t block = 0; block < blocks.size(); ++block) {
|
| 308 |
+
const int start = block * window, length = std::min<int64_t>(window, steps - start);
|
| 309 |
+
Cover occupied; occupied.fill(~uint64_t{0});
|
| 310 |
+
std::vector<uint64_t> rows(length);
|
| 311 |
+
for (int t = 0; t < length; ++t) {
|
| 312 |
+
rows[t] = row_key(cells + (start + t) * width_);
|
| 313 |
+
for (int d = 0; d < width_; ++d) {
|
| 314 |
+
const int bit = t * width_ + d;
|
| 315 |
+
if ((full & (uint64_t{1} << d)) && !(fixed[start + t] & (uint64_t{1} << d)))
|
| 316 |
+
occupied[bit / 64] &= ~(uint64_t{1} << (bit % 64));
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
std::vector<Instance> improved;
|
| 320 |
+
const int bound = blocks[block].size();
|
| 321 |
+
if ((free_count(occupied) + max_cells_ - 1) / max_cells_ < bound) {
|
| 322 |
+
std::vector<std::vector<Placement>> options(length * width_);
|
| 323 |
+
std::vector<char> ready(length * width_, 0);
|
| 324 |
+
auto get_options = [&](int root) -> const std::vector<Placement>& {
|
| 325 |
+
if (!ready[root]) {
|
| 326 |
+
options[root] = candidates(root, rows, occupied, cache);
|
| 327 |
+
ready[root] = 1;
|
| 328 |
+
}
|
| 329 |
+
return options[root];
|
| 330 |
+
};
|
| 331 |
+
improved = beam_cover(get_options, occupied, bound, beam);
|
| 332 |
+
}
|
| 333 |
+
if (improved.empty()) result.insert(result.end(), blocks[block].begin(), blocks[block].end());
|
| 334 |
+
else for (auto [time, root, token] : improved) result.emplace_back(start + time, root, token);
|
| 335 |
+
for (auto& table : cache) if (table.size() > 4096) table.clear();
|
| 336 |
+
}
|
| 337 |
+
std::sort(result.begin(), result.end());
|
| 338 |
+
std::vector<int64_t> tokens;
|
| 339 |
+
for (auto [time, root, token] : result) tokens.push_back(token);
|
| 340 |
+
if (tokens.size() > stream.size()) throw std::runtime_error("local packing regressed");
|
| 341 |
+
return tokens;
|
| 342 |
+
}
|
| 343 |
+
|
| 344 |
+
std::vector<int64_t> optimal(uint64_t row, int mask,
|
| 345 |
+
const std::vector<int64_t>& incumbent) const {
|
| 346 |
+
if (incumbent.size() < 2) return incumbent;
|
| 347 |
+
std::array<int64_t, 256> matched;
|
| 348 |
+
matched.fill(-2);
|
| 349 |
+
auto match = [&](int subset) {
|
| 350 |
+
int64_t& token = matched[subset];
|
| 351 |
+
if (token == -2) {
|
| 352 |
+
const auto found = lookup_.find(row & byte_masks_[subset]);
|
| 353 |
+
token = found == lookup_.end() ? -1 : found->second;
|
| 354 |
+
}
|
| 355 |
+
return token;
|
| 356 |
+
};
|
| 357 |
+
if (int64_t token = match(mask); token >= 0) return {token};
|
| 358 |
+
if (incumbent.size() == 2) return incumbent;
|
| 359 |
+
const int bit = mask & -mask;
|
| 360 |
+
for (int sub = (mask - 1) & mask; sub; sub = (sub - 1) & mask) {
|
| 361 |
+
if (!(sub & bit)) continue;
|
| 362 |
+
const int64_t left = match(sub), right = match(mask ^ sub);
|
| 363 |
+
if (left >= 0 && right >= 0) return {left, right};
|
| 364 |
+
}
|
| 365 |
+
if (incumbent.size() == 3) return incumbent;
|
| 366 |
+
std::array<int, 256> costs, choices;
|
| 367 |
+
costs.fill(99); choices.fill(0); costs[0] = 0;
|
| 368 |
+
std::function<int(int)> solve = [&](int remaining) {
|
| 369 |
+
if (costs[remaining] != 99) return costs[remaining];
|
| 370 |
+
int best = 98;
|
| 371 |
+
const int first = remaining & -remaining;
|
| 372 |
+
for (int sub = remaining; sub; sub = (sub - 1) & remaining) {
|
| 373 |
+
if (!(sub & first) || match(sub) < 0) continue;
|
| 374 |
+
const int cost = 1 + solve(remaining ^ sub);
|
| 375 |
+
if (cost < best) { best = cost; choices[remaining] = sub; }
|
| 376 |
+
}
|
| 377 |
+
return costs[remaining] = best;
|
| 378 |
+
};
|
| 379 |
+
if (solve(mask) >= static_cast<int>(incumbent.size())) return incumbent;
|
| 380 |
+
std::vector<int64_t> result;
|
| 381 |
+
for (int remaining = mask; remaining; remaining ^= choices[remaining])
|
| 382 |
+
result.push_back(match(choices[remaining]));
|
| 383 |
+
return result;
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
std::vector<int64_t> repack(const int64_t* cells, int64_t steps,
|
| 387 |
+
const std::vector<int64_t>& stream) const {
|
| 388 |
+
uint64_t full = 0;
|
| 389 |
+
for (int d = 0; d < width_; ++d) if (cells[d] >= 0) full |= uint64_t{1} << d;
|
| 390 |
+
std::vector<uint64_t> covered(steps, 0), fixed(steps, 0);
|
| 391 |
+
std::vector<std::vector<int64_t>> old_spatial(steps), temporal(steps);
|
| 392 |
+
int64_t front = 0;
|
| 393 |
+
for (int64_t token : stream) {
|
| 394 |
+
while (front < steps && covered[front] == full) ++front;
|
| 395 |
+
if (front == steps || token < 0 || token >= static_cast<int64_t>(patterns_.size()) ||
|
| 396 |
+
anchors_[token] != __builtin_ctzll(full & ~covered[front]))
|
| 397 |
+
throw std::invalid_argument("invalid original token stream");
|
| 398 |
+
const bool frozen = token >= spatial_size_;
|
| 399 |
+
(frozen ? temporal[front] : old_spatial[front]).push_back(token);
|
| 400 |
+
for (auto [offset, id] : patterns_[token]) {
|
| 401 |
+
if (front + offset >= steps || (covered[front + offset] & support_[id]))
|
| 402 |
+
throw std::invalid_argument("invalid original token coverage");
|
| 403 |
+
covered[front + offset] |= support_[id];
|
| 404 |
+
if (frozen) fixed[front + offset] |= support_[id];
|
| 405 |
+
}
|
| 406 |
+
}
|
| 407 |
+
std::vector<int64_t> result;
|
| 408 |
+
result.reserve(stream.size());
|
| 409 |
+
for (int64_t time = 0; time < steps; ++time) {
|
| 410 |
+
if (covered[time] != full) throw std::invalid_argument("incomplete original stream");
|
| 411 |
+
auto tokens = optimal(row_key(cells + time * width_), full & ~fixed[time], old_spatial[time]);
|
| 412 |
+
tokens.insert(tokens.end(), temporal[time].begin(), temporal[time].end());
|
| 413 |
+
std::sort(tokens.begin(), tokens.end(), [&](int64_t a, int64_t b) {
|
| 414 |
+
return std::make_pair(anchors_[a], a) < std::make_pair(anchors_[b], b);
|
| 415 |
+
});
|
| 416 |
+
result.insert(result.end(), tokens.begin(), tokens.end());
|
| 417 |
+
}
|
| 418 |
+
if (result.size() > stream.size()) throw std::runtime_error("packing regressed");
|
| 419 |
+
return result;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
const std::vector<int64_t>& bins_;
|
| 423 |
+
int width_;
|
| 424 |
+
int64_t spatial_size_;
|
| 425 |
+
const std::vector<uint64_t>& support_;
|
| 426 |
+
const std::vector<std::vector<std::pair<int64_t, int64_t>>>& patterns_;
|
| 427 |
+
std::vector<uint64_t> keys_, byte_masks_;
|
| 428 |
+
std::vector<int> anchors_, spans_, counts_;
|
| 429 |
+
std::vector<Cover> relative_;
|
| 430 |
+
std::vector<std::vector<Requirement>> requirements_;
|
| 431 |
+
std::vector<std::vector<int64_t>> temporal_by_root_;
|
| 432 |
+
std::vector<TrieNode> trie_;
|
| 433 |
+
std::array<std::vector<int32_t>, 8> trie_roots_;
|
| 434 |
+
int max_cells_ = 1;
|
| 435 |
+
std::unordered_map<uint64_t, int64_t> lookup_;
|
| 436 |
+
};
|
| 437 |
+
} // namespace ac2pack
|
actioncodec2_kernels/replay.cpp
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Parallel replay of a learned Spatial Set-BPE vocabulary over action cells.
|
| 2 |
+
|
| 3 |
+
#include <pybind11/numpy.h>
|
| 4 |
+
#include <pybind11/pybind11.h>
|
| 5 |
+
|
| 6 |
+
#include <algorithm>
|
| 7 |
+
#include <cstdint>
|
| 8 |
+
#include <functional>
|
| 9 |
+
#include <queue>
|
| 10 |
+
#include <tuple>
|
| 11 |
+
#include <vector>
|
| 12 |
+
|
| 13 |
+
#ifdef _OPENMP
|
| 14 |
+
#include <omp.h>
|
| 15 |
+
#endif
|
| 16 |
+
|
| 17 |
+
namespace py = pybind11;
|
| 18 |
+
|
| 19 |
+
namespace {
|
| 20 |
+
|
| 21 |
+
struct Replay {
|
| 22 |
+
int32_t dimension;
|
| 23 |
+
int32_t vocab_size;
|
| 24 |
+
std::vector<int32_t> offsets;
|
| 25 |
+
std::vector<int32_t> rule;
|
| 26 |
+
std::vector<int32_t> anchor;
|
| 27 |
+
|
| 28 |
+
int32_t child(int32_t left, int32_t right) const {
|
| 29 |
+
if (left > right) std::swap(left, right);
|
| 30 |
+
return rule[static_cast<size_t>(left) * vocab_size + right];
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
std::vector<int32_t> encode(const int32_t* code) const {
|
| 34 |
+
std::vector<int32_t> active;
|
| 35 |
+
active.reserve(dimension);
|
| 36 |
+
for (int32_t axis = 0; axis < dimension; ++axis) {
|
| 37 |
+
if (code[axis] < 0) continue;
|
| 38 |
+
active.push_back(offsets[axis] + code[axis]);
|
| 39 |
+
}
|
| 40 |
+
using Candidate = std::tuple<int32_t, int32_t, int32_t>;
|
| 41 |
+
std::priority_queue<Candidate, std::vector<Candidate>, std::greater<Candidate>> heap;
|
| 42 |
+
auto offer = [&](int32_t left, int32_t right) {
|
| 43 |
+
const int32_t value = child(left, right);
|
| 44 |
+
if (value >= 0) heap.emplace(value, std::min(left, right), std::max(left, right));
|
| 45 |
+
};
|
| 46 |
+
for (int32_t a = 0; a < static_cast<int32_t>(active.size()); ++a) {
|
| 47 |
+
for (int32_t b = a + 1; b < static_cast<int32_t>(active.size()); ++b) {
|
| 48 |
+
offer(active[a], active[b]);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
while (!heap.empty()) {
|
| 52 |
+
const auto [child_token, left, right] = heap.top();
|
| 53 |
+
heap.pop();
|
| 54 |
+
auto left_it = std::find(active.begin(), active.end(), left);
|
| 55 |
+
auto right_it = std::find(active.begin(), active.end(), right);
|
| 56 |
+
if (left_it == active.end() || right_it == active.end()) continue;
|
| 57 |
+
*left_it = child_token;
|
| 58 |
+
active.erase(right_it);
|
| 59 |
+
for (const int32_t other : active) {
|
| 60 |
+
if (other != child_token) offer(child_token, other);
|
| 61 |
+
}
|
| 62 |
+
}
|
| 63 |
+
std::sort(active.begin(), active.end(), [&](int32_t left, int32_t right) {
|
| 64 |
+
if (anchor[left] != anchor[right]) return anchor[left] < anchor[right];
|
| 65 |
+
return left < right;
|
| 66 |
+
});
|
| 67 |
+
return active;
|
| 68 |
+
}
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
} // namespace
|
| 72 |
+
|
| 73 |
+
py::tuple spatial_encode_instances(
|
| 74 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& codes,
|
| 75 |
+
const py::array_t<int64_t, py::array::c_style | py::array::forcecast>& row_anchors,
|
| 76 |
+
const std::vector<int32_t>& bins,
|
| 77 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& merges,
|
| 78 |
+
int threads) {
|
| 79 |
+
if (codes.ndim() != 2 || row_anchors.ndim() != 1 ||
|
| 80 |
+
row_anchors.shape(0) != codes.shape(0) ||
|
| 81 |
+
codes.shape(1) != static_cast<int64_t>(bins.size()) || merges.ndim() != 2 ||
|
| 82 |
+
merges.shape(1) != 2) {
|
| 83 |
+
throw std::invalid_argument("invalid spatial replay inputs");
|
| 84 |
+
}
|
| 85 |
+
Replay replay;
|
| 86 |
+
replay.dimension = static_cast<int32_t>(bins.size());
|
| 87 |
+
replay.offsets.assign(replay.dimension + 1, 0);
|
| 88 |
+
for (int32_t axis = 0; axis < replay.dimension; ++axis) {
|
| 89 |
+
replay.offsets[axis + 1] = replay.offsets[axis] + bins[axis];
|
| 90 |
+
}
|
| 91 |
+
const int32_t atoms = replay.offsets.back();
|
| 92 |
+
replay.vocab_size = atoms + static_cast<int32_t>(merges.shape(0));
|
| 93 |
+
replay.rule.assign(static_cast<size_t>(replay.vocab_size) * replay.vocab_size, -1);
|
| 94 |
+
replay.anchor.assign(replay.vocab_size, -1);
|
| 95 |
+
for (int32_t axis = 0; axis < replay.dimension; ++axis) {
|
| 96 |
+
for (int32_t token = replay.offsets[axis]; token < replay.offsets[axis + 1]; ++token) {
|
| 97 |
+
replay.anchor[token] = axis;
|
| 98 |
+
}
|
| 99 |
+
}
|
| 100 |
+
auto rules = merges.unchecked<2>();
|
| 101 |
+
for (int32_t index = 0; index < merges.shape(0); ++index) {
|
| 102 |
+
const int32_t left = rules(index, 0), right = rules(index, 1), child = atoms + index;
|
| 103 |
+
if (left < 0 || right < 0 || left >= child || right >= child) {
|
| 104 |
+
throw std::invalid_argument("invalid spatial merge rule");
|
| 105 |
+
}
|
| 106 |
+
replay.rule[static_cast<size_t>(std::min(left, right)) * replay.vocab_size +
|
| 107 |
+
std::max(left, right)] = child;
|
| 108 |
+
replay.anchor[child] = std::min(replay.anchor[left], replay.anchor[right]);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
const int64_t rows = codes.shape(0);
|
| 112 |
+
std::vector<int32_t> counts(rows);
|
| 113 |
+
const int32_t* source = codes.data();
|
| 114 |
+
for (int64_t row = 0; row < rows; ++row) {
|
| 115 |
+
int32_t active = 0;
|
| 116 |
+
for (int32_t axis = 0; axis < replay.dimension; ++axis) {
|
| 117 |
+
const int32_t value = source[row * replay.dimension + axis];
|
| 118 |
+
if (value < -1 || value >= bins[axis]) {
|
| 119 |
+
throw std::invalid_argument("a cell index is outside its coordinate's bin count");
|
| 120 |
+
}
|
| 121 |
+
active += value >= 0;
|
| 122 |
+
}
|
| 123 |
+
if (active == 0) throw std::invalid_argument("every row needs an active coordinate");
|
| 124 |
+
}
|
| 125 |
+
#ifndef _OPENMP
|
| 126 |
+
(void)threads;
|
| 127 |
+
#endif
|
| 128 |
+
{
|
| 129 |
+
py::gil_scoped_release release;
|
| 130 |
+
#ifdef _OPENMP
|
| 131 |
+
#pragma omp parallel for schedule(static) num_threads(threads > 0 ? threads : omp_get_max_threads())
|
| 132 |
+
#endif
|
| 133 |
+
for (int64_t row = 0; row < rows; ++row) {
|
| 134 |
+
counts[row] = static_cast<int32_t>(
|
| 135 |
+
replay.encode(source + row * replay.dimension).size());
|
| 136 |
+
}
|
| 137 |
+
}
|
| 138 |
+
std::vector<int64_t> offsets(rows + 1, 0);
|
| 139 |
+
for (int64_t row = 0; row < rows; ++row) offsets[row + 1] = offsets[row] + counts[row];
|
| 140 |
+
py::array_t<int32_t> tokens(offsets.back());
|
| 141 |
+
py::array_t<int64_t> anchors(offsets.back());
|
| 142 |
+
int32_t* output_tokens = tokens.mutable_data();
|
| 143 |
+
int64_t* output_anchors = anchors.mutable_data();
|
| 144 |
+
const int64_t* input_anchors = row_anchors.data();
|
| 145 |
+
{
|
| 146 |
+
py::gil_scoped_release release;
|
| 147 |
+
#ifdef _OPENMP
|
| 148 |
+
#pragma omp parallel for schedule(static) num_threads(threads > 0 ? threads : omp_get_max_threads())
|
| 149 |
+
#endif
|
| 150 |
+
for (int64_t row = 0; row < rows; ++row) {
|
| 151 |
+
const auto encoded = replay.encode(source + row * replay.dimension);
|
| 152 |
+
const int64_t begin = offsets[row];
|
| 153 |
+
for (int32_t index = 0; index < static_cast<int32_t>(encoded.size()); ++index) {
|
| 154 |
+
output_tokens[begin + index] = encoded[index];
|
| 155 |
+
output_anchors[begin + index] = input_anchors[row];
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
return py::make_tuple(tokens, anchors);
|
| 160 |
+
}
|
actioncodec2_kernels/resample.cpp
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
#include <pybind11/numpy.h>
|
| 2 |
#include <pybind11/pybind11.h>
|
| 3 |
|
|
@@ -21,6 +22,8 @@ struct Quat {
|
|
| 21 |
double x, y, z, w;
|
| 22 |
};
|
| 23 |
|
|
|
|
|
|
|
| 24 |
Quat so3_normalize(const Quat& q) {
|
| 25 |
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
| 26 |
if (norm < 1e-12) throw std::invalid_argument("a pose contains a zero quaternion");
|
|
@@ -53,6 +56,8 @@ void so3_to_rotvec(const Quat& raw, double* out) {
|
|
| 53 |
out[2] = q.z * scale;
|
| 54 |
}
|
| 55 |
|
|
|
|
|
|
|
| 56 |
Quat sp_from_quat(const Quat& q) {
|
| 57 |
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
| 58 |
if (!(norm > 0.0)) throw std::invalid_argument("found zero norm quaternions in `quat`");
|
|
@@ -97,6 +102,9 @@ Quat sp_from_rotvec(const double* v) {
|
|
| 97 |
return {scale * v[0], scale * v[1], scale * v[2], std::cos(angle / 2.0)};
|
| 98 |
}
|
| 99 |
|
|
|
|
|
|
|
|
|
|
| 100 |
int64_t ppoly_interval(const Vec& x, double t) {
|
| 101 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 102 |
if (t >= x[n - 1]) return n - 2;
|
|
@@ -104,6 +112,8 @@ int64_t ppoly_interval(const Vec& x, double t) {
|
|
| 104 |
return static_cast<int64_t>(std::upper_bound(x.begin(), x.end(), t) - x.begin()) - 1;
|
| 105 |
}
|
| 106 |
|
|
|
|
|
|
|
| 107 |
double ppoly_value(const double* c, double s) {
|
| 108 |
double result = 0.0, z = 1.0;
|
| 109 |
result = result + c[3] * z;
|
|
@@ -116,6 +126,8 @@ double ppoly_value(const double* c, double s) {
|
|
| 116 |
return result;
|
| 117 |
}
|
| 118 |
|
|
|
|
|
|
|
| 119 |
Vec hermite_coefficients(const Vec& x, const Vec& y, const Vec& slopes, int64_t cols) {
|
| 120 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 121 |
Vec c(static_cast<size_t>((n - 1) * 4 * cols));
|
|
@@ -148,6 +160,7 @@ void ppoly_evaluate(const Vec& x, const Vec& c, int64_t cols, const Vec& times,
|
|
| 148 |
}
|
| 149 |
}
|
| 150 |
|
|
|
|
| 151 |
void gtsv(Vec dl, Vec d, Vec du, Vec& b, int64_t cols) {
|
| 152 |
const int64_t n = static_cast<int64_t>(d.size());
|
| 153 |
auto pivot = [&](int64_t i, bool last) {
|
|
@@ -188,6 +201,7 @@ void gtsv(Vec dl, Vec d, Vec du, Vec& b, int64_t cols) {
|
|
| 188 |
}
|
| 189 |
}
|
| 190 |
|
|
|
|
| 191 |
Vec natural_cubic_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
| 192 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 193 |
Vec dx(n - 1), slope((n - 1) * cols);
|
|
@@ -195,7 +209,7 @@ Vec natural_cubic_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
|
| 195 |
dx[k] = x[k + 1] - x[k];
|
| 196 |
for (int64_t a = 0; a < cols; ++a) slope[k * cols + a] = (y[(k + 1) * cols + a] - y[k * cols + a]) / dx[k];
|
| 197 |
}
|
| 198 |
-
|
| 199 |
Vec upper(n, 0.0), diagonal(n, 0.0), lower(n, 0.0), b(n * cols);
|
| 200 |
for (int64_t k = 1; k < n - 1; ++k) diagonal[k] = 2.0 * (dx[k - 1] + dx[k]);
|
| 201 |
for (int64_t k = 2; k < n; ++k) upper[k] = dx[k - 2];
|
|
@@ -204,7 +218,7 @@ Vec natural_cubic_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
|
| 204 |
for (int64_t a = 0; a < cols; ++a)
|
| 205 |
b[k * cols + a] = 3.0 * (dx[k] * slope[(k - 1) * cols + a] + dx[k - 1] * slope[k * cols + a]);
|
| 206 |
}
|
| 207 |
-
|
| 208 |
diagonal[0] = 2.0 * dx[0];
|
| 209 |
upper[1] = dx[0];
|
| 210 |
diagonal[n - 1] = 2.0 * dx[n - 2];
|
|
@@ -230,6 +244,7 @@ double pchip_edge(double h0, double h1, double m0, double m1) {
|
|
| 230 |
return d;
|
| 231 |
}
|
| 232 |
|
|
|
|
| 233 |
Vec pchip_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
| 234 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 235 |
Vec h(n - 1), m((n - 1) * cols), dk(n * cols, 0.0);
|
|
@@ -262,6 +277,8 @@ Vec pchip_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
|
| 262 |
return dk;
|
| 263 |
}
|
| 264 |
|
|
|
|
|
|
|
| 265 |
using Mat3 = std::array<double, 9>;
|
| 266 |
|
| 267 |
Mat3 skew(const double* v) { return {0.0, -v[2], v[1], v[2], 0.0, -v[0], -v[1], v[0], 0.0}; }
|
|
@@ -363,7 +380,7 @@ void gbsv(int64_t n, int64_t kl, int64_t ku, Vec& ab, Vec& b) {
|
|
| 363 |
}
|
| 364 |
}
|
| 365 |
}
|
| 366 |
-
|
| 367 |
for (int64_t j = 0; j + 1 < n; ++j) {
|
| 368 |
const int64_t lm = std::min(kl, n - 1 - j);
|
| 369 |
if (pivots[j] != j) std::swap(b[pivots[j]], b[j]);
|
|
@@ -372,7 +389,7 @@ void gbsv(int64_t n, int64_t kl, int64_t ku, Vec& ab, Vec& b) {
|
|
| 372 |
for (int64_t i = 1; i <= lm; ++i) b[j + i] = b[j + i] + at(kv + i, j) * temp;
|
| 373 |
}
|
| 374 |
}
|
| 375 |
-
|
| 376 |
const int64_t k = kl + ku;
|
| 377 |
for (int64_t j = n - 1; j >= 0; --j) {
|
| 378 |
if (b[j] != 0.0) {
|
|
@@ -386,7 +403,7 @@ void gbsv(int64_t n, int64_t kl, int64_t ku, Vec& ab, Vec& b) {
|
|
| 386 |
struct RotationSpline {
|
| 387 |
Vec times;
|
| 388 |
std::vector<Quat> rotations;
|
| 389 |
-
Vec coefficients;
|
| 390 |
|
| 391 |
RotationSpline(const Vec& knots, const std::vector<Quat>& raw) : times(knots) {
|
| 392 |
const int64_t n = static_cast<int64_t>(knots.size());
|
|
@@ -416,8 +433,8 @@ struct RotationSpline {
|
|
| 416 |
}
|
| 417 |
}
|
| 418 |
|
| 419 |
-
|
| 420 |
-
|
| 421 |
static void solve_rates(const Vec& dt, Vec& rates, const Vec& rotvecs, Vec& rotvecs_dot) {
|
| 422 |
const int64_t m = static_cast<int64_t>(dt.size());
|
| 423 |
const double first[3] = {rates[0], rates[1], rates[2]};
|
|
@@ -426,7 +443,7 @@ struct RotationSpline {
|
|
| 426 |
a[k] = rate_to_rotvec_dot(&rotvecs[k * 3]);
|
| 427 |
a_inv[k] = rotvec_dot_to_rate(&rotvecs[k * 3]);
|
| 428 |
}
|
| 429 |
-
|
| 430 |
const int64_t size = 3 * (m - 1), kl = 5, ku = 5, ldab = 2 * kl + ku + 1;
|
| 431 |
Vec band(ldab * size, 0.0);
|
| 432 |
auto set = [&](int64_t i, int64_t j, double value) { band[(kl + ku + i - j) + ldab * j] = value; };
|
|
@@ -474,7 +491,7 @@ struct RotationSpline {
|
|
| 474 |
if (converged) break;
|
| 475 |
}
|
| 476 |
for (int64_t k = 0; k < m; ++k) matvec(a[k], &rates[k * 3], &rotvecs_dot[k * 3]);
|
| 477 |
-
|
| 478 |
Vec shifted(m * 3);
|
| 479 |
for (int r = 0; r < 3; ++r) shifted[r] = first[r];
|
| 480 |
for (int64_t i = 0; i < 3 * (m - 1); ++i) shifted[3 + i] = rates[i];
|
|
@@ -516,6 +533,8 @@ Quat slerp(const Quat& left, const Quat& right, double weight) {
|
|
| 516 |
return so3_normalize(out);
|
| 517 |
}
|
| 518 |
|
|
|
|
|
|
|
| 519 |
enum Kind { kLinear = 0, kHold = 1, kPose = 2 };
|
| 520 |
enum Method { kLinearMethod = 0, kPchip = 1, kCubic = 2 };
|
| 521 |
|
|
@@ -525,7 +544,8 @@ int64_t resampled_horizon(int64_t length, double source_hz, double target_hz) {
|
|
| 525 |
|
| 526 |
int64_t output_width(int kind, int64_t width) { return kind == kPose ? 7 : width; }
|
| 527 |
|
| 528 |
-
|
|
|
|
| 529 |
int kind, int64_t count, bool identity, int method, float* output) {
|
| 530 |
Vec values(steps * width);
|
| 531 |
for (int64_t i = 0; i < steps * width; ++i) values[i] = input[i];
|
|
@@ -565,7 +585,7 @@ void resample_row(const float* input, int64_t steps, int64_t width, double sourc
|
|
| 565 |
} else if (method == kCubic) {
|
| 566 |
const Vec c = hermite_coefficients(source_time, continuous, natural_cubic_slopes(source_time, continuous, cols), cols);
|
| 567 |
ppoly_evaluate(source_time, c, cols, interpolation_time, out_width, 0, result.data());
|
| 568 |
-
|
| 569 |
std::vector<int64_t> fallback;
|
| 570 |
for (int64_t a = 0; a < cols; ++a) {
|
| 571 |
double lower = continuous[a], upper = continuous[a];
|
|
@@ -635,14 +655,15 @@ int parse_method(const std::string& method) {
|
|
| 635 |
}
|
| 636 |
|
| 637 |
using F32 = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
|
|
|
| 638 |
|
| 639 |
-
void check_finite(const
|
| 640 |
-
const
|
| 641 |
for (py::ssize_t i = 0; i < values.size(); ++i)
|
| 642 |
if (!std::isfinite(data[i])) throw std::invalid_argument("values must be a non-empty finite [T, D] array");
|
| 643 |
}
|
| 644 |
|
| 645 |
-
F32 resample_uniform_batch(const
|
| 646 |
int64_t target_length, const std::string& method_name, int threads) {
|
| 647 |
if (values.ndim() != 3 || values.shape(0) < 1 || values.shape(1) < 1)
|
| 648 |
throw std::invalid_argument("values must have non-empty shape [B, T, D]");
|
|
@@ -658,7 +679,7 @@ F32 resample_uniform_batch(const F32& values, double source_hz, double target_hz
|
|
| 658 |
if (count < 1) throw std::invalid_argument("target_length must be a positive integer");
|
| 659 |
const int64_t out_width = output_width(kind, width);
|
| 660 |
F32 output({batch, count, out_width});
|
| 661 |
-
const
|
| 662 |
float* out = output.mutable_data();
|
| 663 |
{
|
| 664 |
py::gil_scoped_release release;
|
|
@@ -670,14 +691,15 @@ F32 resample_uniform_batch(const F32& values, double source_hz, double target_hz
|
|
| 670 |
return output;
|
| 671 |
}
|
| 672 |
|
|
|
|
| 673 |
template <class Row>
|
| 674 |
-
F32 map_rows(const
|
| 675 |
if (values.ndim() != 3 || values.shape(2) != in_width || values.shape(1) < 1)
|
| 676 |
throw std::invalid_argument("expected shape [B, T, " + std::to_string(in_width) + "]");
|
| 677 |
check_finite(values);
|
| 678 |
const int64_t batch = values.shape(0), steps = values.shape(1);
|
| 679 |
F32 output({batch, steps, out_width});
|
| 680 |
-
const
|
| 681 |
float* out = output.mutable_data();
|
| 682 |
{
|
| 683 |
py::gil_scoped_release release;
|
|
@@ -688,8 +710,9 @@ F32 map_rows(const F32& values, int64_t in_width, int64_t out_width, int threads
|
|
| 688 |
return output;
|
| 689 |
}
|
| 690 |
|
| 691 |
-
|
| 692 |
-
|
|
|
|
| 693 |
double position[3] = {0.0, 0.0, 0.0};
|
| 694 |
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 695 |
for (int64_t t = 0; t < steps; ++t) {
|
|
@@ -707,8 +730,9 @@ F32 integrate_eef_deltas_batch(const F32& deltas, int threads) {
|
|
| 707 |
});
|
| 708 |
}
|
| 709 |
|
| 710 |
-
|
| 711 |
-
|
|
|
|
| 712 |
for (int64_t t = 0; t < steps; ++t) {
|
| 713 |
for (int a = 0; a < 3; ++a) out[t * 7 + a] = in[t * 6 + a];
|
| 714 |
const Quat q = so3_from_rotvec(in[t * 6 + 3], in[t * 6 + 4], in[t * 6 + 5]);
|
|
@@ -720,8 +744,9 @@ F32 eef_pose_from_rotvec_batch(const F32& poses, int threads) {
|
|
| 720 |
});
|
| 721 |
}
|
| 722 |
|
| 723 |
-
|
| 724 |
-
|
|
|
|
| 725 |
for (int64_t t = 0; t < steps; ++t) {
|
| 726 |
for (int a = 0; a < 3; ++a) out[t * 6 + a] = in[t * 7 + a];
|
| 727 |
double rotvec[3];
|
|
@@ -731,8 +756,9 @@ F32 eef_pose_absolute_batch(const F32& poses, int threads) {
|
|
| 731 |
});
|
| 732 |
}
|
| 733 |
|
| 734 |
-
|
| 735 |
-
|
|
|
|
| 736 |
Quat previous{0.0, 0.0, 0.0, 1.0};
|
| 737 |
for (int64_t t = 0; t < steps; ++t) {
|
| 738 |
for (int a = 0; a < 3; ++a) {
|
|
@@ -748,8 +774,10 @@ F32 eef_pose_deltas_batch(const F32& poses, int threads) {
|
|
| 748 |
});
|
| 749 |
}
|
| 750 |
|
| 751 |
-
|
| 752 |
-
|
|
|
|
|
|
|
| 753 |
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 754 |
for (int64_t t = 0; t < steps; ++t) {
|
| 755 |
const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]};
|
|
@@ -762,8 +790,8 @@ F32 rotvec_left_to_right_batch(const F32& increments, int threads) {
|
|
| 762 |
});
|
| 763 |
}
|
| 764 |
|
| 765 |
-
F32 rotvec_right_to_left_batch(const
|
| 766 |
-
return map_rows(increments, 3, 3, threads, [](const
|
| 767 |
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 768 |
for (int64_t t = 0; t < steps; ++t) {
|
| 769 |
const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]};
|
|
@@ -776,7 +804,7 @@ F32 rotvec_right_to_left_batch(const F32& increments, int threads) {
|
|
| 776 |
});
|
| 777 |
}
|
| 778 |
|
| 779 |
-
}
|
| 780 |
|
| 781 |
void register_resample(py::module_& module) {
|
| 782 |
module.def("resample_uniform_batch", &resample_uniform_batch, py::arg("values"), py::arg("source_hz"),
|
|
|
|
| 1 |
+
|
| 2 |
#include <pybind11/numpy.h>
|
| 3 |
#include <pybind11/pybind11.h>
|
| 4 |
|
|
|
|
| 22 |
double x, y, z, w;
|
| 23 |
};
|
| 24 |
|
| 25 |
+
// ---- runtime ``_so3`` quaternion math (xyzw), NumPy evaluation order ----
|
| 26 |
+
|
| 27 |
Quat so3_normalize(const Quat& q) {
|
| 28 |
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
| 29 |
if (norm < 1e-12) throw std::invalid_argument("a pose contains a zero quaternion");
|
|
|
|
| 56 |
out[2] = q.z * scale;
|
| 57 |
}
|
| 58 |
|
| 59 |
+
// ---- scipy ``Rotation`` operations used by RotationSpline ----
|
| 60 |
+
|
| 61 |
Quat sp_from_quat(const Quat& q) {
|
| 62 |
const double norm = std::sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
| 63 |
if (!(norm > 0.0)) throw std::invalid_argument("found zero norm quaternions in `quat`");
|
|
|
|
| 102 |
return {scale * v[0], scale * v[1], scale * v[2], std::cos(angle / 2.0)};
|
| 103 |
}
|
| 104 |
|
| 105 |
+
// ---- piecewise cubics (scipy PPoly with CubicHermiteSpline coefficients) ----
|
| 106 |
+
|
| 107 |
+
// PPoly interval: x[i] <= t < x[i+1], extrapolating with the end intervals.
|
| 108 |
int64_t ppoly_interval(const Vec& x, double t) {
|
| 109 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 110 |
if (t >= x[n - 1]) return n - 2;
|
|
|
|
| 112 |
return static_cast<int64_t>(std::upper_bound(x.begin(), x.end(), t) - x.begin()) - 1;
|
| 113 |
}
|
| 114 |
|
| 115 |
+
// scipy ``evaluate_poly1`` for a cubic: sum of c[k-1-p] * s**p, powers built by
|
| 116 |
+
// repeated multiplication.
|
| 117 |
double ppoly_value(const double* c, double s) {
|
| 118 |
double result = 0.0, z = 1.0;
|
| 119 |
result = result + c[3] * z;
|
|
|
|
| 126 |
return result;
|
| 127 |
}
|
| 128 |
|
| 129 |
+
// Hermite coefficients per interval and column: c[(k*4 + p)*cols + a] with
|
| 130 |
+
// p = 0..3 the descending powers, as CubicHermiteSpline stacks them.
|
| 131 |
Vec hermite_coefficients(const Vec& x, const Vec& y, const Vec& slopes, int64_t cols) {
|
| 132 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 133 |
Vec c(static_cast<size_t>((n - 1) * 4 * cols));
|
|
|
|
| 160 |
}
|
| 161 |
}
|
| 162 |
|
| 163 |
+
// LAPACK dgtsv (reference algorithm) for several right-hand sides b[n][cols].
|
| 164 |
void gtsv(Vec dl, Vec d, Vec du, Vec& b, int64_t cols) {
|
| 165 |
const int64_t n = static_cast<int64_t>(d.size());
|
| 166 |
auto pivot = [&](int64_t i, bool last) {
|
|
|
|
| 201 |
}
|
| 202 |
}
|
| 203 |
|
| 204 |
+
// scipy CubicSpline(bc_type="natural") slopes at the knots.
|
| 205 |
Vec natural_cubic_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
| 206 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 207 |
Vec dx(n - 1), slope((n - 1) * cols);
|
|
|
|
| 209 |
dx[k] = x[k + 1] - x[k];
|
| 210 |
for (int64_t a = 0; a < cols; ++a) slope[k * cols + a] = (y[(k + 1) * cols + a] - y[k * cols + a]) / dx[k];
|
| 211 |
}
|
| 212 |
+
// Banded A (3 x n): A[0] superdiagonal, A[1] diagonal, A[2] subdiagonal.
|
| 213 |
Vec upper(n, 0.0), diagonal(n, 0.0), lower(n, 0.0), b(n * cols);
|
| 214 |
for (int64_t k = 1; k < n - 1; ++k) diagonal[k] = 2.0 * (dx[k - 1] + dx[k]);
|
| 215 |
for (int64_t k = 2; k < n; ++k) upper[k] = dx[k - 2];
|
|
|
|
| 218 |
for (int64_t a = 0; a < cols; ++a)
|
| 219 |
b[k * cols + a] = 3.0 * (dx[k] * slope[(k - 1) * cols + a] + dx[k - 1] * slope[k * cols + a]);
|
| 220 |
}
|
| 221 |
+
// Natural ends: second derivative zero (bc value 0.0).
|
| 222 |
diagonal[0] = 2.0 * dx[0];
|
| 223 |
upper[1] = dx[0];
|
| 224 |
diagonal[n - 1] = 2.0 * dx[n - 2];
|
|
|
|
| 244 |
return d;
|
| 245 |
}
|
| 246 |
|
| 247 |
+
// scipy PchipInterpolator derivatives at the knots.
|
| 248 |
Vec pchip_slopes(const Vec& x, const Vec& y, int64_t cols) {
|
| 249 |
const int64_t n = static_cast<int64_t>(x.size());
|
| 250 |
Vec h(n - 1), m((n - 1) * cols), dk(n * cols, 0.0);
|
|
|
|
| 277 |
return dk;
|
| 278 |
}
|
| 279 |
|
| 280 |
+
// ---- scipy RotationSpline ----
|
| 281 |
+
|
| 282 |
using Mat3 = std::array<double, 9>;
|
| 283 |
|
| 284 |
Mat3 skew(const double* v) { return {0.0, -v[2], v[1], v[2], 0.0, -v[0], -v[1], v[0], 0.0}; }
|
|
|
|
| 380 |
}
|
| 381 |
}
|
| 382 |
}
|
| 383 |
+
// Forward: L, with the row interchanges.
|
| 384 |
for (int64_t j = 0; j + 1 < n; ++j) {
|
| 385 |
const int64_t lm = std::min(kl, n - 1 - j);
|
| 386 |
if (pivots[j] != j) std::swap(b[pivots[j]], b[j]);
|
|
|
|
| 389 |
for (int64_t i = 1; i <= lm; ++i) b[j + i] = b[j + i] + at(kv + i, j) * temp;
|
| 390 |
}
|
| 391 |
}
|
| 392 |
+
// Backward: U, a band of width kl+ku above the diagonal (dtbsv).
|
| 393 |
const int64_t k = kl + ku;
|
| 394 |
for (int64_t j = n - 1; j >= 0; --j) {
|
| 395 |
if (b[j] != 0.0) {
|
|
|
|
| 403 |
struct RotationSpline {
|
| 404 |
Vec times;
|
| 405 |
std::vector<Quat> rotations;
|
| 406 |
+
Vec coefficients; // ppoly layout, 3 columns
|
| 407 |
|
| 408 |
RotationSpline(const Vec& knots, const std::vector<Quat>& raw) : times(knots) {
|
| 409 |
const int64_t n = static_cast<int64_t>(knots.size());
|
|
|
|
| 433 |
}
|
| 434 |
}
|
| 435 |
|
| 436 |
+
// scipy RotationSpline._solve_for_angular_rates; ``rates`` comes back as the
|
| 437 |
+
// knot angular rates, ``rotvecs_dot`` as the segment rotation-vector rates.
|
| 438 |
static void solve_rates(const Vec& dt, Vec& rates, const Vec& rotvecs, Vec& rotvecs_dot) {
|
| 439 |
const int64_t m = static_cast<int64_t>(dt.size());
|
| 440 |
const double first[3] = {rates[0], rates[1], rates[2]};
|
|
|
|
| 443 |
a[k] = rate_to_rotvec_dot(&rotvecs[k * 3]);
|
| 444 |
a_inv[k] = rotvec_dot_to_rate(&rotvecs[k * 3]);
|
| 445 |
}
|
| 446 |
+
// Block-tridiagonal system of m-1 unknown 3-vectors in (5, 5) band form.
|
| 447 |
const int64_t size = 3 * (m - 1), kl = 5, ku = 5, ldab = 2 * kl + ku + 1;
|
| 448 |
Vec band(ldab * size, 0.0);
|
| 449 |
auto set = [&](int64_t i, int64_t j, double value) { band[(kl + ku + i - j) + ldab * j] = value; };
|
|
|
|
| 491 |
if (converged) break;
|
| 492 |
}
|
| 493 |
for (int64_t k = 0; k < m; ++k) matvec(a[k], &rates[k * 3], &rotvecs_dot[k * 3]);
|
| 494 |
+
// Knot rates: the fixed first rate, then the solved interior ones.
|
| 495 |
Vec shifted(m * 3);
|
| 496 |
for (int r = 0; r < 3; ++r) shifted[r] = first[r];
|
| 497 |
for (int64_t i = 0; i < 3 * (m - 1); ++i) shifted[3 + i] = rates[i];
|
|
|
|
| 533 |
return so3_normalize(out);
|
| 534 |
}
|
| 535 |
|
| 536 |
+
// ---- runtime resample_uniform ----
|
| 537 |
+
|
| 538 |
enum Kind { kLinear = 0, kHold = 1, kPose = 2 };
|
| 539 |
enum Method { kLinearMethod = 0, kPchip = 1, kCubic = 2 };
|
| 540 |
|
|
|
|
| 544 |
|
| 545 |
int64_t output_width(int kind, int64_t width) { return kind == kPose ? 7 : width; }
|
| 546 |
|
| 547 |
+
// One row: float32 [T, D] in, float32 [count, D'] out.
|
| 548 |
+
void resample_row(const double* input, int64_t steps, int64_t width, double source_hz, double target_hz,
|
| 549 |
int kind, int64_t count, bool identity, int method, float* output) {
|
| 550 |
Vec values(steps * width);
|
| 551 |
for (int64_t i = 0; i < steps * width; ++i) values[i] = input[i];
|
|
|
|
| 585 |
} else if (method == kCubic) {
|
| 586 |
const Vec c = hermite_coefficients(source_time, continuous, natural_cubic_slopes(source_time, continuous, cols), cols);
|
| 587 |
ppoly_evaluate(source_time, c, cols, interpolation_time, out_width, 0, result.data());
|
| 588 |
+
// Guard: fall back to PCHIP on axes that materially leave their envelope.
|
| 589 |
std::vector<int64_t> fallback;
|
| 590 |
for (int64_t a = 0; a < cols; ++a) {
|
| 591 |
double lower = continuous[a], upper = continuous[a];
|
|
|
|
| 655 |
}
|
| 656 |
|
| 657 |
using F32 = py::array_t<float, py::array::c_style | py::array::forcecast>;
|
| 658 |
+
using F64 = py::array_t<double, py::array::c_style | py::array::forcecast>;
|
| 659 |
|
| 660 |
+
void check_finite(const F64& values) {
|
| 661 |
+
const double* data = values.data();
|
| 662 |
for (py::ssize_t i = 0; i < values.size(); ++i)
|
| 663 |
if (!std::isfinite(data[i])) throw std::invalid_argument("values must be a non-empty finite [T, D] array");
|
| 664 |
}
|
| 665 |
|
| 666 |
+
F32 resample_uniform_batch(const F64& values, double source_hz, double target_hz, const std::string& kind_name,
|
| 667 |
int64_t target_length, const std::string& method_name, int threads) {
|
| 668 |
if (values.ndim() != 3 || values.shape(0) < 1 || values.shape(1) < 1)
|
| 669 |
throw std::invalid_argument("values must have non-empty shape [B, T, D]");
|
|
|
|
| 679 |
if (count < 1) throw std::invalid_argument("target_length must be a positive integer");
|
| 680 |
const int64_t out_width = output_width(kind, width);
|
| 681 |
F32 output({batch, count, out_width});
|
| 682 |
+
const double* in = values.data();
|
| 683 |
float* out = output.mutable_data();
|
| 684 |
{
|
| 685 |
py::gil_scoped_release release;
|
|
|
|
| 691 |
return output;
|
| 692 |
}
|
| 693 |
|
| 694 |
+
// Elementwise pose maps over [B, T, D] float32 arrays.
|
| 695 |
template <class Row>
|
| 696 |
+
F32 map_rows(const F64& values, int64_t in_width, int64_t out_width, int threads, Row&& row) {
|
| 697 |
if (values.ndim() != 3 || values.shape(2) != in_width || values.shape(1) < 1)
|
| 698 |
throw std::invalid_argument("expected shape [B, T, " + std::to_string(in_width) + "]");
|
| 699 |
check_finite(values);
|
| 700 |
const int64_t batch = values.shape(0), steps = values.shape(1);
|
| 701 |
F32 output({batch, steps, out_width});
|
| 702 |
+
const double* in = values.data();
|
| 703 |
float* out = output.mutable_data();
|
| 704 |
{
|
| 705 |
py::gil_scoped_release release;
|
|
|
|
| 710 |
return output;
|
| 711 |
}
|
| 712 |
|
| 713 |
+
// runtime integrate_eef_deltas: [T, 6] body increments -> [T, 7] absolute poses.
|
| 714 |
+
F32 integrate_eef_deltas_batch(const F64& deltas, int threads) {
|
| 715 |
+
return map_rows(deltas, 6, 7, threads, [](const double* in, int64_t steps, float* out) {
|
| 716 |
double position[3] = {0.0, 0.0, 0.0};
|
| 717 |
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 718 |
for (int64_t t = 0; t < steps; ++t) {
|
|
|
|
| 730 |
});
|
| 731 |
}
|
| 732 |
|
| 733 |
+
// runtime eef_pose_from_components(..., "rotation_vector").
|
| 734 |
+
F32 eef_pose_from_rotvec_batch(const F64& poses, int threads) {
|
| 735 |
+
return map_rows(poses, 6, 7, threads, [](const double* in, int64_t steps, float* out) {
|
| 736 |
for (int64_t t = 0; t < steps; ++t) {
|
| 737 |
for (int a = 0; a < 3; ++a) out[t * 7 + a] = in[t * 6 + a];
|
| 738 |
const Quat q = so3_from_rotvec(in[t * 6 + 3], in[t * 6 + 4], in[t * 6 + 5]);
|
|
|
|
| 744 |
});
|
| 745 |
}
|
| 746 |
|
| 747 |
+
// runtime eef_pose_absolute: [T, 7] xyz + xyzw -> [T, 6] xyz + rotation vector.
|
| 748 |
+
F32 eef_pose_absolute_batch(const F64& poses, int threads) {
|
| 749 |
+
return map_rows(poses, 7, 6, threads, [](const double* in, int64_t steps, float* out) {
|
| 750 |
for (int64_t t = 0; t < steps; ++t) {
|
| 751 |
for (int a = 0; a < 3; ++a) out[t * 6 + a] = in[t * 7 + a];
|
| 752 |
double rotvec[3];
|
|
|
|
| 756 |
});
|
| 757 |
}
|
| 758 |
|
| 759 |
+
// runtime eef_pose_deltas: local [T, 7] path -> [T, 6] body increments.
|
| 760 |
+
F32 eef_pose_deltas_batch(const F64& poses, int threads) {
|
| 761 |
+
return map_rows(poses, 7, 6, threads, [](const double* in, int64_t steps, float* out) {
|
| 762 |
Quat previous{0.0, 0.0, 0.0, 1.0};
|
| 763 |
for (int64_t t = 0; t < steps; ++t) {
|
| 764 |
for (int a = 0; a < 3; ++a) {
|
|
|
|
| 774 |
});
|
| 775 |
}
|
| 776 |
|
| 777 |
+
// Re-express per-step rotation increments between world-frame (left) and
|
| 778 |
+
// body-frame (right) multiplication, as scipy ``Rotation`` composes them.
|
| 779 |
+
F32 rotvec_left_to_right_batch(const F64& increments, int threads) {
|
| 780 |
+
return map_rows(increments, 3, 3, threads, [](const double* in, int64_t steps, float* out) {
|
| 781 |
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 782 |
for (int64_t t = 0; t < steps; ++t) {
|
| 783 |
const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]};
|
|
|
|
| 790 |
});
|
| 791 |
}
|
| 792 |
|
| 793 |
+
F32 rotvec_right_to_left_batch(const F64& increments, int threads) {
|
| 794 |
+
return map_rows(increments, 3, 3, threads, [](const double* in, int64_t steps, float* out) {
|
| 795 |
Quat current{0.0, 0.0, 0.0, 1.0};
|
| 796 |
for (int64_t t = 0; t < steps; ++t) {
|
| 797 |
const double v[3] = {in[t * 3], in[t * 3 + 1], in[t * 3 + 2]};
|
|
|
|
| 804 |
});
|
| 805 |
}
|
| 806 |
|
| 807 |
+
} // namespace
|
| 808 |
|
| 809 |
void register_resample(py::module_& module) {
|
| 810 |
module.def("resample_uniform_batch", &resample_uniform_batch, py::arg("values"), py::arg("source_hz"),
|
actioncodec2_kernels/setbpe.cpp
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
#include <pybind11/numpy.h>
|
| 2 |
#include <pybind11/pybind11.h>
|
| 3 |
#include <pybind11/stl.h>
|
|
@@ -5,6 +12,8 @@
|
|
| 5 |
#include <algorithm>
|
| 6 |
#include <cstdint>
|
| 7 |
#include <functional>
|
|
|
|
|
|
|
| 8 |
#include <queue>
|
| 9 |
#include <stdexcept>
|
| 10 |
#include <string>
|
|
@@ -14,6 +23,8 @@
|
|
| 14 |
#include <vector>
|
| 15 |
|
| 16 |
#include "parallel.hpp"
|
|
|
|
|
|
|
| 17 |
|
| 18 |
namespace py = pybind11;
|
| 19 |
|
|
@@ -74,7 +85,7 @@ class SetBPE {
|
|
| 74 |
for (int64_t token = 0; token < spatial_size_; ++token)
|
| 75 |
spatial_anchor_[token] = lowest_bit(support_[token]);
|
| 76 |
|
| 77 |
-
|
| 78 |
vocab_size_ = spatial_size_ + static_cast<int64_t>(temporal_merges.size());
|
| 79 |
patterns_.resize(vocab_size_);
|
| 80 |
root_.resize(vocab_size_);
|
|
@@ -113,9 +124,16 @@ class SetBPE {
|
|
| 113 |
}
|
| 114 |
|
| 115 |
int64_t vocab_size() const { return vocab_size_; }
|
| 116 |
-
int64_t dimension() const { return
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
|
|
|
| 119 |
std::vector<int64_t> encode(const int64_t* cells, int64_t steps) const {
|
| 120 |
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 121 |
if (steps < 1) throw std::invalid_argument("every episode must have at least one step");
|
|
@@ -134,10 +152,10 @@ class SetBPE {
|
|
| 134 |
throw std::invalid_argument("active dimensions must remain constant within an episode");
|
| 135 |
}
|
| 136 |
|
| 137 |
-
|
| 138 |
std::vector<int64_t> tokens, anchors;
|
| 139 |
std::vector<char> active;
|
| 140 |
-
std::vector<std::vector<std::pair<int64_t, int64_t>>> at(steps);
|
| 141 |
for (int64_t t = 0; t < steps; ++t) {
|
| 142 |
for (int64_t token : spatial_encode(cells + t * dimension)) {
|
| 143 |
at[t].emplace_back(token, static_cast<int64_t>(tokens.size()));
|
|
@@ -147,7 +165,7 @@ class SetBPE {
|
|
| 147 |
}
|
| 148 |
}
|
| 149 |
|
| 150 |
-
|
| 151 |
using Offer = std::tuple<int64_t, int64_t, int64_t, int64_t, int64_t, int64_t>;
|
| 152 |
std::priority_queue<Offer, std::vector<Offer>, std::greater<Offer>> frontier;
|
| 153 |
auto offer = [&](int64_t left_instance, int64_t right_instance) {
|
|
@@ -218,7 +236,7 @@ class SetBPE {
|
|
| 218 |
return stream;
|
| 219 |
}
|
| 220 |
|
| 221 |
-
|
| 222 |
std::vector<int64_t> decode(const int64_t* stream, int64_t length, uint64_t full) const {
|
| 223 |
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 224 |
std::vector<uint64_t> coverage{0};
|
|
@@ -264,7 +282,7 @@ class SetBPE {
|
|
| 264 |
}
|
| 265 |
|
| 266 |
private:
|
| 267 |
-
|
| 268 |
std::vector<int64_t> spatial_encode(const int64_t* row) const {
|
| 269 |
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 270 |
std::vector<int64_t> present;
|
|
@@ -272,7 +290,7 @@ class SetBPE {
|
|
| 272 |
if (row[d] >= 0) present.push_back(offsets_[d] + row[d]);
|
| 273 |
}
|
| 274 |
std::sort(present.begin(), present.end());
|
| 275 |
-
using Candidate = std::tuple<int64_t, int64_t, int64_t, int64_t>;
|
| 276 |
std::priority_queue<Candidate, std::vector<Candidate>, std::greater<Candidate>> heap;
|
| 277 |
for (size_t i = 0; i < present.size(); ++i) {
|
| 278 |
for (size_t j = i + 1; j < present.size(); ++j) {
|
|
@@ -281,7 +299,7 @@ class SetBPE {
|
|
| 281 |
heap.emplace(found->second.rank, found->second.child, present[i], present[j]);
|
| 282 |
}
|
| 283 |
}
|
| 284 |
-
std::vector<int64_t> tokens = present;
|
| 285 |
auto contains = [&](int64_t token) {
|
| 286 |
return std::find(tokens.begin(), tokens.end(), token) != tokens.end();
|
| 287 |
};
|
|
@@ -325,13 +343,40 @@ class SetBPE {
|
|
| 325 |
std::vector<std::vector<std::pair<int64_t, int64_t>>> patterns_;
|
| 326 |
std::vector<int64_t> root_;
|
| 327 |
std::unordered_map<std::tuple<int64_t, int64_t, int64_t>, Rule, TupleHash> temporal_rules_;
|
|
|
|
|
|
|
| 328 |
};
|
| 329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
py::list encode_batch(const SetBPE& codec,
|
| 331 |
const py::array_t<int64_t, py::array::c_style | py::array::forcecast>& cells,
|
| 332 |
int threads) {
|
| 333 |
-
if (cells.ndim() != 3
|
| 334 |
-
throw std::invalid_argument("cells must have shape [B, T, D] with the codec's dimension");
|
| 335 |
const int64_t batch = cells.shape(0), steps = cells.shape(1), width = cells.shape(2);
|
| 336 |
const int64_t* data = cells.data();
|
| 337 |
std::vector<std::vector<int64_t>> streams(batch);
|
|
@@ -346,15 +391,11 @@ py::list encode_batch(const SetBPE& codec,
|
|
| 346 |
return output;
|
| 347 |
}
|
| 348 |
|
|
|
|
| 349 |
py::array_t<int64_t> decode_batch(const SetBPE& codec, const std::vector<std::vector<int64_t>>& streams,
|
| 350 |
int64_t width, uint64_t full, int threads) {
|
| 351 |
const int64_t batch = static_cast<int64_t>(streams.size());
|
| 352 |
if (batch < 1) throw std::invalid_argument("streams must not be empty");
|
| 353 |
-
if (width != codec.dimension())
|
| 354 |
-
throw std::invalid_argument("width must equal the codec's dimension");
|
| 355 |
-
const uint64_t all = (uint64_t{1} << codec.dimension()) - 1;
|
| 356 |
-
if (!full || (full & ~all))
|
| 357 |
-
throw std::invalid_argument("full_support must select existing dimensions");
|
| 358 |
std::vector<std::vector<int64_t>> cells(batch);
|
| 359 |
{
|
| 360 |
py::gil_scoped_release release;
|
|
@@ -375,7 +416,7 @@ py::array_t<int64_t> decode_batch(const SetBPE& codec, const std::vector<std::ve
|
|
| 375 |
return output;
|
| 376 |
}
|
| 377 |
|
| 378 |
-
}
|
| 379 |
|
| 380 |
void register_setbpe(py::module_& module) {
|
| 381 |
py::class_<SetBPE>(module, "SetBPE")
|
|
@@ -386,38 +427,14 @@ void register_setbpe(py::module_& module) {
|
|
| 386 |
.def_property_readonly("vocab_size", &SetBPE::vocab_size)
|
| 387 |
.def("encode_batch", &encode_batch, py::arg("cells"), py::arg("threads") = 0,
|
| 388 |
"Dense [B, T, D] cells (-1 = absent) -> one token stream per row.")
|
|
|
|
|
|
|
|
|
|
| 389 |
.def("decode_batch", &decode_batch, py::arg("streams"), py::arg("width"),
|
| 390 |
py::arg("full_support"), py::arg("threads") = 0,
|
| 391 |
"Equal-horizon streams -> dense [B, T, D] cells, -1 outside full_support.")
|
| 392 |
.def("decode_checked_batch", [](const SetBPE& codec, const py::iterable& streams,
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
py::object integral;
|
| 396 |
-
std::vector<std::vector<int64_t>> rows;
|
| 397 |
-
if (PyList_Check(streams.ptr())) rows.reserve(PyList_GET_SIZE(streams.ptr()));
|
| 398 |
-
for (py::handle stream : streams) {
|
| 399 |
-
std::vector<int64_t> row;
|
| 400 |
-
if (PyList_Check(stream.ptr())) row.reserve(PyList_GET_SIZE(stream.ptr()));
|
| 401 |
-
for (py::handle raw : py::reinterpret_borrow<py::iterable>(stream)) {
|
| 402 |
-
bool integer = PyLong_Check(raw.ptr()) || py::isinstance(raw, numpy_integer);
|
| 403 |
-
if (!integer) {
|
| 404 |
-
if (!integral) integral = py::module_::import("numbers").attr("Integral");
|
| 405 |
-
integer = py::isinstance(raw, integral);
|
| 406 |
-
}
|
| 407 |
-
if (PyBool_Check(raw.ptr()) || !integer)
|
| 408 |
-
throw std::invalid_argument("tokens must be integers");
|
| 409 |
-
py::object number = PyLong_Check(raw.ptr())
|
| 410 |
-
? py::reinterpret_borrow<py::object>(raw)
|
| 411 |
-
: py::reinterpret_steal<py::object>(PyNumber_Long(raw.ptr()));
|
| 412 |
-
if (!number) throw py::error_already_set();
|
| 413 |
-
int overflow = 0;
|
| 414 |
-
const int64_t token = PyLong_AsLongLongAndOverflow(number.ptr(), &overflow);
|
| 415 |
-
if (PyErr_Occurred()) throw py::error_already_set();
|
| 416 |
-
if (overflow) throw std::invalid_argument("token is outside the vocabulary");
|
| 417 |
-
row.push_back(token);
|
| 418 |
-
}
|
| 419 |
-
rows.push_back(std::move(row));
|
| 420 |
-
}
|
| 421 |
-
return decode_batch(codec, rows, width, full, threads);
|
| 422 |
}, py::arg("streams"), py::arg("width"), py::arg("full_support"), py::arg("threads") = 0);
|
| 423 |
}
|
|
|
|
| 1 |
+
// Set-BPE serialization: quantized cell trajectories <-> token streams.
|
| 2 |
+
//
|
| 3 |
+
// A port of the runtime's SpatialVocabulary / TemporalVocabulary (Python,
|
| 4 |
+
// ``tokenization/setbpe_{spatial,temporal}.py``). Every merge is applied from
|
| 5 |
+
// a priority queue ordered exactly as the Python heaps order their tuples, so
|
| 6 |
+
// the token streams are identical, not merely equivalent.
|
| 7 |
+
|
| 8 |
#include <pybind11/numpy.h>
|
| 9 |
#include <pybind11/pybind11.h>
|
| 10 |
#include <pybind11/stl.h>
|
|
|
|
| 12 |
#include <algorithm>
|
| 13 |
#include <cstdint>
|
| 14 |
#include <functional>
|
| 15 |
+
#include <memory>
|
| 16 |
+
#include <mutex>
|
| 17 |
#include <queue>
|
| 18 |
#include <stdexcept>
|
| 19 |
#include <string>
|
|
|
|
| 23 |
#include <vector>
|
| 24 |
|
| 25 |
#include "parallel.hpp"
|
| 26 |
+
#include "repacking.hpp"
|
| 27 |
+
#include "token_streams.hpp"
|
| 28 |
|
| 29 |
namespace py = pybind11;
|
| 30 |
|
|
|
|
| 85 |
for (int64_t token = 0; token < spatial_size_; ++token)
|
| 86 |
spatial_anchor_[token] = lowest_bit(support_[token]);
|
| 87 |
|
| 88 |
+
// Rooted temporal tokens: a pattern of (time offset, spatial token) pairs.
|
| 89 |
vocab_size_ = spatial_size_ + static_cast<int64_t>(temporal_merges.size());
|
| 90 |
patterns_.resize(vocab_size_);
|
| 91 |
root_.resize(vocab_size_);
|
|
|
|
| 124 |
}
|
| 125 |
|
| 126 |
int64_t vocab_size() const { return vocab_size_; }
|
| 127 |
+
int64_t dimension() const { return bins_.size(); }
|
| 128 |
|
| 129 |
+
const ac2pack::Search& packing_search() const {
|
| 130 |
+
std::call_once(packing_once_, [&] {
|
| 131 |
+
packing_ = std::make_unique<ac2pack::Search>(bins_, support_, expansion_, patterns_, root_);
|
| 132 |
+
});
|
| 133 |
+
return *packing_;
|
| 134 |
+
}
|
| 135 |
|
| 136 |
+
// One dense [T, D] trajectory with -1 for semantically absent slots.
|
| 137 |
std::vector<int64_t> encode(const int64_t* cells, int64_t steps) const {
|
| 138 |
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 139 |
if (steps < 1) throw std::invalid_argument("every episode must have at least one step");
|
|
|
|
| 152 |
throw std::invalid_argument("active dimensions must remain constant within an episode");
|
| 153 |
}
|
| 154 |
|
| 155 |
+
// Instances, as in the Python encoder: token, anchor step, liveness.
|
| 156 |
std::vector<int64_t> tokens, anchors;
|
| 157 |
std::vector<char> active;
|
| 158 |
+
std::vector<std::vector<std::pair<int64_t, int64_t>>> at(steps); // (token, instance)
|
| 159 |
for (int64_t t = 0; t < steps; ++t) {
|
| 160 |
for (int64_t token : spatial_encode(cells + t * dimension)) {
|
| 161 |
at[t].emplace_back(token, static_cast<int64_t>(tokens.size()));
|
|
|
|
| 165 |
}
|
| 166 |
}
|
| 167 |
|
| 168 |
+
// (rank, anchor, root anchor dimension, left instance, right instance, child)
|
| 169 |
using Offer = std::tuple<int64_t, int64_t, int64_t, int64_t, int64_t, int64_t>;
|
| 170 |
std::priority_queue<Offer, std::vector<Offer>, std::greater<Offer>> frontier;
|
| 171 |
auto offer = [&](int64_t left_instance, int64_t right_instance) {
|
|
|
|
| 236 |
return stream;
|
| 237 |
}
|
| 238 |
|
| 239 |
+
// Tokens -> dense [T, D] cells (-1 outside ``full``), validating like the runtime.
|
| 240 |
std::vector<int64_t> decode(const int64_t* stream, int64_t length, uint64_t full) const {
|
| 241 |
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 242 |
std::vector<uint64_t> coverage{0};
|
|
|
|
| 282 |
}
|
| 283 |
|
| 284 |
private:
|
| 285 |
+
// Spatial Set-BPE of one dense row: merges in rank order, then anchor order.
|
| 286 |
std::vector<int64_t> spatial_encode(const int64_t* row) const {
|
| 287 |
const int64_t dimension = static_cast<int64_t>(bins_.size());
|
| 288 |
std::vector<int64_t> present;
|
|
|
|
| 290 |
if (row[d] >= 0) present.push_back(offsets_[d] + row[d]);
|
| 291 |
}
|
| 292 |
std::sort(present.begin(), present.end());
|
| 293 |
+
using Candidate = std::tuple<int64_t, int64_t, int64_t, int64_t>; // rank, child, left, right
|
| 294 |
std::priority_queue<Candidate, std::vector<Candidate>, std::greater<Candidate>> heap;
|
| 295 |
for (size_t i = 0; i < present.size(); ++i) {
|
| 296 |
for (size_t j = i + 1; j < present.size(); ++j) {
|
|
|
|
| 299 |
heap.emplace(found->second.rank, found->second.child, present[i], present[j]);
|
| 300 |
}
|
| 301 |
}
|
| 302 |
+
std::vector<int64_t> tokens = present; // a small set; linear lookups are fastest
|
| 303 |
auto contains = [&](int64_t token) {
|
| 304 |
return std::find(tokens.begin(), tokens.end(), token) != tokens.end();
|
| 305 |
};
|
|
|
|
| 343 |
std::vector<std::vector<std::pair<int64_t, int64_t>>> patterns_;
|
| 344 |
std::vector<int64_t> root_;
|
| 345 |
std::unordered_map<std::tuple<int64_t, int64_t, int64_t>, Rule, TupleHash> temporal_rules_;
|
| 346 |
+
mutable std::once_flag packing_once_;
|
| 347 |
+
mutable std::unique_ptr<ac2pack::Search> packing_;
|
| 348 |
};
|
| 349 |
|
| 350 |
+
py::list encode_search_batch(const SetBPE& codec,
|
| 351 |
+
const py::array_t<int64_t, py::array::c_style | py::array::forcecast>& cells,
|
| 352 |
+
bool spatial, bool temporal, int window, int beam, int threads) {
|
| 353 |
+
if (cells.ndim() != 3 || cells.shape(0) < 1 || cells.shape(1) < 1 ||
|
| 354 |
+
cells.shape(2) != codec.dimension())
|
| 355 |
+
throw std::invalid_argument("expected nonempty [B,T,D] cells matching codec width");
|
| 356 |
+
if (window < 1 || window * codec.dimension() > 256 || beam < 1 || beam > 64)
|
| 357 |
+
throw std::invalid_argument("packing requires window*D <= 256 and beam in [1,64]");
|
| 358 |
+
const auto* search = spatial || temporal ? &codec.packing_search() : nullptr;
|
| 359 |
+
const int64_t batch = cells.shape(0), steps = cells.shape(1), width = cells.shape(2);
|
| 360 |
+
std::vector<std::vector<int64_t>> streams(batch);
|
| 361 |
+
{
|
| 362 |
+
py::gil_scoped_release release;
|
| 363 |
+
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
| 364 |
+
const auto* row = cells.data() + index * steps * width;
|
| 365 |
+
auto original = codec.encode(row, steps);
|
| 366 |
+
streams[index] = search ? search->apply(row, steps, original, spatial, temporal, window, beam)
|
| 367 |
+
: std::move(original);
|
| 368 |
+
});
|
| 369 |
+
}
|
| 370 |
+
py::list output(batch);
|
| 371 |
+
for (int64_t index = 0; index < batch; ++index) output[index] = py::cast(streams[index]);
|
| 372 |
+
return output;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// Encode B dense [T, D] trajectories in parallel; returns one stream per row.
|
| 376 |
py::list encode_batch(const SetBPE& codec,
|
| 377 |
const py::array_t<int64_t, py::array::c_style | py::array::forcecast>& cells,
|
| 378 |
int threads) {
|
| 379 |
+
if (cells.ndim() != 3) throw std::invalid_argument("cells must have shape [B, T, D]");
|
|
|
|
| 380 |
const int64_t batch = cells.shape(0), steps = cells.shape(1), width = cells.shape(2);
|
| 381 |
const int64_t* data = cells.data();
|
| 382 |
std::vector<std::vector<int64_t>> streams(batch);
|
|
|
|
| 391 |
return output;
|
| 392 |
}
|
| 393 |
|
| 394 |
+
// Decode B equal-horizon streams to dense [B, T, D] cells (-1 outside ``full``).
|
| 395 |
py::array_t<int64_t> decode_batch(const SetBPE& codec, const std::vector<std::vector<int64_t>>& streams,
|
| 396 |
int64_t width, uint64_t full, int threads) {
|
| 397 |
const int64_t batch = static_cast<int64_t>(streams.size());
|
| 398 |
if (batch < 1) throw std::invalid_argument("streams must not be empty");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 399 |
std::vector<std::vector<int64_t>> cells(batch);
|
| 400 |
{
|
| 401 |
py::gil_scoped_release release;
|
|
|
|
| 416 |
return output;
|
| 417 |
}
|
| 418 |
|
| 419 |
+
} // namespace
|
| 420 |
|
| 421 |
void register_setbpe(py::module_& module) {
|
| 422 |
py::class_<SetBPE>(module, "SetBPE")
|
|
|
|
| 427 |
.def_property_readonly("vocab_size", &SetBPE::vocab_size)
|
| 428 |
.def("encode_batch", &encode_batch, py::arg("cells"), py::arg("threads") = 0,
|
| 429 |
"Dense [B, T, D] cells (-1 = absent) -> one token stream per row.")
|
| 430 |
+
.def("encode_search_batch", &encode_search_batch, py::arg("cells"),
|
| 431 |
+
py::arg("spatial_search") = false, py::arg("temporal_search") = false,
|
| 432 |
+
py::arg("search_window") = 32, py::arg("search_beam") = 4, py::arg("threads") = 0)
|
| 433 |
.def("decode_batch", &decode_batch, py::arg("streams"), py::arg("width"),
|
| 434 |
py::arg("full_support"), py::arg("threads") = 0,
|
| 435 |
"Equal-horizon streams -> dense [B, T, D] cells, -1 outside full_support.")
|
| 436 |
.def("decode_checked_batch", [](const SetBPE& codec, const py::iterable& streams,
|
| 437 |
+
int64_t width, uint64_t full, int threads) {
|
| 438 |
+
return decode_batch(codec, ac2::checked_token_streams(streams, codec.vocab_size()), width, full, threads);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
}, py::arg("streams"), py::arg("width"), py::arg("full_support"), py::arg("threads") = 0);
|
| 440 |
}
|
actioncodec2_kernels/so3.cpp
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
#include <pybind11/numpy.h>
|
| 2 |
#include <pybind11/pybind11.h>
|
| 3 |
|
|
@@ -104,6 +106,8 @@ struct SO3Layout {
|
|
| 104 |
bool left;
|
| 105 |
int32_t input_order;
|
| 106 |
double projection_radius = std::numeric_limits<double>::infinity();
|
|
|
|
|
|
|
| 107 |
};
|
| 108 |
|
| 109 |
void project_primitive(double* value, double radius) {
|
|
@@ -150,22 +154,31 @@ std::vector<int32_t> smooth_axis(const SO3Layout& layout, int axis, double reque
|
|
| 150 |
return output;
|
| 151 |
}
|
| 152 |
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
std::array<int32_t, 3> smooth_choice(const SO3Layout& layout,
|
| 159 |
const std::array<double, 3>& request,
|
| 160 |
const Quaternion& reconstruction,
|
| 161 |
const Quaternion& target,
|
| 162 |
const std::array<double, 3>& direction,
|
| 163 |
-
const std::array<int32_t, 3>& nearest
|
|
|
|
| 164 |
const auto x = smooth_axis(layout, 0, request[0], nearest[0]);
|
| 165 |
const auto y = smooth_axis(layout, 1, request[1], nearest[1]);
|
| 166 |
const auto z = smooth_axis(layout, 2, request[2], nearest[2]);
|
| 167 |
-
|
| 168 |
-
double any_error = 0.0, preferred_error = 0.0;
|
| 169 |
bool found_any = false, found_preferred = false;
|
| 170 |
for (const int32_t ix : x) {
|
| 171 |
for (const int32_t iy : y) {
|
|
@@ -179,23 +192,30 @@ std::array<int32_t, 3> smooth_choice(const SO3Layout& layout,
|
|
| 179 |
const double error = std::sqrt(remaining[0] * remaining[0] + remaining[1] * remaining[1] +
|
| 180 |
remaining[2] * remaining[2]);
|
| 181 |
if (error > layout.limit + 1e-12) continue;
|
| 182 |
-
if (
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
found_any = true;
|
| 186 |
}
|
| 187 |
const double dot = vector[0] * direction[0] + vector[1] * direction[1] +
|
| 188 |
vector[2] * direction[2];
|
| 189 |
-
if (dot >= -1e-15 &&
|
| 190 |
-
|
| 191 |
-
best_preferred = choice;
|
| 192 |
-
preferred_error = error;
|
| 193 |
found_preferred = true;
|
| 194 |
}
|
| 195 |
}
|
| 196 |
}
|
| 197 |
}
|
| 198 |
-
return found_preferred ? best_preferred : (found_any ? best_any : nearest);
|
| 199 |
}
|
| 200 |
|
| 201 |
void encode_trajectory(const SO3Layout& layout, const float* actions, int64_t steps,
|
|
@@ -203,14 +223,16 @@ void encode_trajectory(const SO3Layout& layout, const float* actions, int64_t st
|
|
| 203 |
Quaternion truth{0.0, 0.0, 0.0, 1.0};
|
| 204 |
Quaternion reconstruction = truth;
|
| 205 |
std::array<double, 3> direction{};
|
|
|
|
| 206 |
for (int64_t step = 0; step < steps; ++step) {
|
| 207 |
const Quaternion value = from_input(actions + 3 * step, layout.rotation_vector);
|
| 208 |
truth = layout.input_order == 0 ? value : apply(truth, value, layout.left);
|
| 209 |
const auto request = as_rotvec(residual(truth, reconstruction, layout.left));
|
| 210 |
auto choice = nearest_choice(layout, request);
|
| 211 |
if (layout.smooth) {
|
| 212 |
-
choice = smooth_choice(layout, request, reconstruction, truth, direction, choice);
|
| 213 |
}
|
|
|
|
| 214 |
const double vector[3] = {primitive(layout, 0, choice[0]),
|
| 215 |
primitive(layout, 1, choice[1]),
|
| 216 |
primitive(layout, 2, choice[2])};
|
|
@@ -222,7 +244,7 @@ void encode_trajectory(const SO3Layout& layout, const float* actions, int64_t st
|
|
| 222 |
}
|
| 223 |
}
|
| 224 |
|
| 225 |
-
}
|
| 226 |
|
| 227 |
py::array_t<int32_t> so3_second_order_encode_batch(
|
| 228 |
const py::array_t<double, py::array::c_style | py::array::forcecast>& table,
|
|
@@ -263,7 +285,7 @@ py::array_t<int32_t> so3_second_order_encode_batch(
|
|
| 263 |
const auto request = as_rotvec(correction);
|
| 264 |
auto choice = nearest_choice(layout, request);
|
| 265 |
if (smooth) {
|
| 266 |
-
|
| 267 |
SO3Layout selection = layout;
|
| 268 |
selection.left = false;
|
| 269 |
choice = smooth_choice(selection, request, identity, correction, direction, choice);
|
|
@@ -284,10 +306,11 @@ py::array_t<int32_t> so3_encode_batch(
|
|
| 284 |
const py::array_t<double, py::array::c_style | py::array::forcecast>& table,
|
| 285 |
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& bins,
|
| 286 |
const py::array_t<float, py::array::c_style | py::array::forcecast>& actions,
|
| 287 |
-
int32_t input_order, bool rotation_vector, bool left, double limit,
|
| 288 |
int threads) {
|
| 289 |
if (table.ndim() != 2 || table.shape(0) != 3 || bins.size() != 3 || actions.ndim() != 3 ||
|
| 290 |
-
actions.shape(2) != 3 || (input_order != 0 && input_order != 1) || limit <= 0.0
|
|
|
|
| 291 |
throw std::invalid_argument("invalid SO(3) batch encoder arguments");
|
| 292 |
}
|
| 293 |
for (int axis = 0; axis < 3; ++axis) {
|
|
@@ -295,11 +318,28 @@ py::array_t<int32_t> so3_encode_batch(
|
|
| 295 |
throw std::invalid_argument("SO(3) primitive count exceeds table width");
|
| 296 |
}
|
| 297 |
}
|
| 298 |
-
|
| 299 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
const int64_t batch = actions.shape(0);
|
| 301 |
const int64_t steps = actions.shape(1);
|
| 302 |
py::array_t<int32_t> output({batch, steps, int64_t{3}});
|
|
|
|
|
|
|
|
|
|
| 303 |
{
|
| 304 |
py::gil_scoped_release release;
|
| 305 |
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
|
|
|
| 1 |
+
// Intrinsic first- and stateful second-order SO(3) error-feedback encoders.
|
| 2 |
+
|
| 3 |
#include <pybind11/numpy.h>
|
| 4 |
#include <pybind11/pybind11.h>
|
| 5 |
|
|
|
|
| 106 |
bool left;
|
| 107 |
int32_t input_order;
|
| 108 |
double projection_radius = std::numeric_limits<double>::infinity();
|
| 109 |
+
bool lazy = false; // momentum_lazy ranking (first order only)
|
| 110 |
+
std::array<int32_t, 3> zero{}; // exact zero primitive per axis, used by ``lazy``
|
| 111 |
};
|
| 112 |
|
| 113 |
void project_primitive(double* value, double radius) {
|
|
|
|
| 154 |
return output;
|
| 155 |
}
|
| 156 |
|
| 157 |
+
// Lexicographic selection key: (moving axes, zero distance) under ``lazy``, then error, cell.
|
| 158 |
+
struct ChoiceKey {
|
| 159 |
+
int moving = 0;
|
| 160 |
+
int distance = 0;
|
| 161 |
+
double error = 0.0;
|
| 162 |
+
std::array<int32_t, 3> choice{};
|
| 163 |
+
bool operator<(const ChoiceKey& o) const {
|
| 164 |
+
if (moving != o.moving) return moving < o.moving;
|
| 165 |
+
if (distance != o.distance) return distance < o.distance;
|
| 166 |
+
if (error != o.error) return error < o.error;
|
| 167 |
+
return choice < o.choice;
|
| 168 |
+
}
|
| 169 |
+
};
|
| 170 |
|
| 171 |
std::array<int32_t, 3> smooth_choice(const SO3Layout& layout,
|
| 172 |
const std::array<double, 3>& request,
|
| 173 |
const Quaternion& reconstruction,
|
| 174 |
const Quaternion& target,
|
| 175 |
const std::array<double, 3>& direction,
|
| 176 |
+
const std::array<int32_t, 3>& nearest,
|
| 177 |
+
const std::array<int32_t, 3>& previous = {-1, -1, -1}) {
|
| 178 |
const auto x = smooth_axis(layout, 0, request[0], nearest[0]);
|
| 179 |
const auto y = smooth_axis(layout, 1, request[1], nearest[1]);
|
| 180 |
const auto z = smooth_axis(layout, 2, request[2], nearest[2]);
|
| 181 |
+
ChoiceKey best_any, best_preferred;
|
|
|
|
| 182 |
bool found_any = false, found_preferred = false;
|
| 183 |
for (const int32_t ix : x) {
|
| 184 |
for (const int32_t iy : y) {
|
|
|
|
| 192 |
const double error = std::sqrt(remaining[0] * remaining[0] + remaining[1] * remaining[1] +
|
| 193 |
remaining[2] * remaining[2]);
|
| 194 |
if (error > layout.limit + 1e-12) continue;
|
| 195 |
+
if (layout.lazy && choice == previous) return choice;
|
| 196 |
+
ChoiceKey key;
|
| 197 |
+
key.error = error;
|
| 198 |
+
key.choice = choice;
|
| 199 |
+
if (layout.lazy) {
|
| 200 |
+
for (int axis = 0; axis < 3; ++axis) {
|
| 201 |
+
key.moving += choice[axis] != layout.zero[axis];
|
| 202 |
+
key.distance += std::abs(choice[axis] - layout.zero[axis]);
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
if (!found_any || key < best_any) {
|
| 206 |
+
best_any = key;
|
| 207 |
found_any = true;
|
| 208 |
}
|
| 209 |
const double dot = vector[0] * direction[0] + vector[1] * direction[1] +
|
| 210 |
vector[2] * direction[2];
|
| 211 |
+
if (dot >= -1e-15 && (!found_preferred || key < best_preferred)) {
|
| 212 |
+
best_preferred = key;
|
|
|
|
|
|
|
| 213 |
found_preferred = true;
|
| 214 |
}
|
| 215 |
}
|
| 216 |
}
|
| 217 |
}
|
| 218 |
+
return found_preferred ? best_preferred.choice : (found_any ? best_any.choice : nearest);
|
| 219 |
}
|
| 220 |
|
| 221 |
void encode_trajectory(const SO3Layout& layout, const float* actions, int64_t steps,
|
|
|
|
| 223 |
Quaternion truth{0.0, 0.0, 0.0, 1.0};
|
| 224 |
Quaternion reconstruction = truth;
|
| 225 |
std::array<double, 3> direction{};
|
| 226 |
+
std::array<int32_t, 3> previous = layout.zero;
|
| 227 |
for (int64_t step = 0; step < steps; ++step) {
|
| 228 |
const Quaternion value = from_input(actions + 3 * step, layout.rotation_vector);
|
| 229 |
truth = layout.input_order == 0 ? value : apply(truth, value, layout.left);
|
| 230 |
const auto request = as_rotvec(residual(truth, reconstruction, layout.left));
|
| 231 |
auto choice = nearest_choice(layout, request);
|
| 232 |
if (layout.smooth) {
|
| 233 |
+
choice = smooth_choice(layout, request, reconstruction, truth, direction, choice, previous);
|
| 234 |
}
|
| 235 |
+
previous = choice;
|
| 236 |
const double vector[3] = {primitive(layout, 0, choice[0]),
|
| 237 |
primitive(layout, 1, choice[1]),
|
| 238 |
primitive(layout, 2, choice[2])};
|
|
|
|
| 244 |
}
|
| 245 |
}
|
| 246 |
|
| 247 |
+
} // namespace
|
| 248 |
|
| 249 |
py::array_t<int32_t> so3_second_order_encode_batch(
|
| 250 |
const py::array_t<double, py::array::c_style | py::array::forcecast>& table,
|
|
|
|
| 285 |
const auto request = as_rotvec(correction);
|
| 286 |
auto choice = nearest_choice(layout, request);
|
| 287 |
if (smooth) {
|
| 288 |
+
// Python scores U^-1 W for both conventions; use exactly that order.
|
| 289 |
SO3Layout selection = layout;
|
| 290 |
selection.left = false;
|
| 291 |
choice = smooth_choice(selection, request, identity, correction, direction, choice);
|
|
|
|
| 306 |
const py::array_t<double, py::array::c_style | py::array::forcecast>& table,
|
| 307 |
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& bins,
|
| 308 |
const py::array_t<float, py::array::c_style | py::array::forcecast>& actions,
|
| 309 |
+
int32_t input_order, bool rotation_vector, bool left, double limit, int32_t selection,
|
| 310 |
int threads) {
|
| 311 |
if (table.ndim() != 2 || table.shape(0) != 3 || bins.size() != 3 || actions.ndim() != 3 ||
|
| 312 |
+
actions.shape(2) != 3 || (input_order != 0 && input_order != 1) || limit <= 0.0 ||
|
| 313 |
+
selection < 0 || selection > 2) {
|
| 314 |
throw std::invalid_argument("invalid SO(3) batch encoder arguments");
|
| 315 |
}
|
| 316 |
for (int axis = 0; axis < 3; ++axis) {
|
|
|
|
| 318 |
throw std::invalid_argument("SO(3) primitive count exceeds table width");
|
| 319 |
}
|
| 320 |
}
|
| 321 |
+
// selection: 0 nearest, 1 smooth, 2 momentum_lazy
|
| 322 |
+
SO3Layout layout{table.data(), bins.data(), table.shape(1), limit, selection != 0,
|
| 323 |
+
rotation_vector, left, input_order};
|
| 324 |
+
layout.lazy = selection == 2;
|
| 325 |
+
for (int axis = 0; axis < 3; ++axis) {
|
| 326 |
+
double best = std::numeric_limits<double>::infinity();
|
| 327 |
+
for (int32_t index = 0; index < bins.data()[axis]; ++index) {
|
| 328 |
+
if (std::fabs(primitive(layout, axis, index)) < best) {
|
| 329 |
+
best = std::fabs(primitive(layout, axis, index));
|
| 330 |
+
layout.zero[axis] = index;
|
| 331 |
+
}
|
| 332 |
+
}
|
| 333 |
+
if (layout.lazy && best != 0.0) {
|
| 334 |
+
throw std::invalid_argument("momentum_lazy needs an exact zero primitive per axis");
|
| 335 |
+
}
|
| 336 |
+
}
|
| 337 |
const int64_t batch = actions.shape(0);
|
| 338 |
const int64_t steps = actions.shape(1);
|
| 339 |
py::array_t<int32_t> output({batch, steps, int64_t{3}});
|
| 340 |
+
#ifndef _OPENMP
|
| 341 |
+
(void)threads;
|
| 342 |
+
#endif
|
| 343 |
{
|
| 344 |
py::gil_scoped_release release;
|
| 345 |
ac2::parallel_for(batch, threads, [&](int64_t index) {
|
actioncodec2_kernels/sparse.cpp
ADDED
|
@@ -0,0 +1,1308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
#include <pybind11/numpy.h>
|
| 3 |
+
#include <pybind11/pybind11.h>
|
| 4 |
+
#include <pybind11/stl.h>
|
| 5 |
+
|
| 6 |
+
#include <algorithm>
|
| 7 |
+
#include <array>
|
| 8 |
+
#include <chrono>
|
| 9 |
+
#include <cstdint>
|
| 10 |
+
#include <limits>
|
| 11 |
+
#include <queue>
|
| 12 |
+
#include <set>
|
| 13 |
+
#include <stdexcept>
|
| 14 |
+
#include <string>
|
| 15 |
+
#include <tuple>
|
| 16 |
+
#include <unordered_map>
|
| 17 |
+
#include <vector>
|
| 18 |
+
|
| 19 |
+
#include "parallel.hpp"
|
| 20 |
+
|
| 21 |
+
#ifdef _OPENMP
|
| 22 |
+
#include <omp.h>
|
| 23 |
+
#endif
|
| 24 |
+
#if defined(_OPENMP) && defined(__GLIBCXX__)
|
| 25 |
+
#include <parallel/algorithm>
|
| 26 |
+
#define AC2_PARALLEL_SORT __gnu_parallel::sort
|
| 27 |
+
#else
|
| 28 |
+
#define AC2_PARALLEL_SORT std::sort
|
| 29 |
+
#endif
|
| 30 |
+
|
| 31 |
+
namespace py = pybind11;
|
| 32 |
+
|
| 33 |
+
namespace {
|
| 34 |
+
|
| 35 |
+
constexpr int kMaxLookahead = 32;
|
| 36 |
+
constexpr size_t kCertifyBatch = 32;
|
| 37 |
+
constexpr size_t kApplyGrain = 256; // minimum candidate pairs per parallel apply group
|
| 38 |
+
|
| 39 |
+
constexpr size_t kShards = 64; // rule-table shards updated concurrently after a merge
|
| 40 |
+
|
| 41 |
+
inline uint64_t mix(uint64_t key) { return key * 0x9E3779B97F4A7C15ull; }
|
| 42 |
+
inline size_t shard_of(uint64_t key) { return static_cast<size_t>(mix(key) >> 58); }
|
| 43 |
+
|
| 44 |
+
// Small open-addressing accumulator for rule-count deltas; clearing costs O(used).
|
| 45 |
+
class DeltaMap {
|
| 46 |
+
public:
|
| 47 |
+
void add(uint64_t key, int64_t delta) {
|
| 48 |
+
if ((filled_.size() + 1) * 2 > keys_.size()) grow();
|
| 49 |
+
size_t h = slot(key);
|
| 50 |
+
while (keys_[h] != kEmpty && keys_[h] != key) h = (h + 1) & (keys_.size() - 1);
|
| 51 |
+
if (keys_[h] == kEmpty) {
|
| 52 |
+
keys_[h] = key;
|
| 53 |
+
filled_.push_back(static_cast<uint32_t>(h));
|
| 54 |
+
}
|
| 55 |
+
values_[h] += delta;
|
| 56 |
+
}
|
| 57 |
+
template <class F>
|
| 58 |
+
void each(F&& visit) const {
|
| 59 |
+
for (uint32_t h : filled_) visit(keys_[h], values_[h]);
|
| 60 |
+
}
|
| 61 |
+
void clear() {
|
| 62 |
+
for (uint32_t h : filled_) {
|
| 63 |
+
keys_[h] = kEmpty;
|
| 64 |
+
values_[h] = 0;
|
| 65 |
+
}
|
| 66 |
+
filled_.clear();
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
private:
|
| 70 |
+
static constexpr uint64_t kEmpty = ~0ull;
|
| 71 |
+
size_t slot(uint64_t key) const {
|
| 72 |
+
return static_cast<size_t>(mix(key) >> 20) & (keys_.size() - 1);
|
| 73 |
+
}
|
| 74 |
+
void grow() {
|
| 75 |
+
std::vector<uint64_t> keys = std::move(keys_);
|
| 76 |
+
std::vector<int64_t> values = std::move(values_);
|
| 77 |
+
std::vector<uint32_t> filled = std::move(filled_);
|
| 78 |
+
const size_t capacity = std::max<size_t>(64, keys.size() * 2);
|
| 79 |
+
keys_.assign(capacity, kEmpty);
|
| 80 |
+
values_.assign(capacity, 0);
|
| 81 |
+
filled_.clear();
|
| 82 |
+
for (uint32_t h : filled) add(keys[h], values[h]);
|
| 83 |
+
}
|
| 84 |
+
std::vector<uint64_t> keys_;
|
| 85 |
+
std::vector<int64_t> values_;
|
| 86 |
+
std::vector<uint32_t> filled_;
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
inline uint64_t rule_key(int32_t left, int32_t right, int32_t offset) {
|
| 90 |
+
return (static_cast<uint64_t>(left) << 40) | (static_cast<uint64_t>(right) << 16) |
|
| 91 |
+
static_cast<uint64_t>(offset);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
inline void unpack(uint64_t key, int32_t& left, int32_t& right, int32_t& offset) {
|
| 95 |
+
left = static_cast<int32_t>(key >> 40);
|
| 96 |
+
right = static_cast<int32_t>((key >> 16) & 0xffffffull);
|
| 97 |
+
offset = static_cast<int32_t>(key & 0xffffull);
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// Immutable-per-token geometry shared by the trainer, encoder and decoder.
|
| 101 |
+
struct Tables {
|
| 102 |
+
int32_t dims = 0; // physical slots including E
|
| 103 |
+
int32_t e_dim = 0; // index of E
|
| 104 |
+
int32_t lookahead = 0; // maximum token span in frames
|
| 105 |
+
std::vector<uint32_t> masks; // token * lookahead -> support mask
|
| 106 |
+
std::vector<uint32_t> emask; // token -> bit per offset holding E
|
| 107 |
+
std::vector<int8_t> first_from; // token * lookahead + k -> first support offset >= k
|
| 108 |
+
std::vector<int32_t> span, rdim;
|
| 109 |
+
std::vector<std::vector<std::pair<int32_t, int32_t>>> pattern; // (offset, spatial token)
|
| 110 |
+
int32_t base = 0;
|
| 111 |
+
|
| 112 |
+
void init(const std::vector<uint32_t>& spatial_support, int32_t dims_, int32_t lookahead_) {
|
| 113 |
+
dims = dims_;
|
| 114 |
+
e_dim = dims_ - 1;
|
| 115 |
+
lookahead = lookahead_;
|
| 116 |
+
if (lookahead < 1 || lookahead > kMaxLookahead) throw std::invalid_argument("lookahead");
|
| 117 |
+
if (dims < 2 || dims > 16) throw std::invalid_argument("dims must be in [2, 16]");
|
| 118 |
+
base = static_cast<int32_t>(spatial_support.size());
|
| 119 |
+
for (int32_t t = 0; t < base; ++t) {
|
| 120 |
+
uint32_t s = spatial_support[t];
|
| 121 |
+
if (!s) throw std::invalid_argument("spatial token without support");
|
| 122 |
+
push({{0, t}}, std::vector<uint32_t>{s});
|
| 123 |
+
}
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
void push(std::vector<std::pair<int32_t, int32_t>> pat, const std::vector<uint32_t>& m) {
|
| 127 |
+
const size_t token = span.size();
|
| 128 |
+
masks.resize((token + 1) * lookahead, 0);
|
| 129 |
+
uint32_t e = 0;
|
| 130 |
+
int32_t sp = 0;
|
| 131 |
+
for (size_t o = 0; o < m.size(); ++o) {
|
| 132 |
+
masks[token * lookahead + o] = m[o];
|
| 133 |
+
if (m[o]) sp = static_cast<int32_t>(o) + 1;
|
| 134 |
+
if (m[o] >> e_dim & 1u) e |= 1u << o;
|
| 135 |
+
}
|
| 136 |
+
emask.push_back(e);
|
| 137 |
+
first_from.resize((token + 1) * lookahead, -1);
|
| 138 |
+
for (int32_t k = sp - 1, next = -1; k >= 0; --k) {
|
| 139 |
+
if (masks[token * lookahead + k]) next = k;
|
| 140 |
+
first_from[token * lookahead + k] = static_cast<int8_t>(next);
|
| 141 |
+
}
|
| 142 |
+
span.push_back(sp);
|
| 143 |
+
rdim.push_back(__builtin_ctz(m[0]));
|
| 144 |
+
pattern.push_back(std::move(pat));
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
int32_t size() const { return static_cast<int32_t>(span.size()); }
|
| 148 |
+
uint32_t mask(int32_t token, int32_t o) const {
|
| 149 |
+
return masks[static_cast<size_t>(token) * lookahead + o];
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// Merge geometry; returns false when the union would exceed the lookahead or overlap.
|
| 153 |
+
bool compose(int32_t a, int32_t b, int32_t off, std::vector<uint32_t>& out) const {
|
| 154 |
+
if (off < 0 || off >= lookahead) return false;
|
| 155 |
+
const int32_t sp = std::max(span[a], off + span[b]);
|
| 156 |
+
if (sp > lookahead) return false;
|
| 157 |
+
out.assign(sp, 0);
|
| 158 |
+
for (int32_t o = 0; o < span[a]; ++o) out[o] = mask(a, o);
|
| 159 |
+
for (int32_t o = 0; o < span[b]; ++o) {
|
| 160 |
+
if (out[o + off] & mask(b, o)) return false;
|
| 161 |
+
out[o + off] |= mask(b, o);
|
| 162 |
+
}
|
| 163 |
+
return true;
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
int32_t add_merge(int32_t a, int32_t b, int32_t off) {
|
| 167 |
+
std::vector<uint32_t> m;
|
| 168 |
+
if (!compose(a, b, off, m)) throw std::invalid_argument("illegal temporal merge");
|
| 169 |
+
auto pat = pattern[a];
|
| 170 |
+
for (auto [o, t] : pattern[b]) pat.push_back({o + off, t});
|
| 171 |
+
std::sort(pat.begin(), pat.end());
|
| 172 |
+
push(std::move(pat), m);
|
| 173 |
+
return size() - 1;
|
| 174 |
+
}
|
| 175 |
+
};
|
| 176 |
+
|
| 177 |
+
using Key = std::tuple<int64_t, int32_t, int64_t, int32_t>;
|
| 178 |
+
using InstanceId = uint32_t;
|
| 179 |
+
constexpr InstanceId kNoInstance = UINT32_MAX;
|
| 180 |
+
|
| 181 |
+
// Mutable instance state over a global frame axis. Used per chunk by the encoder and
|
| 182 |
+
// globally by the trainer.
|
| 183 |
+
struct Instances {
|
| 184 |
+
const Tables* tb = nullptr;
|
| 185 |
+
int64_t frames = 0;
|
| 186 |
+
std::vector<int32_t> tok;
|
| 187 |
+
std::vector<int64_t> root;
|
| 188 |
+
std::vector<uint8_t> alive;
|
| 189 |
+
std::vector<InstanceId> member; // frame * dims + dim -> instance
|
| 190 |
+
|
| 191 |
+
void reset(const Tables* tables, int64_t frame_count) {
|
| 192 |
+
tb = tables;
|
| 193 |
+
frames = frame_count;
|
| 194 |
+
tok.clear();
|
| 195 |
+
root.clear();
|
| 196 |
+
alive.clear();
|
| 197 |
+
member.assign(static_cast<size_t>(frame_count) * tables->dims, kNoInstance);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
InstanceId cell(int64_t f, int32_t d) const {
|
| 201 |
+
return member[static_cast<size_t>(f) * tb->dims + d];
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
Key key(InstanceId i) const {
|
| 205 |
+
const uint32_t e = tb->emask[tok[i]];
|
| 206 |
+
return Key(root[i], e ? 1 : 0, e ? root[i] + __builtin_ctz(e) : 0, tb->rdim[tok[i]]);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
InstanceId add(int32_t token, int64_t r) {
|
| 210 |
+
if (tok.size() >= kNoInstance) throw std::length_error("instance ids");
|
| 211 |
+
const InstanceId i = static_cast<InstanceId>(tok.size());
|
| 212 |
+
tok.push_back(token);
|
| 213 |
+
root.push_back(r);
|
| 214 |
+
alive.push_back(1);
|
| 215 |
+
place(i);
|
| 216 |
+
return i;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
// Reserves dead slots that disjoint frame ranges then fill concurrently via ``place``.
|
| 220 |
+
InstanceId reserve(size_t count) {
|
| 221 |
+
const size_t base = tok.size();
|
| 222 |
+
if (base + count > static_cast<size_t>(kNoInstance)) throw std::length_error("instance ids");
|
| 223 |
+
tok.resize(base + count, 0);
|
| 224 |
+
root.resize(base + count, 0);
|
| 225 |
+
alive.resize(base + count, 0);
|
| 226 |
+
return static_cast<InstanceId>(base);
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
void place(InstanceId i) {
|
| 230 |
+
const int32_t token = tok[i];
|
| 231 |
+
const int64_t r = root[i];
|
| 232 |
+
alive[i] = 1;
|
| 233 |
+
for (int32_t o = 0; o < tb->span[token]; ++o) {
|
| 234 |
+
uint32_t m = tb->mask(token, o);
|
| 235 |
+
while (m) {
|
| 236 |
+
const int32_t d = __builtin_ctz(m);
|
| 237 |
+
m &= m - 1;
|
| 238 |
+
InstanceId& slot = member[static_cast<size_t>(r + o) * tb->dims + d];
|
| 239 |
+
if (slot != kNoInstance) throw std::runtime_error("instance overlap");
|
| 240 |
+
slot = i;
|
| 241 |
+
}
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
void kill(InstanceId i) {
|
| 246 |
+
alive[i] = 0;
|
| 247 |
+
const int32_t token = tok[i];
|
| 248 |
+
for (int32_t o = 0; o < tb->span[token]; ++o) {
|
| 249 |
+
uint32_t m = tb->mask(token, o);
|
| 250 |
+
while (m) {
|
| 251 |
+
const int32_t d = __builtin_ctz(m);
|
| 252 |
+
m &= m - 1;
|
| 253 |
+
member[static_cast<size_t>(root[i] + o) * tb->dims + d] = kNoInstance;
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
bool first(InstanceId a, InstanceId b) const {
|
| 259 |
+
return std::make_pair(root[a], tb->rdim[tok[a]]) < std::make_pair(root[b], tb->rdim[tok[b]]);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// Ordered candidate pairs (a, b, offset) involving i inside the lookahead window.
|
| 263 |
+
// Each neighbour is emitted once, at its first support cell inside the window.
|
| 264 |
+
template <class F>
|
| 265 |
+
void partners(InstanceId i, bool allow_zero, F&& emit) const {
|
| 266 |
+
const int64_t r = root[i];
|
| 267 |
+
const int32_t L = tb->lookahead;
|
| 268 |
+
const int64_t lo = std::max<int64_t>(0, r - L + 1), hi = std::min<int64_t>(frames - 1, r + L - 1);
|
| 269 |
+
for (int64_t f = lo; f <= hi; ++f) {
|
| 270 |
+
for (int32_t d = 0; d < tb->dims; ++d) {
|
| 271 |
+
const InstanceId j = cell(f, d);
|
| 272 |
+
if (j == kNoInstance || j == i) continue;
|
| 273 |
+
const int32_t tj = tok[j];
|
| 274 |
+
const int64_t o = f - root[j];
|
| 275 |
+
const int64_t k = std::max<int64_t>(0, lo - root[j]);
|
| 276 |
+
if (tb->first_from[static_cast<size_t>(tj) * L + k] != o) continue;
|
| 277 |
+
if (__builtin_ctz(tb->mask(tj, static_cast<int32_t>(o))) != d) continue;
|
| 278 |
+
const bool ij = first(i, j);
|
| 279 |
+
const InstanceId a = ij ? i : j, b = ij ? j : i;
|
| 280 |
+
const int64_t off = root[b] - root[a];
|
| 281 |
+
if (off >= L || (off == 0 && !allow_zero)) continue;
|
| 282 |
+
if (std::max<int64_t>(tb->span[tok[a]], off + tb->span[tok[b]]) > L) continue;
|
| 283 |
+
emit(a, b, static_cast<int32_t>(off));
|
| 284 |
+
}
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// Would merging a (rooted first) with b keep the stream decodable?
|
| 289 |
+
bool valid(InstanceId a, InstanceId b, int32_t off) const {
|
| 290 |
+
const int32_t ta = tok[a], tb_ = tok[b];
|
| 291 |
+
const int32_t sp = std::max(tb->span[ta], off + tb->span[tb_]);
|
| 292 |
+
uint32_t emask = tb->emask[ta] | (tb->emask[tb_] << off);
|
| 293 |
+
const Key km(root[a], emask ? 1 : 0, emask ? root[a] + __builtin_ctz(emask) : 0,
|
| 294 |
+
tb->rdim[ta]);
|
| 295 |
+
for (int32_t o = 0; o < sp; ++o) {
|
| 296 |
+
uint32_t m = (o < tb->span[ta] ? tb->mask(ta, o) : 0) |
|
| 297 |
+
(o >= off && o - off < tb->span[tb_] ? tb->mask(tb_, o - off) : 0);
|
| 298 |
+
if (!m) continue;
|
| 299 |
+
const int64_t u = root[a] + o;
|
| 300 |
+
const InstanceId h = cell(u, tb->e_dim);
|
| 301 |
+
if (h != kNoInstance && h != a && h != b && !(key(h) > km)) return false;
|
| 302 |
+
if (emask >> o & 1u) {
|
| 303 |
+
for (int32_t d = 0; d < tb->dims; ++d) {
|
| 304 |
+
const InstanceId j = cell(u, d);
|
| 305 |
+
if (j != kNoInstance && j != a && j != b && !(key(j) < km)) return false;
|
| 306 |
+
}
|
| 307 |
+
}
|
| 308 |
+
}
|
| 309 |
+
return true;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
// Right partner of left instance a under rule (left, right, off), or -1.
|
| 313 |
+
InstanceId right_of(InstanceId a, int32_t right, int32_t off) const {
|
| 314 |
+
const int64_t f = root[a] + off;
|
| 315 |
+
if (f >= frames) return kNoInstance;
|
| 316 |
+
const InstanceId b = cell(f, tb->rdim[right]);
|
| 317 |
+
if (b == kNoInstance || b == a || !alive[b] || tok[b] != right || root[b] != f) return kNoInstance;
|
| 318 |
+
return b;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
// Left partner of right instance b under rule (left, right, off), or -1.
|
| 322 |
+
InstanceId left_of(InstanceId b, int32_t left, int32_t off) const {
|
| 323 |
+
const int64_t f = root[b] - off;
|
| 324 |
+
if (f < 0) return kNoInstance;
|
| 325 |
+
const InstanceId a = cell(f, tb->rdim[left]);
|
| 326 |
+
if (a == kNoInstance || a == b || !alive[a] || tok[a] != left || root[a] != f) return kNoInstance;
|
| 327 |
+
return a;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
std::vector<int32_t> serialize() const {
|
| 331 |
+
std::vector<InstanceId> live;
|
| 332 |
+
for (size_t i = 0; i < tok.size(); ++i)
|
| 333 |
+
if (alive[i]) live.push_back(static_cast<InstanceId>(i));
|
| 334 |
+
std::sort(live.begin(), live.end(), [&](InstanceId x, InstanceId y) { return key(x) < key(y); });
|
| 335 |
+
std::vector<int32_t> out;
|
| 336 |
+
out.reserve(live.size());
|
| 337 |
+
for (InstanceId i : live) out.push_back(tok[i]);
|
| 338 |
+
return out;
|
| 339 |
+
}
|
| 340 |
+
};
|
| 341 |
+
|
| 342 |
+
struct Stat {
|
| 343 |
+
bool retired = false; // a rule is learned at most once; leftovers stay unmerged
|
| 344 |
+
int64_t raw = 0;
|
| 345 |
+
uint32_t version = 0;
|
| 346 |
+
int64_t exact = -1; // matching- and validity-certified count at exact_version
|
| 347 |
+
uint32_t exact_version = 0;
|
| 348 |
+
int64_t added = 0; // cumulative pair insertions; bounds growth since certification
|
| 349 |
+
int64_t added_at_exact = 0;
|
| 350 |
+
uint32_t dirty_epoch = 0; // merge batch that last queued this key for re-push
|
| 351 |
+
bool live = false; // a heap entry (queued_version, queued_count) represents the key
|
| 352 |
+
uint32_t queued_version = 0;
|
| 353 |
+
int64_t queued_count = 0;
|
| 354 |
+
};
|
| 355 |
+
|
| 356 |
+
// Open-addressing rule table; references are invalidated only by inserting a new key.
|
| 357 |
+
class StatMap {
|
| 358 |
+
public:
|
| 359 |
+
Stat* find(uint64_t key) {
|
| 360 |
+
if (keys_.empty()) return nullptr;
|
| 361 |
+
for (size_t h = slot(key);; h = (h + 1) & (keys_.size() - 1)) {
|
| 362 |
+
if (keys_[h] == key) return &values_[h];
|
| 363 |
+
if (keys_[h] == kEmpty) return nullptr;
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
Stat& operator[](uint64_t key) {
|
| 367 |
+
if ((used_ + 1) * 2 > keys_.size()) grow();
|
| 368 |
+
size_t h = slot(key);
|
| 369 |
+
while (keys_[h] != kEmpty && keys_[h] != key) h = (h + 1) & (keys_.size() - 1);
|
| 370 |
+
if (keys_[h] == kEmpty) {
|
| 371 |
+
keys_[h] = key;
|
| 372 |
+
++used_;
|
| 373 |
+
}
|
| 374 |
+
return values_[h];
|
| 375 |
+
}
|
| 376 |
+
size_t size() const { return used_; }
|
| 377 |
+
template <class F>
|
| 378 |
+
void each(F&& visit) const {
|
| 379 |
+
for (size_t h = 0; h < keys_.size(); ++h)
|
| 380 |
+
if (keys_[h] != kEmpty) visit(keys_[h], values_[h]);
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
private:
|
| 384 |
+
static constexpr uint64_t kEmpty = ~0ull;
|
| 385 |
+
size_t slot(uint64_t key) const {
|
| 386 |
+
return static_cast<size_t>(mix(key) >> 20) & (keys_.size() - 1);
|
| 387 |
+
}
|
| 388 |
+
void grow() {
|
| 389 |
+
std::vector<uint64_t> keys = std::move(keys_);
|
| 390 |
+
std::vector<Stat> values = std::move(values_);
|
| 391 |
+
const size_t capacity = std::max<size_t>(1 << 16, keys.size() * 2);
|
| 392 |
+
keys_.assign(capacity, kEmpty);
|
| 393 |
+
values_.assign(capacity, Stat{});
|
| 394 |
+
used_ = 0;
|
| 395 |
+
for (size_t h = 0; h < keys.size(); ++h)
|
| 396 |
+
if (keys[h] != kEmpty) (*this)[keys[h]] = values[h];
|
| 397 |
+
}
|
| 398 |
+
std::vector<uint64_t> keys_;
|
| 399 |
+
std::vector<Stat> values_;
|
| 400 |
+
size_t used_ = 0;
|
| 401 |
+
};
|
| 402 |
+
|
| 403 |
+
// Rule table split by key hash so disjoint shards can be updated concurrently.
|
| 404 |
+
class ShardedStats {
|
| 405 |
+
public:
|
| 406 |
+
Stat* find(uint64_t key) { return shards_[shard_of(key)].find(key); }
|
| 407 |
+
Stat& operator[](uint64_t key) { return shards_[shard_of(key)][key]; }
|
| 408 |
+
StatMap& shard(size_t p) { return shards_[p]; }
|
| 409 |
+
size_t size() const {
|
| 410 |
+
size_t n = 0;
|
| 411 |
+
for (const auto& s : shards_) n += s.size();
|
| 412 |
+
return n;
|
| 413 |
+
}
|
| 414 |
+
template <class F>
|
| 415 |
+
void each(F&& visit) const {
|
| 416 |
+
for (const auto& s : shards_) s.each(visit);
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
private:
|
| 420 |
+
StatMap shards_[kShards];
|
| 421 |
+
};
|
| 422 |
+
|
| 423 |
+
struct Entry {
|
| 424 |
+
int64_t count;
|
| 425 |
+
uint64_t key;
|
| 426 |
+
uint32_t version;
|
| 427 |
+
bool operator<(const Entry& o) const {
|
| 428 |
+
if (count != o.count) return count < o.count;
|
| 429 |
+
return key > o.key;
|
| 430 |
+
}
|
| 431 |
+
};
|
| 432 |
+
|
| 433 |
+
class Trainer {
|
| 434 |
+
public:
|
| 435 |
+
Trainer(std::vector<uint32_t> spatial_support, int32_t dims, int32_t lookahead,
|
| 436 |
+
int64_t vocab_size, bool allow_zero, int64_t min_count, int threads,
|
| 437 |
+
bool dense_census = false, int64_t max_instances = UINT32_MAX)
|
| 438 |
+
: vocab_size_(vocab_size), allow_zero_(allow_zero), dense_census_(dense_census),
|
| 439 |
+
min_count_(min_count), max_instances_(max_instances) {
|
| 440 |
+
if (max_instances_ < 1 || max_instances_ > UINT32_MAX)
|
| 441 |
+
throw std::invalid_argument("max_instances");
|
| 442 |
+
tables_.init(spatial_support, dims, lookahead);
|
| 443 |
+
#ifdef _OPENMP
|
| 444 |
+
threads_ = threads > 0 ? threads : std::min(omp_get_max_threads(), 32);
|
| 445 |
+
#else
|
| 446 |
+
(void)threads;
|
| 447 |
+
threads_ = 1;
|
| 448 |
+
#endif
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
// anchors: global frame index per instance (chunks already separated by >= lookahead).
|
| 452 |
+
void set_instances(py::array_t<int64_t, py::array::c_style> anchors,
|
| 453 |
+
py::array_t<int32_t, py::array::c_style> tokens, int64_t frames) {
|
| 454 |
+
auto a = anchors.unchecked<1>();
|
| 455 |
+
auto t = tokens.unchecked<1>();
|
| 456 |
+
if (a.shape(0) != t.shape(0)) throw std::invalid_argument("anchors/tokens length");
|
| 457 |
+
if (a.shape(0) > max_instances_)
|
| 458 |
+
throw std::length_error("live instance capacity exceeded before training");
|
| 459 |
+
st_.reset(&tables_, frames);
|
| 460 |
+
// Pre-sized so references into the outer vector stay valid while children are added.
|
| 461 |
+
occurrence_.assign(static_cast<size_t>(vocab_size_) + 16, {});
|
| 462 |
+
dead_.assign(static_cast<size_t>(vocab_size_) + 16, 0);
|
| 463 |
+
for (py::ssize_t i = 0; i < a.shape(0); ++i) {
|
| 464 |
+
const int32_t tk = t(i);
|
| 465 |
+
if (tk < 0 || tk >= tables_.base) throw std::invalid_argument("token outside spatial vocab");
|
| 466 |
+
occurrence_[tk].push_back(st_.add(tk, a(i)));
|
| 467 |
+
}
|
| 468 |
+
for (auto& occ : occurrence_)
|
| 469 |
+
std::sort(occ.begin(), occ.end(), [&](InstanceId x, InstanceId y) { return st_.root[x] < st_.root[y]; });
|
| 470 |
+
initial_instances_ = a.shape(0);
|
| 471 |
+
// Supports never change, so frames preceded by >= lookahead empty frames split the
|
| 472 |
+
// axis into ranges whose merges never read or write each other's cells.
|
| 473 |
+
safe_.clear();
|
| 474 |
+
int64_t empty = tables_.lookahead;
|
| 475 |
+
for (int64_t f = 0; f < frames; ++f) {
|
| 476 |
+
bool occupied = false;
|
| 477 |
+
for (int32_t d = 0; d < tables_.dims && !occupied; ++d) occupied = st_.cell(f, d) != kNoInstance;
|
| 478 |
+
if (!occupied) {
|
| 479 |
+
++empty;
|
| 480 |
+
continue;
|
| 481 |
+
}
|
| 482 |
+
if (empty >= tables_.lookahead) safe_.push_back(f);
|
| 483 |
+
empty = 0;
|
| 484 |
+
}
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
py::dict train(bool progress) {
|
| 488 |
+
const auto t0 = std::chrono::steady_clock::now();
|
| 489 |
+
{
|
| 490 |
+
py::gil_scoped_release release;
|
| 491 |
+
census();
|
| 492 |
+
std::vector<uint64_t> keys;
|
| 493 |
+
keys.reserve(stats_.size());
|
| 494 |
+
stats_.each([&](uint64_t k, const Stat&) { keys.push_back(k); });
|
| 495 |
+
for (uint64_t k : keys) queue(k, stats_[k], stats_[k].raw);
|
| 496 |
+
initial_types_ = stats_.size();
|
| 497 |
+
census_seconds_ = seconds_since(t0);
|
| 498 |
+
run(progress);
|
| 499 |
+
}
|
| 500 |
+
py::dict report;
|
| 501 |
+
report["initial_instances"] = initial_instances_;
|
| 502 |
+
report["final_instances"] = live_;
|
| 503 |
+
report["initial_rule_types"] = initial_types_;
|
| 504 |
+
report["merges"] = static_cast<int64_t>(merges_.size());
|
| 505 |
+
report["dry_runs"] = dry_runs_;
|
| 506 |
+
report["rejected_by_validity"] = rejected_;
|
| 507 |
+
report["applied"] = applied_;
|
| 508 |
+
report["census_seconds"] = census_seconds_;
|
| 509 |
+
report["dry_run_seconds"] = dry_seconds_;
|
| 510 |
+
report["apply_seconds"] = apply_seconds_;
|
| 511 |
+
report["collect_seconds"] = collect_seconds_;
|
| 512 |
+
report["parallel_seconds"] = parallel_seconds_;
|
| 513 |
+
report["serial_seconds"] = serial_seconds_;
|
| 514 |
+
report["merge_seconds"] = merge_seconds_;
|
| 515 |
+
report["changes"] = changes_;
|
| 516 |
+
report["compactions"] = compactions_;
|
| 517 |
+
report["total_seconds"] = seconds_since(t0);
|
| 518 |
+
return report;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
std::vector<std::tuple<int32_t, int32_t, int32_t>> merges() const { return merges_; }
|
| 522 |
+
|
| 523 |
+
// Count every forward pair once: blockwise parallel key generation, parallel sort, RLE.
|
| 524 |
+
void census() {
|
| 525 |
+
const int64_t n = static_cast<int64_t>(st_.tok.size());
|
| 526 |
+
const int64_t base = tables_.base;
|
| 527 |
+
const int64_t universe = base * base * tables_.lookahead;
|
| 528 |
+
if (dense_census_ && universe <= 4000000) {
|
| 529 |
+
std::vector<std::vector<int64_t>> local(
|
| 530 |
+
threads_, std::vector<int64_t>(static_cast<size_t>(universe), 0));
|
| 531 |
+
#ifdef _OPENMP
|
| 532 |
+
#pragma omp parallel num_threads(threads_)
|
| 533 |
+
#endif
|
| 534 |
+
{
|
| 535 |
+
#ifdef _OPENMP
|
| 536 |
+
auto& counts = local[omp_get_thread_num()];
|
| 537 |
+
#else
|
| 538 |
+
auto& counts = local[0];
|
| 539 |
+
#endif
|
| 540 |
+
#ifdef _OPENMP
|
| 541 |
+
#pragma omp for schedule(static)
|
| 542 |
+
#endif
|
| 543 |
+
for (int64_t i = 0; i < n; ++i) {
|
| 544 |
+
const InstanceId ii = static_cast<InstanceId>(i);
|
| 545 |
+
st_.partners(ii, allow_zero_, [&](InstanceId a, InstanceId b, int32_t off) {
|
| 546 |
+
if (a == ii) {
|
| 547 |
+
const int64_t key = (static_cast<int64_t>(st_.tok[a]) * base + st_.tok[b]) *
|
| 548 |
+
tables_.lookahead + off;
|
| 549 |
+
++counts[static_cast<size_t>(key)];
|
| 550 |
+
}
|
| 551 |
+
});
|
| 552 |
+
}
|
| 553 |
+
}
|
| 554 |
+
for (int64_t key = 0; key < universe; ++key) {
|
| 555 |
+
int64_t count = 0;
|
| 556 |
+
for (const auto& counts : local) count += counts[static_cast<size_t>(key)];
|
| 557 |
+
if (!count) continue;
|
| 558 |
+
const int32_t offset = static_cast<int32_t>(key % tables_.lookahead);
|
| 559 |
+
const int64_t pair = key / tables_.lookahead;
|
| 560 |
+
const int32_t left = static_cast<int32_t>(pair / base);
|
| 561 |
+
const int32_t right = static_cast<int32_t>(pair % base);
|
| 562 |
+
stats_[rule_key(left, right, offset)].raw += count;
|
| 563 |
+
}
|
| 564 |
+
return;
|
| 565 |
+
}
|
| 566 |
+
const int64_t block = int64_t{1} << 22;
|
| 567 |
+
std::vector<std::vector<uint64_t>> local(threads_);
|
| 568 |
+
std::vector<uint64_t> keys;
|
| 569 |
+
for (int64_t start = 0; start < n; start += block) {
|
| 570 |
+
const int64_t stop = std::min(n, start + block);
|
| 571 |
+
#ifdef _OPENMP
|
| 572 |
+
#pragma omp parallel num_threads(threads_)
|
| 573 |
+
#endif
|
| 574 |
+
{
|
| 575 |
+
#ifdef _OPENMP
|
| 576 |
+
auto& out = local[omp_get_thread_num()];
|
| 577 |
+
#else
|
| 578 |
+
auto& out = local[0];
|
| 579 |
+
#endif
|
| 580 |
+
out.clear();
|
| 581 |
+
#ifdef _OPENMP
|
| 582 |
+
#pragma omp for schedule(static)
|
| 583 |
+
#endif
|
| 584 |
+
for (int64_t i = start; i < stop; ++i) {
|
| 585 |
+
const InstanceId ii = static_cast<InstanceId>(i);
|
| 586 |
+
st_.partners(ii, allow_zero_, [&](InstanceId a, InstanceId b, int32_t off) {
|
| 587 |
+
if (a == ii) out.push_back(rule_key(st_.tok[a], st_.tok[b], off));
|
| 588 |
+
});
|
| 589 |
+
}
|
| 590 |
+
}
|
| 591 |
+
keys.clear();
|
| 592 |
+
for (auto& v : local) keys.insert(keys.end(), v.begin(), v.end());
|
| 593 |
+
AC2_PARALLEL_SORT(keys.begin(), keys.end());
|
| 594 |
+
for (size_t k = 0; k < keys.size();) {
|
| 595 |
+
size_t e = k;
|
| 596 |
+
while (e < keys.size() && keys[e] == keys[k]) ++e;
|
| 597 |
+
stats_[keys[k]].raw += static_cast<int64_t>(e - k);
|
| 598 |
+
k = e;
|
| 599 |
+
}
|
| 600 |
+
}
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
private:
|
| 604 |
+
// Reclaim dead slots without changing token order, coverage, or candidate statistics.
|
| 605 |
+
void compact_instances() {
|
| 606 |
+
std::vector<InstanceId> remap(st_.tok.size(), kNoInstance);
|
| 607 |
+
std::vector<int32_t> tokens;
|
| 608 |
+
std::vector<int64_t> roots;
|
| 609 |
+
std::vector<uint8_t> alive;
|
| 610 |
+
tokens.reserve(static_cast<size_t>(live_));
|
| 611 |
+
roots.reserve(static_cast<size_t>(live_));
|
| 612 |
+
alive.reserve(static_cast<size_t>(live_));
|
| 613 |
+
for (size_t old = 0; old < st_.tok.size(); ++old) {
|
| 614 |
+
if (!st_.alive[old]) continue;
|
| 615 |
+
remap[old] = static_cast<InstanceId>(tokens.size());
|
| 616 |
+
tokens.push_back(st_.tok[old]);
|
| 617 |
+
roots.push_back(st_.root[old]);
|
| 618 |
+
alive.push_back(1);
|
| 619 |
+
}
|
| 620 |
+
if (tokens.size() != static_cast<size_t>(live_))
|
| 621 |
+
throw std::runtime_error("instance compaction live count mismatch");
|
| 622 |
+
for (auto& occ : occurrence_) {
|
| 623 |
+
size_t next = 0;
|
| 624 |
+
for (InstanceId old : occ) {
|
| 625 |
+
const InstanceId mapped = remap[static_cast<size_t>(old)];
|
| 626 |
+
if (mapped != kNoInstance) occ[next++] = mapped;
|
| 627 |
+
}
|
| 628 |
+
occ.resize(next);
|
| 629 |
+
}
|
| 630 |
+
for (auto& slot : st_.member)
|
| 631 |
+
if (slot != kNoInstance) slot = remap[static_cast<size_t>(slot)];
|
| 632 |
+
st_.tok.swap(tokens);
|
| 633 |
+
st_.root.swap(roots);
|
| 634 |
+
st_.alive.swap(alive);
|
| 635 |
+
std::fill(dead_.begin(), dead_.end(), 0);
|
| 636 |
+
++compactions_;
|
| 637 |
+
}
|
| 638 |
+
|
| 639 |
+
void change(uint64_t k, int64_t delta, StatMap& table, std::vector<uint64_t>& dirty) {
|
| 640 |
+
Stat& s = table[k];
|
| 641 |
+
s.raw += delta;
|
| 642 |
+
if (delta > 0) s.added += delta;
|
| 643 |
+
++s.version;
|
| 644 |
+
if (s.dirty_epoch != epoch_) {
|
| 645 |
+
s.dirty_epoch = epoch_;
|
| 646 |
+
dirty.push_back(k);
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
std::vector<InstanceId>& lefts(int32_t left) {
|
| 651 |
+
auto& occ = occurrence_[left];
|
| 652 |
+
// Lists are root-sorted by construction: atoms are sorted once, and every child list
|
| 653 |
+
// is appended in the root order of its single merge batch.
|
| 654 |
+
if (dead_[left] * 4 > static_cast<int64_t>(occ.size())) {
|
| 655 |
+
occ.erase(std::remove_if(occ.begin(), occ.end(), [&](InstanceId i) { return !st_.alive[i]; }),
|
| 656 |
+
occ.end());
|
| 657 |
+
dead_[left] = 0;
|
| 658 |
+
}
|
| 659 |
+
return occ;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
static double seconds_since(std::chrono::steady_clock::time_point t) {
|
| 663 |
+
return std::chrono::duration<double>(std::chrono::steady_clock::now() - t).count();
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
// Candidate (a, b) pairs are visited in increasing root order of a by scanning the
|
| 667 |
+
// shorter occurrence list; the partner is one cell lookup away in either direction.
|
| 668 |
+
const std::vector<InstanceId>& scanned(int32_t left, int32_t right, bool& forward) const {
|
| 669 |
+
const auto& lo = occurrence_[left];
|
| 670 |
+
const auto& ro = occurrence_[right];
|
| 671 |
+
forward = left == right || lo.size() <= ro.size();
|
| 672 |
+
return forward ? lo : ro;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
template <class F>
|
| 676 |
+
void pairs_in(int32_t left, int32_t right, int32_t off, size_t begin, size_t end,
|
| 677 |
+
F&& visit) const {
|
| 678 |
+
bool forward;
|
| 679 |
+
const auto& scan = scanned(left, right, forward);
|
| 680 |
+
for (size_t i = begin; i < end; ++i) {
|
| 681 |
+
const InstanceId x = scan[i];
|
| 682 |
+
if (!st_.alive[x]) continue;
|
| 683 |
+
if (forward) {
|
| 684 |
+
const InstanceId b = st_.right_of(x, right, off);
|
| 685 |
+
if (b != kNoInstance) visit(x, b);
|
| 686 |
+
} else {
|
| 687 |
+
const InstanceId a = st_.left_of(x, left, off);
|
| 688 |
+
if (a != kNoInstance) visit(a, x);
|
| 689 |
+
}
|
| 690 |
+
}
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
+
// Cuts of a root-sorted list into blocks of about ``grain`` entries at safe frames, so
|
| 694 |
+
// per-block replay (self-rule chains included) equals one sequential pass.
|
| 695 |
+
std::vector<size_t> blocks(const std::vector<InstanceId>& scan, size_t grain) const {
|
| 696 |
+
std::vector<size_t> cuts{0};
|
| 697 |
+
for (size_t next = grain; next < scan.size();) {
|
| 698 |
+
auto frame = std::upper_bound(safe_.begin(), safe_.end(), st_.root[scan[next]]);
|
| 699 |
+
if (frame == safe_.end()) break;
|
| 700 |
+
const size_t at = static_cast<size_t>(
|
| 701 |
+
std::lower_bound(scan.begin() + next, scan.end(), *frame,
|
| 702 |
+
[&](InstanceId i, int64_t f) { return st_.root[i] < f; }) -
|
| 703 |
+
scan.begin());
|
| 704 |
+
if (at >= scan.size()) break;
|
| 705 |
+
cuts.push_back(at);
|
| 706 |
+
next = at + grain;
|
| 707 |
+
}
|
| 708 |
+
cuts.push_back(scan.size());
|
| 709 |
+
return cuts;
|
| 710 |
+
}
|
| 711 |
+
|
| 712 |
+
// Candidate pairs in scan order, gathered by blocks in parallel.
|
| 713 |
+
std::vector<std::pair<InstanceId, InstanceId>> collect(int32_t left, int32_t right, int32_t off) const {
|
| 714 |
+
bool forward;
|
| 715 |
+
const std::vector<size_t> cuts = blocks(scanned(left, right, forward), 1 << 16);
|
| 716 |
+
std::vector<std::vector<std::pair<InstanceId, InstanceId>>> parts(cuts.size() - 1);
|
| 717 |
+
#ifdef _OPENMP
|
| 718 |
+
#pragma omp parallel for schedule(dynamic, 1) num_threads(threads_) if (parts.size() > 1)
|
| 719 |
+
#endif
|
| 720 |
+
for (size_t k = 0; k < parts.size(); ++k)
|
| 721 |
+
pairs_in(left, right, off, cuts[k], cuts[k + 1],
|
| 722 |
+
[&](InstanceId a, InstanceId b) { parts[k].emplace_back(a, b); });
|
| 723 |
+
std::vector<std::pair<InstanceId, InstanceId>> pairs;
|
| 724 |
+
for (auto& part : parts) pairs.insert(pairs.end(), part.begin(), part.end());
|
| 725 |
+
return pairs;
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
// Exact count of one block under the real merge's matching and validity tests.
|
| 729 |
+
int64_t count_exact(int32_t left, int32_t right, int32_t off, size_t begin, size_t end) const {
|
| 730 |
+
int64_t n = 0;
|
| 731 |
+
// Self rules chain: a may be the right half of a recent match (roots increase).
|
| 732 |
+
std::vector<InstanceId> recent;
|
| 733 |
+
size_t head = 0;
|
| 734 |
+
pairs_in(left, right, off, begin, end, [&](InstanceId a, InstanceId b) {
|
| 735 |
+
if (left == right) {
|
| 736 |
+
while (head < recent.size() && st_.root[recent[head]] < st_.root[a]) ++head;
|
| 737 |
+
if (std::find(recent.begin() + head, recent.end(), a) != recent.end()) return;
|
| 738 |
+
}
|
| 739 |
+
if (!st_.valid(a, b, off)) return;
|
| 740 |
+
if (left == right) recent.push_back(b);
|
| 741 |
+
++n;
|
| 742 |
+
});
|
| 743 |
+
return n;
|
| 744 |
+
}
|
| 745 |
+
|
| 746 |
+
// Certifies a batch of keys; every key is split into blocks so one large list does
|
| 747 |
+
// not serialize the batch. Read-only on shared state.
|
| 748 |
+
void certify(std::vector<Entry>& batch) {
|
| 749 |
+
struct Timer {
|
| 750 |
+
double& sink;
|
| 751 |
+
std::chrono::steady_clock::time_point t;
|
| 752 |
+
~Timer() { sink += seconds_since(t); }
|
| 753 |
+
} timer{dry_seconds_, std::chrono::steady_clock::now()};
|
| 754 |
+
dry_runs_ += static_cast<int64_t>(batch.size());
|
| 755 |
+
struct Task {
|
| 756 |
+
size_t key, begin, end;
|
| 757 |
+
};
|
| 758 |
+
std::vector<Task> tasks;
|
| 759 |
+
for (size_t k = 0; k < batch.size(); ++k) {
|
| 760 |
+
int32_t l, r, o;
|
| 761 |
+
unpack(batch[k].key, l, r, o);
|
| 762 |
+
lefts(l); // compaction mutates lists: do it before the parallel region
|
| 763 |
+
lefts(r);
|
| 764 |
+
bool forward;
|
| 765 |
+
const std::vector<size_t> cuts = blocks(scanned(l, r, forward), 1 << 15);
|
| 766 |
+
for (size_t c = 0; c + 1 < cuts.size(); ++c) tasks.push_back({k, cuts[c], cuts[c + 1]});
|
| 767 |
+
}
|
| 768 |
+
std::vector<int64_t> partial(tasks.size());
|
| 769 |
+
#ifdef _OPENMP
|
| 770 |
+
#pragma omp parallel for schedule(dynamic, 1) num_threads(threads_)
|
| 771 |
+
#endif
|
| 772 |
+
for (size_t t = 0; t < tasks.size(); ++t) {
|
| 773 |
+
int32_t l, r, o;
|
| 774 |
+
unpack(batch[tasks[t].key].key, l, r, o);
|
| 775 |
+
partial[t] = count_exact(l, r, o, tasks[t].begin, tasks[t].end);
|
| 776 |
+
}
|
| 777 |
+
std::vector<int64_t> exact(batch.size(), 0);
|
| 778 |
+
for (size_t t = 0; t < tasks.size(); ++t) exact[tasks[t].key] += partial[t];
|
| 779 |
+
for (size_t k = 0; k < batch.size(); ++k) {
|
| 780 |
+
Stat& s = stats_[batch[k].key];
|
| 781 |
+
s.exact = exact[k];
|
| 782 |
+
s.exact_version = s.version;
|
| 783 |
+
s.added_at_exact = s.added;
|
| 784 |
+
if (s.exact >= min_count_) queue(batch[k].key, s, s.exact);
|
| 785 |
+
}
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
// Pairs are applied in root order; ranges split at safe frames are independent, so
|
| 789 |
+
// each is replayed sequentially by one thread with exactly the serial outcome.
|
| 790 |
+
void apply(int32_t left, int32_t right, int32_t off, int32_t child) {
|
| 791 |
+
struct Timer {
|
| 792 |
+
double& sink;
|
| 793 |
+
std::chrono::steady_clock::time_point t;
|
| 794 |
+
~Timer() { sink += seconds_since(t); }
|
| 795 |
+
} timer{apply_seconds_, std::chrono::steady_clock::now()};
|
| 796 |
+
lefts(left);
|
| 797 |
+
lefts(right);
|
| 798 |
+
// Candidates are collected first: applying merges kills instances in both lists.
|
| 799 |
+
const auto collect_start = std::chrono::steady_clock::now();
|
| 800 |
+
std::vector<std::pair<InstanceId, InstanceId>> pairs = collect(left, right, off);
|
| 801 |
+
if (st_.tok.size() + pairs.size() > static_cast<size_t>(max_instances_)) {
|
| 802 |
+
pairs.clear();
|
| 803 |
+
pairs.shrink_to_fit();
|
| 804 |
+
compact_instances();
|
| 805 |
+
pairs = collect(left, right, off);
|
| 806 |
+
if (st_.tok.size() + pairs.size() > static_cast<size_t>(max_instances_))
|
| 807 |
+
throw std::length_error("live instance capacity exceeded after compaction");
|
| 808 |
+
}
|
| 809 |
+
collect_seconds_ += seconds_since(collect_start);
|
| 810 |
+
const std::vector<size_t> cuts = split(pairs);
|
| 811 |
+
const size_t groups = cuts.size() - 1;
|
| 812 |
+
const InstanceId base = st_.reserve(pairs.size());
|
| 813 |
+
if (pool_.size() < groups) pool_.resize(groups, std::vector<DeltaMap>(kShards));
|
| 814 |
+
dirty_shards_.resize(kShards);
|
| 815 |
+
std::vector<std::vector<InstanceId>> born(groups);
|
| 816 |
+
std::vector<int64_t> applied(groups, 0), rejected(groups, 0);
|
| 817 |
+
const auto loop_start = std::chrono::steady_clock::now();
|
| 818 |
+
#ifdef _OPENMP
|
| 819 |
+
#pragma omp parallel for schedule(dynamic, 1) num_threads(std::min<size_t>(threads_, groups))
|
| 820 |
+
#endif
|
| 821 |
+
for (size_t g = 0; g < groups; ++g) {
|
| 822 |
+
auto& delta = pool_[g];
|
| 823 |
+
auto add = [&](uint64_t key, int64_t d) { delta[shard_of(key)].add(key, d); };
|
| 824 |
+
for (size_t k = cuts[g]; k < cuts[g + 1]; ++k) {
|
| 825 |
+
const InstanceId a = pairs[k].first, b = pairs[k].second;
|
| 826 |
+
if (!st_.alive[a] || !st_.alive[b]) continue;
|
| 827 |
+
if (!st_.valid(a, b, off)) {
|
| 828 |
+
++rejected[g];
|
| 829 |
+
continue;
|
| 830 |
+
}
|
| 831 |
+
st_.partners(a, allow_zero_, [&](InstanceId x, InstanceId y, int32_t o) {
|
| 832 |
+
add(rule_key(st_.tok[x], st_.tok[y], o), -1);
|
| 833 |
+
});
|
| 834 |
+
st_.partners(b, allow_zero_, [&](InstanceId x, InstanceId y, int32_t o) {
|
| 835 |
+
if (x != a && y != a) add(rule_key(st_.tok[x], st_.tok[y], o), -1);
|
| 836 |
+
});
|
| 837 |
+
const int64_t r = st_.root[a];
|
| 838 |
+
st_.kill(a);
|
| 839 |
+
st_.kill(b);
|
| 840 |
+
const InstanceId m = base + static_cast<InstanceId>(k);
|
| 841 |
+
st_.tok[m] = child;
|
| 842 |
+
st_.root[m] = r;
|
| 843 |
+
st_.place(m);
|
| 844 |
+
born[g].push_back(m);
|
| 845 |
+
st_.partners(m, allow_zero_, [&](InstanceId x, InstanceId y, int32_t o) {
|
| 846 |
+
add(rule_key(st_.tok[x], st_.tok[y], o), +1);
|
| 847 |
+
});
|
| 848 |
+
++applied[g];
|
| 849 |
+
}
|
| 850 |
+
}
|
| 851 |
+
(groups > 1 ? parallel_seconds_ : serial_seconds_) += seconds_since(loop_start);
|
| 852 |
+
const auto merge_start = std::chrono::steady_clock::now();
|
| 853 |
+
std::vector<int64_t> changes(kShards, 0);
|
| 854 |
+
#ifdef _OPENMP
|
| 855 |
+
#pragma omp parallel for schedule(dynamic, 1) num_threads(threads_) if (groups > 1)
|
| 856 |
+
#endif
|
| 857 |
+
for (size_t p = 0; p < kShards; ++p) {
|
| 858 |
+
StatMap& table = stats_.shard(p);
|
| 859 |
+
for (size_t g = 0; g < groups; ++g) {
|
| 860 |
+
pool_[g][p].each([&](uint64_t k, int64_t d) {
|
| 861 |
+
change(k, d, table, dirty_shards_[p]);
|
| 862 |
+
++changes[p];
|
| 863 |
+
});
|
| 864 |
+
pool_[g][p].clear();
|
| 865 |
+
}
|
| 866 |
+
}
|
| 867 |
+
for (size_t p = 0; p < kShards; ++p) {
|
| 868 |
+
dirty_.insert(dirty_.end(), dirty_shards_[p].begin(), dirty_shards_[p].end());
|
| 869 |
+
dirty_shards_[p].clear();
|
| 870 |
+
changes_ += changes[p];
|
| 871 |
+
}
|
| 872 |
+
for (size_t g = 0; g < groups; ++g) {
|
| 873 |
+
auto& occ = occurrence_[child];
|
| 874 |
+
occ.insert(occ.end(), born[g].begin(), born[g].end());
|
| 875 |
+
dead_[left] += applied[g];
|
| 876 |
+
dead_[right] += applied[g];
|
| 877 |
+
applied_ += applied[g];
|
| 878 |
+
rejected_ += rejected[g];
|
| 879 |
+
live_ -= applied[g];
|
| 880 |
+
}
|
| 881 |
+
merge_seconds_ += seconds_since(merge_start);
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
// Cut root-ordered pairs into contiguous groups at safe frames.
|
| 885 |
+
std::vector<size_t> split(const std::vector<std::pair<InstanceId, InstanceId>>& pairs) const {
|
| 886 |
+
std::vector<size_t> cuts{0};
|
| 887 |
+
const size_t n = pairs.size();
|
| 888 |
+
const size_t groups = std::max<size_t>(1, std::min<size_t>(threads_, n / kApplyGrain));
|
| 889 |
+
for (size_t g = 1; g < groups; ++g) {
|
| 890 |
+
const size_t target = std::max(cuts.back(), n * g / groups);
|
| 891 |
+
if (target >= n) break;
|
| 892 |
+
auto frame = std::upper_bound(safe_.begin(), safe_.end(), st_.root[pairs[target].first]);
|
| 893 |
+
if (frame == safe_.end()) break;
|
| 894 |
+
const size_t cut = static_cast<size_t>(
|
| 895 |
+
std::lower_bound(pairs.begin() + target, pairs.end(), *frame,
|
| 896 |
+
[&](const std::pair<InstanceId, InstanceId>& p, int64_t f) {
|
| 897 |
+
return st_.root[p.first] < f;
|
| 898 |
+
}) -
|
| 899 |
+
pairs.begin());
|
| 900 |
+
if (cut > cuts.back() && cut < n) cuts.push_back(cut);
|
| 901 |
+
}
|
| 902 |
+
cuts.push_back(n);
|
| 903 |
+
return cuts;
|
| 904 |
+
}
|
| 905 |
+
|
| 906 |
+
static bool certified(const Stat& s) { return s.exact >= 0 && s.exact_version == s.version; }
|
| 907 |
+
|
| 908 |
+
static int64_t bound(const Stat& s) {
|
| 909 |
+
if (certified(s)) return s.exact;
|
| 910 |
+
return s.exact >= 0 ? std::min(s.raw, s.exact + s.added - s.added_at_exact) : s.raw;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
void queue(uint64_t key, Stat& s, int64_t count) {
|
| 914 |
+
heap_.push({count, key, s.version});
|
| 915 |
+
s.live = true;
|
| 916 |
+
s.queued_version = s.version;
|
| 917 |
+
s.queued_count = count;
|
| 918 |
+
}
|
| 919 |
+
|
| 920 |
+
bool take(Entry& e, Stat*& s) {
|
| 921 |
+
e = heap_.top();
|
| 922 |
+
heap_.pop();
|
| 923 |
+
s = stats_.find(e.key);
|
| 924 |
+
if (!s || s->retired || !s->live || e.version != s->queued_version) return false;
|
| 925 |
+
s->live = false;
|
| 926 |
+
if (e.version == s->version) return true;
|
| 927 |
+
const int64_t b = bound(*s);
|
| 928 |
+
if (b > 0) queue(e.key, *s, b);
|
| 929 |
+
return false;
|
| 930 |
+
}
|
| 931 |
+
|
| 932 |
+
bool current_certified(const Entry& e) {
|
| 933 |
+
const Stat* s = stats_.find(e.key);
|
| 934 |
+
return s && !s->retired && s->live && e.version == s->queued_version &&
|
| 935 |
+
e.version == s->version && certified(*s);
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
void run(bool progress) {
|
| 939 |
+
live_ = initial_instances_;
|
| 940 |
+
while (tables_.size() < vocab_size_ && !heap_.empty()) {
|
| 941 |
+
Entry top;
|
| 942 |
+
Stat* found;
|
| 943 |
+
if (!take(top, found)) continue;
|
| 944 |
+
Stat& s = *found;
|
| 945 |
+
if (!certified(s)) {
|
| 946 |
+
std::vector<Entry> batch{top};
|
| 947 |
+
while (!heap_.empty() && batch.size() < kCertifyBatch) {
|
| 948 |
+
if (current_certified(heap_.top())) break;
|
| 949 |
+
Entry e;
|
| 950 |
+
Stat* next;
|
| 951 |
+
if (take(e, next)) batch.push_back(e);
|
| 952 |
+
}
|
| 953 |
+
certify(batch);
|
| 954 |
+
continue;
|
| 955 |
+
}
|
| 956 |
+
if (top.count != s.exact) continue;
|
| 957 |
+
if (s.exact < min_count_) continue;
|
| 958 |
+
int32_t l, r, o;
|
| 959 |
+
unpack(top.key, l, r, o);
|
| 960 |
+
std::vector<uint32_t> m;
|
| 961 |
+
if (!tables_.compose(l, r, o, m)) {
|
| 962 |
+
s.exact = 0;
|
| 963 |
+
continue;
|
| 964 |
+
}
|
| 965 |
+
const int32_t child = tables_.add_merge(l, r, o);
|
| 966 |
+
merges_.emplace_back(l, r, o);
|
| 967 |
+
s.retired = true;
|
| 968 |
+
dirty_.clear();
|
| 969 |
+
++epoch_;
|
| 970 |
+
apply(l, r, o, child);
|
| 971 |
+
// Lazy decrease-key: only a grown bound needs a new entry now.
|
| 972 |
+
for (uint64_t k : dirty_) {
|
| 973 |
+
Stat& d = stats_[k];
|
| 974 |
+
if (d.retired) continue;
|
| 975 |
+
const int64_t b = bound(d);
|
| 976 |
+
if (b > 0 && (!d.live || b > d.queued_count)) queue(k, d, b);
|
| 977 |
+
}
|
| 978 |
+
dirty_.clear();
|
| 979 |
+
if (progress && merges_.size() % 256 == 0) {
|
| 980 |
+
py::gil_scoped_acquire acquire;
|
| 981 |
+
py::print("sparse temporal merges", merges_.size(), "live", live_, py::arg("flush") = true);
|
| 982 |
+
}
|
| 983 |
+
}
|
| 984 |
+
}
|
| 985 |
+
|
| 986 |
+
Tables tables_;
|
| 987 |
+
Instances st_;
|
| 988 |
+
int64_t vocab_size_;
|
| 989 |
+
bool allow_zero_;
|
| 990 |
+
bool dense_census_;
|
| 991 |
+
int64_t min_count_;
|
| 992 |
+
int64_t max_instances_;
|
| 993 |
+
std::vector<std::vector<InstanceId>> occurrence_;
|
| 994 |
+
ShardedStats stats_;
|
| 995 |
+
std::vector<std::vector<DeltaMap>> pool_; // [apply group][shard]
|
| 996 |
+
std::vector<std::vector<uint64_t>> dirty_shards_; // [shard]
|
| 997 |
+
std::priority_queue<Entry> heap_;
|
| 998 |
+
std::vector<uint64_t> dirty_;
|
| 999 |
+
std::vector<std::tuple<int32_t, int32_t, int32_t>> merges_;
|
| 1000 |
+
std::vector<int64_t> dead_;
|
| 1001 |
+
std::vector<int64_t> safe_;
|
| 1002 |
+
uint32_t epoch_ = 0;
|
| 1003 |
+
int threads_ = 1;
|
| 1004 |
+
int64_t initial_instances_ = 0, live_ = 0, dry_runs_ = 0, rejected_ = 0, applied_ = 0;
|
| 1005 |
+
int64_t compactions_ = 0;
|
| 1006 |
+
size_t initial_types_ = 0;
|
| 1007 |
+
int64_t changes_ = 0;
|
| 1008 |
+
double census_seconds_ = 0, dry_seconds_ = 0, apply_seconds_ = 0, collect_seconds_ = 0,
|
| 1009 |
+
parallel_seconds_ = 0, serial_seconds_ = 0, merge_seconds_ = 0;
|
| 1010 |
+
};
|
| 1011 |
+
|
| 1012 |
+
struct Spatial {
|
| 1013 |
+
std::vector<int64_t> offsets;
|
| 1014 |
+
std::vector<uint32_t> support;
|
| 1015 |
+
std::vector<std::vector<int32_t>> expansion; // token -> value per sparse slot (-1 absent)
|
| 1016 |
+
std::unordered_map<uint64_t, std::pair<int32_t, int32_t>> rules; // (lo, hi) -> (child, rank)
|
| 1017 |
+
int32_t dims = 0;
|
| 1018 |
+
|
| 1019 |
+
static uint64_t pair_key(int32_t a, int32_t b) {
|
| 1020 |
+
if (a > b) std::swap(a, b);
|
| 1021 |
+
return (static_cast<uint64_t>(a) << 32) | static_cast<uint32_t>(b);
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
void init(const std::vector<int64_t>& bins, const std::vector<std::pair<int32_t, int32_t>>& merges) {
|
| 1025 |
+
dims = static_cast<int32_t>(bins.size());
|
| 1026 |
+
offsets.assign(1, 0);
|
| 1027 |
+
for (int64_t b : bins) {
|
| 1028 |
+
if (b < 1) throw std::invalid_argument("every sparse slot needs at least one value");
|
| 1029 |
+
offsets.push_back(offsets.back() + b);
|
| 1030 |
+
}
|
| 1031 |
+
for (int32_t d = 0; d < dims; ++d)
|
| 1032 |
+
for (int64_t v = 0; v < bins[d]; ++v) {
|
| 1033 |
+
support.push_back(1u << d);
|
| 1034 |
+
std::vector<int32_t> e(dims, -1);
|
| 1035 |
+
e[d] = static_cast<int32_t>(v);
|
| 1036 |
+
expansion.push_back(std::move(e));
|
| 1037 |
+
}
|
| 1038 |
+
const int32_t atoms = static_cast<int32_t>(support.size());
|
| 1039 |
+
for (size_t r = 0; r < merges.size(); ++r) {
|
| 1040 |
+
const auto [a, b] = merges[r];
|
| 1041 |
+
const int32_t child = atoms + static_cast<int32_t>(r);
|
| 1042 |
+
if (a < 0 || b < 0 || a >= child || b >= child || (support[a] & support[b]))
|
| 1043 |
+
throw std::invalid_argument("invalid spatial merge");
|
| 1044 |
+
support.push_back(support[a] | support[b]);
|
| 1045 |
+
std::vector<int32_t> e(dims);
|
| 1046 |
+
for (int32_t d = 0; d < dims; ++d) e[d] = std::max(expansion[a][d], expansion[b][d]);
|
| 1047 |
+
expansion.push_back(std::move(e));
|
| 1048 |
+
rules.emplace(pair_key(a, b), std::make_pair(child, static_cast<int32_t>(r)));
|
| 1049 |
+
}
|
| 1050 |
+
}
|
| 1051 |
+
|
| 1052 |
+
std::vector<int32_t> encode(const int32_t* row) const {
|
| 1053 |
+
std::set<int32_t> tokens;
|
| 1054 |
+
for (int32_t d = 0; d < dims; ++d)
|
| 1055 |
+
if (row[d] >= 0) tokens.insert(static_cast<int32_t>(offsets[d] + row[d]));
|
| 1056 |
+
using Item = std::tuple<int32_t, int32_t, int32_t, int32_t>;
|
| 1057 |
+
std::priority_queue<Item, std::vector<Item>, std::greater<Item>> heap;
|
| 1058 |
+
auto offer = [&](int32_t a, int32_t b) {
|
| 1059 |
+
auto it = rules.find(pair_key(a, b));
|
| 1060 |
+
if (it != rules.end())
|
| 1061 |
+
heap.emplace(it->second.second, it->second.first, std::min(a, b), std::max(a, b));
|
| 1062 |
+
};
|
| 1063 |
+
for (auto i = tokens.begin(); i != tokens.end(); ++i)
|
| 1064 |
+
for (auto j = std::next(i); j != tokens.end(); ++j) offer(*i, *j);
|
| 1065 |
+
while (!heap.empty()) {
|
| 1066 |
+
const auto [rank, child, left, right] = heap.top();
|
| 1067 |
+
heap.pop();
|
| 1068 |
+
if (!tokens.count(left) || !tokens.count(right)) continue;
|
| 1069 |
+
tokens.erase(left);
|
| 1070 |
+
tokens.erase(right);
|
| 1071 |
+
for (int32_t other : tokens) offer(child, other);
|
| 1072 |
+
tokens.insert(child);
|
| 1073 |
+
}
|
| 1074 |
+
return {tokens.begin(), tokens.end()};
|
| 1075 |
+
}
|
| 1076 |
+
};
|
| 1077 |
+
|
| 1078 |
+
// Runtime codec over dense primitive cells ``(T, D)`` with ``-1`` for inactive slots.
|
| 1079 |
+
class SparseCodec {
|
| 1080 |
+
public:
|
| 1081 |
+
SparseCodec(std::vector<int64_t> bins, std::vector<int64_t> zero,
|
| 1082 |
+
std::vector<std::pair<int32_t, int32_t>> spatial_merges, int32_t lookahead,
|
| 1083 |
+
std::vector<std::tuple<int32_t, int32_t, int32_t>> temporal_merges)
|
| 1084 |
+
: bins_(std::move(bins)), zero_(std::move(zero)) {
|
| 1085 |
+
if (bins_.empty() || bins_.size() != zero_.size())
|
| 1086 |
+
throw std::invalid_argument("bins and zero must have one entry per slot");
|
| 1087 |
+
dims_ = static_cast<int32_t>(bins_.size());
|
| 1088 |
+
std::vector<int64_t> sparse;
|
| 1089 |
+
for (int32_t d = 0; d < dims_; ++d) {
|
| 1090 |
+
// -2 retains every dense value; -1 change-codes an absolute slot.
|
| 1091 |
+
if (zero_[d] >= bins_[d] || zero_[d] < -2) throw std::invalid_argument("zero index");
|
| 1092 |
+
sparse.push_back(bins_[d] - (zero_[d] >= 0));
|
| 1093 |
+
}
|
| 1094 |
+
sparse.push_back(1); // E
|
| 1095 |
+
spatial_.init(sparse, spatial_merges);
|
| 1096 |
+
tables_.init(spatial_.support, dims_ + 1, lookahead);
|
| 1097 |
+
for (size_t r = 0; r < temporal_merges.size(); ++r) {
|
| 1098 |
+
auto [a, b, o] = temporal_merges[r];
|
| 1099 |
+
if (a < 0 || b < 0 || a >= tables_.size() || b >= tables_.size())
|
| 1100 |
+
throw std::invalid_argument("merge order");
|
| 1101 |
+
tables_.add_merge(a, b, o);
|
| 1102 |
+
if (!rank_.emplace(rule_key(a, b, o), static_cast<int32_t>(r)).second)
|
| 1103 |
+
throw std::invalid_argument("duplicate temporal rule");
|
| 1104 |
+
}
|
| 1105 |
+
}
|
| 1106 |
+
|
| 1107 |
+
int32_t vocab_size() const { return tables_.size(); }
|
| 1108 |
+
int32_t spatial_vocab_size() const { return static_cast<int32_t>(spatial_.support.size()); }
|
| 1109 |
+
|
| 1110 |
+
std::vector<std::vector<int32_t>> encode_batch(
|
| 1111 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& cells,
|
| 1112 |
+
int threads) const {
|
| 1113 |
+
if (cells.ndim() != 3 || cells.shape(2) != dims_ || cells.shape(0) < 1 || cells.shape(1) < 1)
|
| 1114 |
+
throw std::invalid_argument("cells must have shape (B, T, D) with B, T >= 1");
|
| 1115 |
+
const int64_t batch = cells.shape(0), steps = cells.shape(1);
|
| 1116 |
+
const int32_t* data = cells.data();
|
| 1117 |
+
std::vector<std::vector<int32_t>> out(batch);
|
| 1118 |
+
{
|
| 1119 |
+
py::gil_scoped_release release;
|
| 1120 |
+
ac2::parallel_for(batch, threads, [&](int64_t b) {
|
| 1121 |
+
out[b] = encode_one(data + b * steps * dims_, steps);
|
| 1122 |
+
});
|
| 1123 |
+
}
|
| 1124 |
+
return out;
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
py::array_t<int32_t> decode_batch(const std::vector<std::vector<int32_t>>& rows,
|
| 1128 |
+
int64_t horizon, const std::vector<bool>& active) const {
|
| 1129 |
+
if (horizon < 1) throw std::invalid_argument("horizon must be positive");
|
| 1130 |
+
if (static_cast<int32_t>(active.size()) != dims_)
|
| 1131 |
+
throw std::invalid_argument("active must have one flag per slot");
|
| 1132 |
+
py::array_t<int32_t> out({static_cast<int64_t>(rows.size()), horizon, int64_t{dims_}});
|
| 1133 |
+
int32_t* data = out.mutable_data();
|
| 1134 |
+
for (size_t k = 0; k < rows.size(); ++k) decode_one(rows[k], horizon, active, data + k * horizon * dims_);
|
| 1135 |
+
return out;
|
| 1136 |
+
}
|
| 1137 |
+
|
| 1138 |
+
// Spatial tokens of sparse frames ``(N, D + 1)``: per-frame counts and flat tokens.
|
| 1139 |
+
py::tuple spatial_encode(
|
| 1140 |
+
const py::array_t<int32_t, py::array::c_style | py::array::forcecast>& frames) const {
|
| 1141 |
+
if (frames.ndim() != 2 || frames.shape(1) != dims_ + 1)
|
| 1142 |
+
throw std::invalid_argument("frames must have shape (N, D + 1)");
|
| 1143 |
+
const int64_t n = frames.shape(0);
|
| 1144 |
+
py::array_t<int64_t> counts(n);
|
| 1145 |
+
std::vector<int32_t> flat;
|
| 1146 |
+
for (int64_t i = 0; i < n; ++i) {
|
| 1147 |
+
const int32_t* row = frames.data() + i * (dims_ + 1);
|
| 1148 |
+
for (int32_t d = 0; d <= dims_; ++d)
|
| 1149 |
+
if (row[d] >= static_cast<int32_t>(spatial_.offsets[d + 1] - spatial_.offsets[d]))
|
| 1150 |
+
throw std::invalid_argument("sparse value out of range");
|
| 1151 |
+
const auto tokens = spatial_.encode(row);
|
| 1152 |
+
counts.mutable_data()[i] = static_cast<int64_t>(tokens.size());
|
| 1153 |
+
flat.insert(flat.end(), tokens.begin(), tokens.end());
|
| 1154 |
+
}
|
| 1155 |
+
return py::make_tuple(counts, py::array_t<int32_t>(flat.size(), flat.data()));
|
| 1156 |
+
}
|
| 1157 |
+
|
| 1158 |
+
std::vector<std::vector<std::pair<int32_t, int32_t>>> patterns() const { return tables_.pattern; }
|
| 1159 |
+
|
| 1160 |
+
private:
|
| 1161 |
+
// Dense chunk -> sparse frames (T, D + 1); validates slot activity and ranges.
|
| 1162 |
+
std::vector<int32_t> sparsify(const int32_t* cells, int64_t steps) const {
|
| 1163 |
+
const int32_t width = dims_ + 1;
|
| 1164 |
+
std::vector<int32_t> sparse(static_cast<size_t>(steps) * width, -1);
|
| 1165 |
+
int64_t last = -1;
|
| 1166 |
+
for (int32_t d = 0; d < dims_; ++d) {
|
| 1167 |
+
const bool active = cells[d] >= 0;
|
| 1168 |
+
for (int64_t t = 0; t < steps; ++t) {
|
| 1169 |
+
const int32_t v = cells[t * dims_ + d];
|
| 1170 |
+
if ((v >= 0) != active)
|
| 1171 |
+
throw std::invalid_argument("a slot must be active for the whole chunk or not at all");
|
| 1172 |
+
if (v >= bins_[d]) throw std::invalid_argument("cell outside the primitive range");
|
| 1173 |
+
if (!active) continue;
|
| 1174 |
+
int32_t s = -1;
|
| 1175 |
+
if (zero_[d] == -2) {
|
| 1176 |
+
s = v;
|
| 1177 |
+
} else if (zero_[d] < 0) {
|
| 1178 |
+
if (t == 0 || v != cells[(t - 1) * dims_ + d]) s = v;
|
| 1179 |
+
} else if (v != zero_[d]) {
|
| 1180 |
+
s = v - (v > zero_[d]);
|
| 1181 |
+
}
|
| 1182 |
+
sparse[t * width + d] = s;
|
| 1183 |
+
if (s >= 0) last = std::max(last, t);
|
| 1184 |
+
}
|
| 1185 |
+
}
|
| 1186 |
+
for (int64_t t = 0; t < last; ++t) sparse[t * width + dims_] = 0;
|
| 1187 |
+
return sparse;
|
| 1188 |
+
}
|
| 1189 |
+
|
| 1190 |
+
std::vector<int32_t> encode_one(const int32_t* cells, int64_t steps) const {
|
| 1191 |
+
const std::vector<int32_t> sparse = sparsify(cells, steps);
|
| 1192 |
+
Instances s;
|
| 1193 |
+
s.reset(&tables_, steps + tables_.lookahead);
|
| 1194 |
+
for (int64_t t = 0; t < steps; ++t)
|
| 1195 |
+
for (int32_t token : spatial_.encode(sparse.data() + t * (dims_ + 1))) s.add(token, t);
|
| 1196 |
+
using Cand = std::tuple<int32_t, int64_t, int32_t, int64_t, InstanceId, InstanceId, int32_t>;
|
| 1197 |
+
std::priority_queue<Cand, std::vector<Cand>, std::greater<Cand>> heap;
|
| 1198 |
+
auto offer = [&](InstanceId i) {
|
| 1199 |
+
s.partners(i, true, [&](InstanceId a, InstanceId b, int32_t off) {
|
| 1200 |
+
auto it = rank_.find(rule_key(s.tok[a], s.tok[b], off));
|
| 1201 |
+
if (it == rank_.end()) return;
|
| 1202 |
+
heap.emplace(it->second, s.root[a], tables_.rdim[s.tok[a]], s.root[b], a, b, off);
|
| 1203 |
+
});
|
| 1204 |
+
};
|
| 1205 |
+
for (size_t i = 0; i < s.tok.size(); ++i) offer(static_cast<InstanceId>(i));
|
| 1206 |
+
while (!heap.empty()) {
|
| 1207 |
+
auto [r, ra, da, rb, a, b, off] = heap.top();
|
| 1208 |
+
heap.pop();
|
| 1209 |
+
if (!s.alive[a] || !s.alive[b] || !s.valid(a, b, off)) continue;
|
| 1210 |
+
const int64_t root = s.root[a];
|
| 1211 |
+
s.kill(a);
|
| 1212 |
+
s.kill(b);
|
| 1213 |
+
offer(s.add(tables_.base + r, root));
|
| 1214 |
+
}
|
| 1215 |
+
return s.serialize();
|
| 1216 |
+
}
|
| 1217 |
+
|
| 1218 |
+
void decode_one(const std::vector<int32_t>& row, int64_t horizon,
|
| 1219 |
+
const std::vector<bool>& active, int32_t* cells) const {
|
| 1220 |
+
const int32_t L = tables_.lookahead, width = dims_ + 1;
|
| 1221 |
+
uint32_t allowed = 1u << dims_;
|
| 1222 |
+
for (int32_t d = 0; d < dims_; ++d)
|
| 1223 |
+
if (active[d]) allowed |= 1u << d;
|
| 1224 |
+
std::vector<uint32_t> cover(horizon + L, 0);
|
| 1225 |
+
std::vector<int32_t> sparse(static_cast<size_t>(horizon) * width, -1);
|
| 1226 |
+
int64_t front = 0;
|
| 1227 |
+
std::vector<uint32_t> own(L, 0);
|
| 1228 |
+
std::vector<int32_t> frames;
|
| 1229 |
+
for (int32_t token : row) {
|
| 1230 |
+
if (token < 0 || token >= tables_.size()) throw std::invalid_argument("token outside vocab");
|
| 1231 |
+
if (front >= horizon) throw std::invalid_argument("token after the last open frame");
|
| 1232 |
+
frames.clear();
|
| 1233 |
+
for (auto [o, t] : tables_.pattern[token]) {
|
| 1234 |
+
const int64_t u = front + o;
|
| 1235 |
+
const uint32_t m = spatial_.support[t];
|
| 1236 |
+
if (u >= horizon) throw std::invalid_argument("token exceeds horizon");
|
| 1237 |
+
if (m & ~allowed) throw std::invalid_argument("token touches an inactive slot");
|
| 1238 |
+
if (m & own[o]) throw std::invalid_argument("overlapping token supports");
|
| 1239 |
+
if ((m >> dims_ & 1u) && u == horizon - 1)
|
| 1240 |
+
throw std::invalid_argument("end-of-frame atom on the last frame");
|
| 1241 |
+
if (!own[o]) frames.push_back(o);
|
| 1242 |
+
own[o] |= m;
|
| 1243 |
+
for (int32_t d = 0; d < width; ++d)
|
| 1244 |
+
if (spatial_.expansion[t][d] >= 0) sparse[u * width + d] = spatial_.expansion[t][d];
|
| 1245 |
+
}
|
| 1246 |
+
for (int32_t o : frames) {
|
| 1247 |
+
const int64_t u = front + o;
|
| 1248 |
+
if (cover[u] & own[o]) throw std::invalid_argument("overlapping token supports");
|
| 1249 |
+
if (cover[u] >> dims_ & 1u) throw std::invalid_argument("token touches a closed frame");
|
| 1250 |
+
cover[u] |= own[o];
|
| 1251 |
+
own[o] = 0;
|
| 1252 |
+
}
|
| 1253 |
+
while (front < horizon && (cover[front] >> dims_ & 1u)) ++front;
|
| 1254 |
+
}
|
| 1255 |
+
for (int32_t d = 0; d < dims_; ++d) {
|
| 1256 |
+
int32_t carried = -1;
|
| 1257 |
+
for (int64_t t = 0; t < horizon; ++t) {
|
| 1258 |
+
const int32_t v = sparse[t * width + d];
|
| 1259 |
+
int32_t& out = cells[t * dims_ + d];
|
| 1260 |
+
if (!active[d]) {
|
| 1261 |
+
out = -1;
|
| 1262 |
+
} else if (zero_[d] == -2) {
|
| 1263 |
+
if (v < 0) throw std::invalid_argument("dense active slot is missing");
|
| 1264 |
+
out = v;
|
| 1265 |
+
} else if (zero_[d] < 0) {
|
| 1266 |
+
if (v >= 0) carried = v;
|
| 1267 |
+
if (carried < 0) throw std::invalid_argument("an absolute slot must be stated on frame 0");
|
| 1268 |
+
out = carried;
|
| 1269 |
+
} else {
|
| 1270 |
+
out = v < 0 ? static_cast<int32_t>(zero_[d]) : v + (v >= zero_[d]);
|
| 1271 |
+
}
|
| 1272 |
+
}
|
| 1273 |
+
}
|
| 1274 |
+
}
|
| 1275 |
+
|
| 1276 |
+
std::vector<int64_t> bins_, zero_;
|
| 1277 |
+
int32_t dims_ = 0;
|
| 1278 |
+
Spatial spatial_;
|
| 1279 |
+
Tables tables_;
|
| 1280 |
+
std::unordered_map<uint64_t, int32_t> rank_;
|
| 1281 |
+
};
|
| 1282 |
+
|
| 1283 |
+
} // namespace
|
| 1284 |
+
|
| 1285 |
+
void register_sparse(py::module_& m) {
|
| 1286 |
+
py::class_<Trainer>(m, "SparseTrainer")
|
| 1287 |
+
.def(py::init<std::vector<uint32_t>, int32_t, int32_t, int64_t, bool, int64_t, int, bool, int64_t>(),
|
| 1288 |
+
py::arg("spatial_support"), py::arg("dims"), py::arg("lookahead"),
|
| 1289 |
+
py::arg("vocab_size"), py::arg("allow_zero_offset"), py::arg("min_count") = 2,
|
| 1290 |
+
py::arg("threads") = 0, py::arg("dense_census") = false,
|
| 1291 |
+
py::arg("max_instances") = UINT32_MAX)
|
| 1292 |
+
.def("set_instances", &Trainer::set_instances)
|
| 1293 |
+
.def("train", &Trainer::train, py::arg("progress") = false)
|
| 1294 |
+
.def("merges", &Trainer::merges);
|
| 1295 |
+
py::class_<SparseCodec>(m, "SparseCodec")
|
| 1296 |
+
.def(py::init<std::vector<int64_t>, std::vector<int64_t>,
|
| 1297 |
+
std::vector<std::pair<int32_t, int32_t>>, int32_t,
|
| 1298 |
+
std::vector<std::tuple<int32_t, int32_t, int32_t>>>(),
|
| 1299 |
+
py::arg("bins"), py::arg("zero"), py::arg("spatial_merges"), py::arg("lookahead"),
|
| 1300 |
+
py::arg("temporal_merges"))
|
| 1301 |
+
.def_property_readonly("vocab_size", &SparseCodec::vocab_size)
|
| 1302 |
+
.def_property_readonly("spatial_vocab_size", &SparseCodec::spatial_vocab_size)
|
| 1303 |
+
.def("encode_batch", &SparseCodec::encode_batch, py::arg("cells"), py::arg("threads") = 0)
|
| 1304 |
+
.def("decode_batch", &SparseCodec::decode_batch, py::arg("rows"), py::arg("horizon"),
|
| 1305 |
+
py::arg("active"))
|
| 1306 |
+
.def("spatial_encode", &SparseCodec::spatial_encode, py::arg("frames"))
|
| 1307 |
+
.def("patterns", &SparseCodec::patterns);
|
| 1308 |
+
}
|
actioncodec2_kernels/spatial.cpp
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
#include <pybind11/pybind11.h>
|
| 3 |
+
#include <pybind11/numpy.h>
|
| 4 |
+
#include <pybind11/stl.h>
|
| 5 |
+
|
| 6 |
+
#include <algorithm>
|
| 7 |
+
#include <cstdint>
|
| 8 |
+
#include <vector>
|
| 9 |
+
|
| 10 |
+
#ifdef _OPENMP
|
| 11 |
+
#include <omp.h>
|
| 12 |
+
#endif
|
| 13 |
+
|
| 14 |
+
#include "common.hpp"
|
| 15 |
+
|
| 16 |
+
namespace py = pybind11;
|
| 17 |
+
|
| 18 |
+
namespace {
|
| 19 |
+
|
| 20 |
+
inline uint64_t pair_key(int32_t left, int32_t right) {
|
| 21 |
+
uint32_t low = static_cast<uint32_t>(left < right ? left : right);
|
| 22 |
+
uint32_t high = static_cast<uint32_t>(left < right ? right : left);
|
| 23 |
+
return (static_cast<uint64_t>(low) << 32) | high;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
struct Corpus {
|
| 27 |
+
int64_t records = 0;
|
| 28 |
+
int32_t width = 0; // slots per record == action dimension
|
| 29 |
+
std::vector<int32_t> member; // [records * width], -1 when free
|
| 30 |
+
std::vector<int64_t> weights; // [records]
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
} // namespace
|
| 34 |
+
|
| 35 |
+
py::tuple train_spatial_native(py::array_t<int32_t, py::array::c_style | py::array::forcecast> codes,
|
| 36 |
+
py::array_t<int64_t, py::array::c_style | py::array::forcecast> weights,
|
| 37 |
+
const std::vector<int32_t>& bins, int64_t vocab_size,
|
| 38 |
+
int threads) {
|
| 39 |
+
auto codes_view = codes.unchecked<2>();
|
| 40 |
+
auto weights_view = weights.unchecked<1>();
|
| 41 |
+
const int64_t records = codes_view.shape(0);
|
| 42 |
+
const int32_t dimension = static_cast<int32_t>(codes_view.shape(1));
|
| 43 |
+
if (dimension != static_cast<int32_t>(bins.size())) {
|
| 44 |
+
throw std::invalid_argument("codes must have one column per bin count");
|
| 45 |
+
}
|
| 46 |
+
if (weights_view.shape(0) != records) {
|
| 47 |
+
throw std::invalid_argument("weights must have one entry per record");
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
std::vector<int32_t> offsets(dimension + 1, 0);
|
| 51 |
+
for (int32_t index = 0; index < dimension; ++index) offsets[index + 1] = offsets[index] + bins[index];
|
| 52 |
+
const int32_t atom_count = offsets[dimension];
|
| 53 |
+
if (vocab_size < atom_count) {
|
| 54 |
+
throw std::invalid_argument("vocab_size is below the number of primitive atoms");
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#ifdef _OPENMP
|
| 58 |
+
const int thread_count = threads > 0 ? threads : omp_get_max_threads();
|
| 59 |
+
#else
|
| 60 |
+
(void)threads;
|
| 61 |
+
const int thread_count = 1;
|
| 62 |
+
#endif
|
| 63 |
+
|
| 64 |
+
Corpus corpus;
|
| 65 |
+
corpus.records = records;
|
| 66 |
+
corpus.width = dimension;
|
| 67 |
+
corpus.member.resize(static_cast<size_t>(records) * dimension);
|
| 68 |
+
corpus.weights.resize(records);
|
| 69 |
+
int64_t total_weight = 0;
|
| 70 |
+
int64_t initial_length = 0;
|
| 71 |
+
for (int64_t record = 0; record < records; ++record) {
|
| 72 |
+
corpus.weights[record] = weights_view(record);
|
| 73 |
+
total_weight += weights_view(record);
|
| 74 |
+
int32_t active = 0;
|
| 75 |
+
for (int32_t slot = 0; slot < dimension; ++slot) {
|
| 76 |
+
const int32_t value = codes_view(record, slot);
|
| 77 |
+
if (value < -1 || value >= bins[slot]) {
|
| 78 |
+
throw std::invalid_argument("a cell index is outside its coordinate's bin count");
|
| 79 |
+
}
|
| 80 |
+
if (value < 0) {
|
| 81 |
+
corpus.member[static_cast<size_t>(record) * dimension + slot] = -1;
|
| 82 |
+
} else {
|
| 83 |
+
corpus.member[static_cast<size_t>(record) * dimension + slot] = offsets[slot] + value;
|
| 84 |
+
++active;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
if (active == 0) throw std::invalid_argument("every record needs an active coordinate");
|
| 88 |
+
initial_length += weights_view(record) * active;
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
// Occurrence sets, kept sorted so intersections and differences are linear.
|
| 92 |
+
std::vector<std::vector<int32_t>> occurrence(atom_count);
|
| 93 |
+
for (int32_t slot = 0; slot < dimension; ++slot) {
|
| 94 |
+
for (int64_t record = 0; record < records; ++record) {
|
| 95 |
+
const int32_t token = corpus.member[static_cast<size_t>(record) * dimension + slot];
|
| 96 |
+
if (token >= 0) occurrence[token].push_back(static_cast<int32_t>(record));
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
for (auto& members : occurrence) std::sort(members.begin(), members.end());
|
| 100 |
+
|
| 101 |
+
// --- initial census -------------------------------------------------------
|
| 102 |
+
ac2::CounterMap counts(static_cast<size_t>(records) * 4 + 64);
|
| 103 |
+
{
|
| 104 |
+
std::vector<ac2::CounterMap> shards;
|
| 105 |
+
shards.reserve(thread_count);
|
| 106 |
+
for (int index = 0; index < thread_count; ++index) shards.emplace_back(1024);
|
| 107 |
+
#ifdef _OPENMP
|
| 108 |
+
#pragma omp parallel num_threads(thread_count)
|
| 109 |
+
#endif
|
| 110 |
+
{
|
| 111 |
+
#ifdef _OPENMP
|
| 112 |
+
const int thread = omp_get_thread_num();
|
| 113 |
+
#else
|
| 114 |
+
const int thread = 0;
|
| 115 |
+
#endif
|
| 116 |
+
ac2::CounterMap& local = shards[thread];
|
| 117 |
+
#ifdef _OPENMP
|
| 118 |
+
#pragma omp for schedule(static)
|
| 119 |
+
#endif
|
| 120 |
+
for (int64_t record = 0; record < records; ++record) {
|
| 121 |
+
const int32_t* row = &corpus.member[static_cast<size_t>(record) * dimension];
|
| 122 |
+
const int64_t weight = corpus.weights[record];
|
| 123 |
+
for (int32_t a = 0; a < dimension; ++a) {
|
| 124 |
+
if (row[a] < 0) continue;
|
| 125 |
+
for (int32_t b = a + 1; b < dimension; ++b) {
|
| 126 |
+
if (row[b] < 0) continue;
|
| 127 |
+
local.add(pair_key(row[a], row[b]), weight);
|
| 128 |
+
}
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
for (auto& shard : shards) {
|
| 133 |
+
shard.for_each([&](uint64_t key, int64_t value) { counts.add(key, value); });
|
| 134 |
+
}
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
std::vector<ac2::HeapEntry> heap;
|
| 138 |
+
heap.reserve(counts.size());
|
| 139 |
+
counts.for_each([&](uint64_t key, int64_t value) { heap.push_back({value, key, 0}); });
|
| 140 |
+
std::make_heap(heap.begin(), heap.end(), ac2::HeapOrder{});
|
| 141 |
+
const size_t initial_pair_types = heap.size();
|
| 142 |
+
|
| 143 |
+
// --- greedy merging -------------------------------------------------------
|
| 144 |
+
std::vector<std::pair<int32_t, int32_t>> merges;
|
| 145 |
+
merges.reserve(static_cast<size_t>(vocab_size - atom_count));
|
| 146 |
+
int64_t length = initial_length; // total active tokens over the corpus
|
| 147 |
+
int64_t heap_pops = 0;
|
| 148 |
+
int64_t counter_updates = 0;
|
| 149 |
+
|
| 150 |
+
std::vector<int32_t> matched;
|
| 151 |
+
std::vector<int32_t> remainder;
|
| 152 |
+
std::vector<int32_t> affected;
|
| 153 |
+
std::vector<int32_t> stamp(static_cast<size_t>(vocab_size) + 16, 0);
|
| 154 |
+
int32_t generation = 0;
|
| 155 |
+
std::vector<ac2::CounterMap> deltas;
|
| 156 |
+
deltas.reserve(thread_count);
|
| 157 |
+
for (int index = 0; index < thread_count; ++index) deltas.emplace_back(1024);
|
| 158 |
+
|
| 159 |
+
while (static_cast<int64_t>(merges.size()) + atom_count < vocab_size) {
|
| 160 |
+
int32_t left = -1;
|
| 161 |
+
int32_t right = -1;
|
| 162 |
+
int64_t frequency = 0;
|
| 163 |
+
while (!heap.empty()) {
|
| 164 |
+
const ac2::HeapEntry top = ac2::heap_pop(heap);
|
| 165 |
+
++heap_pops;
|
| 166 |
+
const int64_t current = counts.get(top.key);
|
| 167 |
+
if (current <= 0) continue;
|
| 168 |
+
if (current != top.count) continue; // a fresher entry for this key exists
|
| 169 |
+
left = static_cast<int32_t>(top.key >> 32);
|
| 170 |
+
right = static_cast<int32_t>(top.key & 0xffffffffu);
|
| 171 |
+
frequency = current;
|
| 172 |
+
break;
|
| 173 |
+
}
|
| 174 |
+
if (left < 0) break; // every record has collapsed to a single token
|
| 175 |
+
|
| 176 |
+
matched.clear();
|
| 177 |
+
std::set_intersection(occurrence[left].begin(), occurrence[left].end(),
|
| 178 |
+
occurrence[right].begin(), occurrence[right].end(),
|
| 179 |
+
std::back_inserter(matched));
|
| 180 |
+
if (matched.empty()) { // cannot happen while counters are exact
|
| 181 |
+
counts.add(pair_key(left, right), -frequency);
|
| 182 |
+
continue;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
const int32_t child = static_cast<int32_t>(occurrence.size());
|
| 186 |
+
occurrence.emplace_back();
|
| 187 |
+
|
| 188 |
+
const bool parallel = matched.size() > 4096 && thread_count > 1;
|
| 189 |
+
if (parallel) {
|
| 190 |
+
for (auto& shard : deltas) shard = ac2::CounterMap(1024);
|
| 191 |
+
}
|
| 192 |
+
#ifdef _OPENMP
|
| 193 |
+
#pragma omp parallel if (parallel) num_threads(thread_count)
|
| 194 |
+
#endif
|
| 195 |
+
{
|
| 196 |
+
#ifdef _OPENMP
|
| 197 |
+
const int thread = parallel ? omp_get_thread_num() : 0;
|
| 198 |
+
#else
|
| 199 |
+
const int thread = 0;
|
| 200 |
+
#endif
|
| 201 |
+
ac2::CounterMap& local = parallel ? deltas[thread] : counts;
|
| 202 |
+
#ifdef _OPENMP
|
| 203 |
+
#pragma omp for schedule(static)
|
| 204 |
+
#endif
|
| 205 |
+
for (int64_t index = 0; index < static_cast<int64_t>(matched.size()); ++index) {
|
| 206 |
+
const int32_t record = matched[index];
|
| 207 |
+
const int32_t* row = &corpus.member[static_cast<size_t>(record) * dimension];
|
| 208 |
+
const int64_t weight = corpus.weights[record];
|
| 209 |
+
for (int32_t slot = 0; slot < dimension; ++slot) {
|
| 210 |
+
const int32_t other = row[slot];
|
| 211 |
+
if (other < 0 || other == left || other == right) continue;
|
| 212 |
+
local.add(pair_key(left, other), -weight);
|
| 213 |
+
local.add(pair_key(right, other), -weight);
|
| 214 |
+
local.add(pair_key(child, other), weight);
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
}
|
| 218 |
+
if (parallel) {
|
| 219 |
+
for (auto& shard : deltas) {
|
| 220 |
+
shard.for_each([&](uint64_t key, int64_t value) { counts.add(key, value); });
|
| 221 |
+
}
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// Rewrite the touched rows: the child takes the left parent's slot.
|
| 225 |
+
for (const int32_t record : matched) {
|
| 226 |
+
int32_t* row = &corpus.member[static_cast<size_t>(record) * dimension];
|
| 227 |
+
for (int32_t slot = 0; slot < dimension; ++slot) {
|
| 228 |
+
if (row[slot] == left) {
|
| 229 |
+
row[slot] = child;
|
| 230 |
+
} else if (row[slot] == right) {
|
| 231 |
+
row[slot] = -1;
|
| 232 |
+
}
|
| 233 |
+
}
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
counts.add(pair_key(left, right), -frequency);
|
| 237 |
+
counter_updates += static_cast<int64_t>(matched.size()) * dimension;
|
| 238 |
+
|
| 239 |
+
occurrence[child] = matched;
|
| 240 |
+
for (const int32_t parent : {left, right}) {
|
| 241 |
+
remainder.clear();
|
| 242 |
+
std::set_difference(occurrence[parent].begin(), occurrence[parent].end(), matched.begin(),
|
| 243 |
+
matched.end(), std::back_inserter(remainder));
|
| 244 |
+
occurrence[parent].swap(remainder);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
affected.clear();
|
| 248 |
+
if (static_cast<size_t>(child) >= stamp.size()) stamp.resize(child + 1024, 0);
|
| 249 |
+
++generation;
|
| 250 |
+
for (const int32_t record : matched) {
|
| 251 |
+
const int32_t* row = &corpus.member[static_cast<size_t>(record) * dimension];
|
| 252 |
+
for (int32_t slot = 0; slot < dimension; ++slot) {
|
| 253 |
+
const int32_t other = row[slot];
|
| 254 |
+
if (other < 0 || other == child) continue;
|
| 255 |
+
if (stamp[other] == generation) continue;
|
| 256 |
+
stamp[other] = generation;
|
| 257 |
+
affected.push_back(other);
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
for (const int32_t other : affected) {
|
| 261 |
+
for (const int32_t parent : {left, right, child}) {
|
| 262 |
+
const uint64_t key = pair_key(parent, other);
|
| 263 |
+
const int64_t value = counts.get(key);
|
| 264 |
+
if (value > 0) ac2::heap_push(heap, {value, key, 0});
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
merges.emplace_back(left, right);
|
| 269 |
+
length -= frequency;
|
| 270 |
+
|
| 271 |
+
if (heap.size() > 4 * counts.size() + 65536) {
|
| 272 |
+
heap.clear();
|
| 273 |
+
counts.for_each([&](uint64_t key, int64_t value) { heap.push_back({value, key, 0}); });
|
| 274 |
+
std::make_heap(heap.begin(), heap.end(), ac2::HeapOrder{});
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
py::list merge_list;
|
| 279 |
+
for (const auto& merge : merges) {
|
| 280 |
+
merge_list.append(py::make_tuple(merge.first, merge.second));
|
| 281 |
+
}
|
| 282 |
+
py::dict report;
|
| 283 |
+
report["records"] = records;
|
| 284 |
+
report["weight"] = total_weight;
|
| 285 |
+
report["atoms"] = atom_count;
|
| 286 |
+
report["merges"] = static_cast<int64_t>(merges.size());
|
| 287 |
+
report["vocab_size"] = atom_count + static_cast<int64_t>(merges.size());
|
| 288 |
+
report["tokens_per_action_initial"] = static_cast<float>(initial_length) / total_weight;
|
| 289 |
+
report["tokens_per_action"] = static_cast<float>(length) / static_cast<float>(total_weight);
|
| 290 |
+
report["compression"] = static_cast<float>(initial_length) / static_cast<float>(length);
|
| 291 |
+
report["initial_pair_types"] = static_cast<int64_t>(initial_pair_types);
|
| 292 |
+
report["heap_pops"] = heap_pops;
|
| 293 |
+
report["counter_updates"] = counter_updates;
|
| 294 |
+
report["threads"] = thread_count;
|
| 295 |
+
report["backend"] = "native_sparse_exact";
|
| 296 |
+
return py::make_tuple(merge_list, report);
|
| 297 |
+
}
|
actioncodec2_kernels/temporal.cpp
ADDED
|
@@ -0,0 +1,799 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Native exact rooted temporal Set-BPE trainer.
|
| 2 |
+
//
|
| 3 |
+
// A temporal symbol is rooted at one token of the current control step and may
|
| 4 |
+
// absorb spatial tokens from a bounded number of future steps. Training is the
|
| 5 |
+
// exact sparse local-graph algorithm: counters exist only for rule keys that
|
| 6 |
+
// actually occur inside the lookahead window, each merge updates only the edges
|
| 7 |
+
// incident to the instances it changed, and shifted self-rules (left == right)
|
| 8 |
+
// are certified by exact left-to-right maximum matching before they may win.
|
| 9 |
+
//
|
| 10 |
+
// This mirrors actioncodec2/setbpe/temporal.py merge for merge, including the
|
| 11 |
+
// tie-break, so the reference trainer can prove the two agree. What differs is
|
| 12 |
+
// the constant factor: the Python trainer allocates a dense
|
| 13 |
+
// ``lookahead x V x V`` legality table, while this one derives legality from the
|
| 14 |
+
// per-token time-shifted support masks in O(span) with no allocation, which is
|
| 15 |
+
// what makes large vocabularies tractable.
|
| 16 |
+
|
| 17 |
+
#include <pybind11/pybind11.h>
|
| 18 |
+
#include <pybind11/numpy.h>
|
| 19 |
+
#include <pybind11/stl.h>
|
| 20 |
+
|
| 21 |
+
#include <algorithm>
|
| 22 |
+
#include <array>
|
| 23 |
+
#include <chrono>
|
| 24 |
+
#include <cstdint>
|
| 25 |
+
#include <deque>
|
| 26 |
+
#include <limits>
|
| 27 |
+
#include <memory>
|
| 28 |
+
#include <string>
|
| 29 |
+
#include <unordered_map>
|
| 30 |
+
#include <vector>
|
| 31 |
+
|
| 32 |
+
#include "common.hpp"
|
| 33 |
+
|
| 34 |
+
#ifdef _OPENMP
|
| 35 |
+
#include <omp.h>
|
| 36 |
+
#endif
|
| 37 |
+
|
| 38 |
+
namespace py = pybind11;
|
| 39 |
+
|
| 40 |
+
namespace {
|
| 41 |
+
|
| 42 |
+
struct TemporalEntry {
|
| 43 |
+
int64_t count;
|
| 44 |
+
uint64_t key;
|
| 45 |
+
uint32_t version;
|
| 46 |
+
uint8_t certified;
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
struct TemporalOrder {
|
| 50 |
+
bool operator()(const TemporalEntry& a, const TemporalEntry& b) const {
|
| 51 |
+
if (a.count != b.count) return a.count < b.count; // larger count first
|
| 52 |
+
if (a.key != b.key) return a.key > b.key; // smaller key first
|
| 53 |
+
return a.certified > b.certified; // uncertified first
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
struct InstanceChunk {
|
| 58 |
+
const int32_t* tokens;
|
| 59 |
+
const int64_t* anchors;
|
| 60 |
+
int64_t size;
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
size_t initial_counter_capacity(int64_t instance_count, int32_t base_vocab, int32_t lookahead) {
|
| 64 |
+
const size_t by_instances = static_cast<size_t>(instance_count) * 4 + 64;
|
| 65 |
+
const size_t vocab = static_cast<size_t>(base_vocab);
|
| 66 |
+
const size_t offsets = static_cast<size_t>(std::max(lookahead - 1, 0));
|
| 67 |
+
if (vocab != 0 && offsets > (std::numeric_limits<size_t>::max() - 64) / vocab / vocab) {
|
| 68 |
+
return by_instances;
|
| 69 |
+
}
|
| 70 |
+
const size_t by_key_universe = vocab * vocab * offsets + 64;
|
| 71 |
+
return std::min(by_instances, by_key_universe);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
inline uint64_t rule_key(int32_t left, int32_t right, int32_t offset) {
|
| 75 |
+
return (static_cast<uint64_t>(left) << 40) | (static_cast<uint64_t>(right) << 16) |
|
| 76 |
+
static_cast<uint64_t>(offset);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
inline void unpack(uint64_t key, int32_t& left, int32_t& right, int32_t& offset) {
|
| 80 |
+
left = static_cast<int32_t>(key >> 40);
|
| 81 |
+
right = static_cast<int32_t>((key >> 16) & 0xffffffull);
|
| 82 |
+
offset = static_cast<int32_t>(key & 0xffffull);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
class Trainer {
|
| 86 |
+
public:
|
| 87 |
+
Trainer(int64_t total_anchors, int64_t instance_capacity_hint,
|
| 88 |
+
const uint64_t* spatial_support, const int32_t* spatial_anchor, int32_t base_vocab,
|
| 89 |
+
int32_t dimension, int32_t lookahead, int64_t vocab_size, int threads)
|
| 90 |
+
: base_vocab_(base_vocab),
|
| 91 |
+
dimension_(dimension),
|
| 92 |
+
lookahead_(lookahead),
|
| 93 |
+
total_anchors_(total_anchors),
|
| 94 |
+
counts_(initial_counter_capacity(instance_capacity_hint, base_vocab, lookahead)) {
|
| 95 |
+
#ifdef _OPENMP
|
| 96 |
+
// More workers eventually make the per-merge local-map reduction slower
|
| 97 |
+
// than the incident scan itself. Keep auto mode at the measured scaling
|
| 98 |
+
// knee; an explicit value remains an opt-in override.
|
| 99 |
+
threads_ = threads > 0 ? threads : std::min(omp_get_max_threads(), 32);
|
| 100 |
+
#else
|
| 101 |
+
(void)threads;
|
| 102 |
+
threads_ = 1;
|
| 103 |
+
#endif
|
| 104 |
+
const size_t capacity = static_cast<size_t>(vocab_size) + 16;
|
| 105 |
+
masks_.assign(capacity * lookahead_, 0);
|
| 106 |
+
spans_.assign(capacity, 1);
|
| 107 |
+
root_anchor_.assign(capacity, 0);
|
| 108 |
+
pattern_start_.assign(capacity + 1, 0);
|
| 109 |
+
for (int32_t token = 0; token < base_vocab_; ++token) {
|
| 110 |
+
masks_[static_cast<size_t>(token) * lookahead_] = spatial_support[token];
|
| 111 |
+
root_anchor_[token] = spatial_anchor[token];
|
| 112 |
+
pattern_start_[token] = static_cast<int32_t>(pattern_.size());
|
| 113 |
+
pattern_.push_back({0, token});
|
| 114 |
+
}
|
| 115 |
+
pattern_start_[base_vocab_] = static_cast<int32_t>(pattern_.size());
|
| 116 |
+
token_count_ = base_vocab_;
|
| 117 |
+
|
| 118 |
+
member_.assign(static_cast<size_t>(total_anchors) * dimension_, -1);
|
| 119 |
+
occurrence_.resize(capacity);
|
| 120 |
+
dirty_.assign(capacity, 0);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
void add_chunk(const InstanceChunk& chunk) {
|
| 124 |
+
for (int64_t index = 0; index < chunk.size; ++index) {
|
| 125 |
+
add_instance(chunk.tokens[index], chunk.anchors[index]);
|
| 126 |
+
}
|
| 127 |
+
initial_count_ += chunk.size;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
void finalize_instances() {
|
| 131 |
+
if (initial_count_ <= 0) throw std::runtime_error("temporal training needs instances");
|
| 132 |
+
active_count_ = initial_count_;
|
| 133 |
+
const auto census_started = std::chrono::steady_clock::now();
|
| 134 |
+
census();
|
| 135 |
+
census_seconds_ = elapsed(census_started);
|
| 136 |
+
counts_.for_each([&](uint64_t key, int64_t value) { heap_.push_back({value, key, 0, 0}); });
|
| 137 |
+
std::make_heap(heap_.begin(), heap_.end(), TemporalOrder{});
|
| 138 |
+
initial_edge_types_ = heap_.size();
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
// -- geometry ------------------------------------------------------------
|
| 142 |
+
|
| 143 |
+
bool legal(int32_t left, int32_t right, int32_t offset) const {
|
| 144 |
+
const int32_t right_span = spans_[right];
|
| 145 |
+
if (offset + right_span > lookahead_) return false;
|
| 146 |
+
const uint64_t* left_masks = &masks_[static_cast<size_t>(left) * lookahead_];
|
| 147 |
+
const uint64_t* right_masks = &masks_[static_cast<size_t>(right) * lookahead_];
|
| 148 |
+
const int32_t overlap = std::min(spans_[left] - offset, right_span);
|
| 149 |
+
for (int32_t step = 0; step < overlap; ++step) {
|
| 150 |
+
if (left_masks[offset + step] & right_masks[step]) return false;
|
| 151 |
+
}
|
| 152 |
+
return true;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// -- instance bookkeeping ------------------------------------------------
|
| 156 |
+
|
| 157 |
+
int32_t add_instance(int32_t token, int64_t anchor) {
|
| 158 |
+
if (token < 0 || static_cast<size_t>(token) >= occurrence_.size()) {
|
| 159 |
+
throw std::runtime_error("temporal instance token " + std::to_string(token) +
|
| 160 |
+
" is outside trainer capacity " +
|
| 161 |
+
std::to_string(occurrence_.size()));
|
| 162 |
+
}
|
| 163 |
+
if (anchor < 0 || anchor >= total_anchors_) {
|
| 164 |
+
throw std::runtime_error("temporal instance anchor is outside the corpus timeline");
|
| 165 |
+
}
|
| 166 |
+
int32_t* row = &member_[static_cast<size_t>(anchor) * dimension_];
|
| 167 |
+
int32_t slot = 0;
|
| 168 |
+
while (slot < dimension_ && row[slot] >= 0) ++slot;
|
| 169 |
+
if (slot == dimension_) {
|
| 170 |
+
throw std::runtime_error("temporal anchor exceeded the token-width bound");
|
| 171 |
+
}
|
| 172 |
+
row[slot] = token;
|
| 173 |
+
occurrence_[token].push_back(anchor);
|
| 174 |
+
dirty_[token] = 1;
|
| 175 |
+
return slot;
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
// Removal touches only the row mirror. The occurrence list is allowed to
|
| 179 |
+
// keep stale anchors because ``occurs_at`` is an exact membership test, so
|
| 180 |
+
// compaction can be deferred to the next time the list is actually read.
|
| 181 |
+
void remove_instance(int32_t token, int64_t anchor) {
|
| 182 |
+
int32_t* row = &member_[static_cast<size_t>(anchor) * dimension_];
|
| 183 |
+
for (int32_t slot = 0; slot < dimension_; ++slot) {
|
| 184 |
+
if (row[slot] == token) {
|
| 185 |
+
row[slot] = -1;
|
| 186 |
+
break;
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
dirty_[token] = 1;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
const std::vector<int64_t>& sorted_occurrence(int32_t token) {
|
| 193 |
+
auto& anchors = occurrence_[token];
|
| 194 |
+
if (dirty_[token]) {
|
| 195 |
+
std::sort(anchors.begin(), anchors.end());
|
| 196 |
+
anchors.erase(std::unique(anchors.begin(), anchors.end()), anchors.end());
|
| 197 |
+
anchors.erase(std::remove_if(anchors.begin(), anchors.end(),
|
| 198 |
+
[&](int64_t anchor) { return !occurs_at(token, anchor); }),
|
| 199 |
+
anchors.end());
|
| 200 |
+
dirty_[token] = 0;
|
| 201 |
+
}
|
| 202 |
+
return anchors;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
bool occurs_at(int32_t token, int64_t anchor) const {
|
| 206 |
+
const int32_t* row = &member_[static_cast<size_t>(anchor) * dimension_];
|
| 207 |
+
for (int32_t slot = 0; slot < dimension_; ++slot) {
|
| 208 |
+
if (row[slot] == token) return true;
|
| 209 |
+
}
|
| 210 |
+
return false;
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
// -- matching ------------------------------------------------------------
|
| 214 |
+
|
| 215 |
+
std::vector<std::pair<int64_t, int64_t>> select(int32_t left, int32_t right, int32_t offset) {
|
| 216 |
+
std::vector<std::pair<int64_t, int64_t>> matches;
|
| 217 |
+
const std::vector<int64_t>& anchors = sorted_occurrence(left);
|
| 218 |
+
if (left != right) {
|
| 219 |
+
#ifdef _OPENMP
|
| 220 |
+
if (threads_ > 1 && anchors.size() >= 65536) {
|
| 221 |
+
const int workers = std::min<int64_t>(threads_, anchors.size() / 32768);
|
| 222 |
+
std::vector<std::vector<std::pair<int64_t, int64_t>>> local(workers);
|
| 223 |
+
#pragma omp parallel num_threads(workers)
|
| 224 |
+
{
|
| 225 |
+
const int worker = omp_get_thread_num();
|
| 226 |
+
const size_t start = anchors.size() * static_cast<size_t>(worker) / workers;
|
| 227 |
+
const size_t stop = anchors.size() * static_cast<size_t>(worker + 1) / workers;
|
| 228 |
+
auto& output = local[worker];
|
| 229 |
+
output.reserve(stop - start);
|
| 230 |
+
for (size_t index = start; index < stop; ++index) {
|
| 231 |
+
const int64_t anchor = anchors[index];
|
| 232 |
+
const int64_t target = anchor + offset;
|
| 233 |
+
if (target < total_anchors_ && occurs_at(right, target)) {
|
| 234 |
+
output.emplace_back(anchor, target);
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
size_t count = 0;
|
| 239 |
+
for (const auto& output : local) count += output.size();
|
| 240 |
+
matches.reserve(count);
|
| 241 |
+
for (auto& output : local) {
|
| 242 |
+
matches.insert(matches.end(), output.begin(), output.end());
|
| 243 |
+
}
|
| 244 |
+
return matches;
|
| 245 |
+
}
|
| 246 |
+
#endif
|
| 247 |
+
for (const int64_t anchor : anchors) {
|
| 248 |
+
const int64_t target = anchor + offset;
|
| 249 |
+
if (target < total_anchors_ && occurs_at(right, target)) {
|
| 250 |
+
matches.emplace_back(anchor, target);
|
| 251 |
+
}
|
| 252 |
+
}
|
| 253 |
+
return matches;
|
| 254 |
+
}
|
| 255 |
+
// Shifted self-rule: an instance cannot be both the left and the right end
|
| 256 |
+
// of two different merges, so the true gain is a maximum matching, which
|
| 257 |
+
// for a fixed shift on a line is exactly greedy left to right.
|
| 258 |
+
std::deque<int64_t> consumed_targets;
|
| 259 |
+
for (const int64_t anchor : anchors) {
|
| 260 |
+
while (!consumed_targets.empty() && consumed_targets.front() < anchor) {
|
| 261 |
+
consumed_targets.pop_front();
|
| 262 |
+
}
|
| 263 |
+
if (!consumed_targets.empty() && consumed_targets.front() == anchor) {
|
| 264 |
+
consumed_targets.pop_front();
|
| 265 |
+
continue;
|
| 266 |
+
}
|
| 267 |
+
const int64_t wide = anchor + offset;
|
| 268 |
+
if (wide >= total_anchors_) continue;
|
| 269 |
+
const int64_t target = wide;
|
| 270 |
+
if (!occurs_at(left, target)) continue;
|
| 271 |
+
matches.emplace_back(anchor, target);
|
| 272 |
+
consumed_targets.push_back(target);
|
| 273 |
+
}
|
| 274 |
+
return matches;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
// -- search --------------------------------------------------------------
|
| 278 |
+
|
| 279 |
+
bool best(int32_t& left, int32_t& right, int32_t& offset,
|
| 280 |
+
std::vector<std::pair<int64_t, int64_t>>& matches) {
|
| 281 |
+
while (!heap_.empty()) {
|
| 282 |
+
std::pop_heap(heap_.begin(), heap_.end(), TemporalOrder{});
|
| 283 |
+
const TemporalEntry top = heap_.back();
|
| 284 |
+
heap_.pop_back();
|
| 285 |
+
++heap_pops_;
|
| 286 |
+
const int64_t current = counts_.get(top.key);
|
| 287 |
+
if (current <= 0) continue;
|
| 288 |
+
unpack(top.key, left, right, offset);
|
| 289 |
+
const bool self_rule = left == right;
|
| 290 |
+
const uint32_t version = self_rule ? self_version(top.key) : 0;
|
| 291 |
+
const bool stale = self_rule ? version != top.version : current != top.count;
|
| 292 |
+
if (stale) {
|
| 293 |
+
heap_.push_back({current, top.key, version, 0});
|
| 294 |
+
std::push_heap(heap_.begin(), heap_.end(), TemporalOrder{});
|
| 295 |
+
continue;
|
| 296 |
+
}
|
| 297 |
+
if (self_rule && !top.certified) {
|
| 298 |
+
// Raw edge count only upper-bounds the achievable gain here; replace
|
| 299 |
+
// the priority with the exact matching size and re-queue.
|
| 300 |
+
auto certified = select(left, right, offset);
|
| 301 |
+
self_cache_[top.key] = {version, certified};
|
| 302 |
+
heap_.push_back({static_cast<int64_t>(certified.size()), top.key, version, 1});
|
| 303 |
+
std::push_heap(heap_.begin(), heap_.end(), TemporalOrder{});
|
| 304 |
+
++self_certifications_;
|
| 305 |
+
continue;
|
| 306 |
+
}
|
| 307 |
+
if (top.certified) {
|
| 308 |
+
auto cached = self_cache_.find(top.key);
|
| 309 |
+
if (cached == self_cache_.end() || cached->second.first != version) continue;
|
| 310 |
+
matches = cached->second.second;
|
| 311 |
+
} else {
|
| 312 |
+
matches = select(left, right, offset);
|
| 313 |
+
}
|
| 314 |
+
if (static_cast<int64_t>(matches.size()) != top.count) {
|
| 315 |
+
throw std::runtime_error("temporal counter disagrees with exact matching");
|
| 316 |
+
}
|
| 317 |
+
return !matches.empty();
|
| 318 |
+
}
|
| 319 |
+
return false;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
// -- commit --------------------------------------------------------------
|
| 323 |
+
|
| 324 |
+
void commit(int32_t left, int32_t right, int32_t offset,
|
| 325 |
+
const std::vector<std::pair<int64_t, int64_t>>& matches) {
|
| 326 |
+
const int32_t child = token_count_++;
|
| 327 |
+
// The child's pattern is the left pattern plus the right pattern shifted.
|
| 328 |
+
const int32_t start = static_cast<int32_t>(pattern_.size());
|
| 329 |
+
for (int32_t index = pattern_start_[left]; index < pattern_start_[left + 1]; ++index) {
|
| 330 |
+
pattern_.push_back(pattern_[index]);
|
| 331 |
+
}
|
| 332 |
+
for (int32_t index = pattern_start_[right]; index < pattern_start_[right + 1]; ++index) {
|
| 333 |
+
pattern_.push_back({pattern_[index].first + offset, pattern_[index].second});
|
| 334 |
+
}
|
| 335 |
+
std::sort(pattern_.begin() + start, pattern_.end());
|
| 336 |
+
pattern_start_[child] = start;
|
| 337 |
+
pattern_start_[child + 1] = static_cast<int32_t>(pattern_.size());
|
| 338 |
+
|
| 339 |
+
int32_t span = 0;
|
| 340 |
+
int32_t roots = 0;
|
| 341 |
+
uint64_t* child_masks = &masks_[static_cast<size_t>(child) * lookahead_];
|
| 342 |
+
for (int32_t index = start; index < static_cast<int32_t>(pattern_.size()); ++index) {
|
| 343 |
+
const int32_t time = pattern_[index].first;
|
| 344 |
+
const uint64_t support = masks_[static_cast<size_t>(pattern_[index].second) * lookahead_];
|
| 345 |
+
if (time >= lookahead_) throw std::runtime_error("temporal merge exceeds the lookahead");
|
| 346 |
+
if (child_masks[time] & support) {
|
| 347 |
+
throw std::runtime_error("temporal merge joins overlapping spatial supports");
|
| 348 |
+
}
|
| 349 |
+
child_masks[time] |= support;
|
| 350 |
+
span = std::max(span, time + 1);
|
| 351 |
+
roots += (time == 0);
|
| 352 |
+
}
|
| 353 |
+
if (roots != 1) throw std::runtime_error("a rooted temporal token needs one time-zero token");
|
| 354 |
+
spans_[child] = span;
|
| 355 |
+
root_anchor_[child] = root_anchor_[left];
|
| 356 |
+
|
| 357 |
+
ac2::CounterMap deltas(
|
| 358 |
+
std::min(matches.size() * 8 + 64, incident_key_capacity(token_count_)));
|
| 359 |
+
std::vector<std::pair<int32_t, int64_t>> removed; // (token, anchor), sorted by anchor
|
| 360 |
+
removed.reserve(matches.size() * 2);
|
| 361 |
+
size_t left_index = 0;
|
| 362 |
+
size_t right_index = 0;
|
| 363 |
+
while (left_index < matches.size() || right_index < matches.size()) {
|
| 364 |
+
const int64_t left_anchor =
|
| 365 |
+
left_index < matches.size() ? matches[left_index].first : total_anchors_;
|
| 366 |
+
const int64_t right_anchor =
|
| 367 |
+
right_index < matches.size() ? matches[right_index].second : total_anchors_;
|
| 368 |
+
if (left_anchor <= right_anchor) {
|
| 369 |
+
removed.emplace_back(left, left_anchor);
|
| 370 |
+
++left_index;
|
| 371 |
+
} else {
|
| 372 |
+
removed.emplace_back(right, right_anchor);
|
| 373 |
+
++right_index;
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
accumulate_incident(removed, -1, deltas);
|
| 377 |
+
correct_internal(removed, +1, deltas);
|
| 378 |
+
|
| 379 |
+
for (const auto& instance : removed) remove_instance(instance.first, instance.second);
|
| 380 |
+
std::vector<std::pair<int32_t, int64_t>> children;
|
| 381 |
+
children.reserve(matches.size());
|
| 382 |
+
for (const auto& match : matches) {
|
| 383 |
+
add_instance(child, match.first);
|
| 384 |
+
children.emplace_back(child, match.first);
|
| 385 |
+
}
|
| 386 |
+
accumulate_incident(children, +1, deltas);
|
| 387 |
+
correct_internal(children, -1, deltas);
|
| 388 |
+
|
| 389 |
+
apply(deltas, rule_key(left, right, offset));
|
| 390 |
+
active_count_ -= static_cast<int64_t>(matches.size());
|
| 391 |
+
merges_.push_back({left, right, offset});
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
// -- accessors -----------------------------------------------------------
|
| 395 |
+
|
| 396 |
+
const std::vector<std::array<int32_t, 3>>& merges() const { return merges_; }
|
| 397 |
+
int64_t active_count() const { return active_count_; }
|
| 398 |
+
int64_t initial_count() const { return initial_count_; }
|
| 399 |
+
int64_t heap_pops() const { return heap_pops_; }
|
| 400 |
+
int64_t counter_updates() const { return counter_updates_; }
|
| 401 |
+
int64_t self_certifications() const { return self_certifications_; }
|
| 402 |
+
size_t initial_edge_types() const { return initial_edge_types_; }
|
| 403 |
+
size_t live_edge_types() const { return counts_.live_entries(); }
|
| 404 |
+
size_t counter_buckets() const { return counts_.buckets(); }
|
| 405 |
+
int threads() const { return threads_; }
|
| 406 |
+
double census_seconds() const { return census_seconds_; }
|
| 407 |
+
double incident_seconds() const { return incident_seconds_; }
|
| 408 |
+
double incident_parallel_seconds() const { return incident_parallel_seconds_; }
|
| 409 |
+
double incident_merge_seconds() const { return incident_merge_seconds_; }
|
| 410 |
+
double correction_seconds() const { return correction_seconds_; }
|
| 411 |
+
double correction_parallel_seconds() const { return correction_parallel_seconds_; }
|
| 412 |
+
double apply_seconds() const { return apply_seconds_; }
|
| 413 |
+
|
| 414 |
+
private:
|
| 415 |
+
static double elapsed(std::chrono::steady_clock::time_point started) {
|
| 416 |
+
return std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
size_t incident_key_capacity(int32_t vocabulary) const {
|
| 420 |
+
return static_cast<size_t>(6) * static_cast<size_t>(vocabulary) *
|
| 421 |
+
static_cast<size_t>(std::max(lookahead_ - 1, 0)) +
|
| 422 |
+
64;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
void census_anchor(int64_t anchor, ac2::CounterMap& counter) const {
|
| 426 |
+
const int32_t* row = &member_[static_cast<size_t>(anchor) * dimension_];
|
| 427 |
+
for (int32_t slot = 0; slot < dimension_; ++slot) {
|
| 428 |
+
const int32_t left = row[slot];
|
| 429 |
+
if (left < 0) continue;
|
| 430 |
+
for (int32_t offset = 1; offset < lookahead_; ++offset) {
|
| 431 |
+
const int64_t target = anchor + offset;
|
| 432 |
+
if (target >= total_anchors_) break;
|
| 433 |
+
const int32_t* other_row = &member_[static_cast<size_t>(target) * dimension_];
|
| 434 |
+
for (int32_t other_slot = 0; other_slot < dimension_; ++other_slot) {
|
| 435 |
+
const int32_t right = other_row[other_slot];
|
| 436 |
+
if (right >= 0 && legal(left, right, offset)) {
|
| 437 |
+
counter.add(rule_key(left, right, offset), 1);
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
}
|
| 441 |
+
}
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
void census() {
|
| 445 |
+
const size_t key_universe = initial_counter_capacity(total_anchors_, base_vocab_, lookahead_);
|
| 446 |
+
#ifdef _OPENMP
|
| 447 |
+
if (threads_ > 1 && total_anchors_ >= 4096) {
|
| 448 |
+
const int workers = std::min<int64_t>(threads_, total_anchors_ / 2048);
|
| 449 |
+
// Small rule universes fit comfortably in one local map per worker.
|
| 450 |
+
// For a large spatial vocabulary, each long-running worker would
|
| 451 |
+
// eventually discover almost the whole universe and multiply memory by
|
| 452 |
+
// the thread count. Merge fixed-size anchor batches instead: this keeps
|
| 453 |
+
// local memory bounded while the global counter remains exact.
|
| 454 |
+
const bool batched = key_universe > (1u << 20);
|
| 455 |
+
const int64_t anchors_per_worker = batched ? 8192 : total_anchors_;
|
| 456 |
+
const int64_t batch_size = anchors_per_worker * workers;
|
| 457 |
+
const size_t local_hint = batched
|
| 458 |
+
? std::min<size_t>(key_universe,
|
| 459 |
+
anchors_per_worker * 32 + 64)
|
| 460 |
+
: key_universe;
|
| 461 |
+
std::vector<std::unique_ptr<ac2::CounterMap>> local;
|
| 462 |
+
local.reserve(workers);
|
| 463 |
+
for (int worker = 0; worker < workers; ++worker) {
|
| 464 |
+
local.push_back(std::make_unique<ac2::CounterMap>(local_hint));
|
| 465 |
+
}
|
| 466 |
+
for (int64_t batch_start = 0; batch_start < total_anchors_; batch_start += batch_size) {
|
| 467 |
+
const int64_t batch_end = std::min(total_anchors_, batch_start + batch_size);
|
| 468 |
+
#pragma omp parallel for schedule(static) num_threads(workers)
|
| 469 |
+
for (int64_t anchor = batch_start; anchor < batch_end; ++anchor) {
|
| 470 |
+
census_anchor(anchor, *local[omp_get_thread_num()]);
|
| 471 |
+
}
|
| 472 |
+
for (const auto& counter : local) {
|
| 473 |
+
counter->for_each([&](uint64_t key, int64_t value) { counts_.add(key, value); });
|
| 474 |
+
if (batched) counter->clear();
|
| 475 |
+
}
|
| 476 |
+
}
|
| 477 |
+
return;
|
| 478 |
+
}
|
| 479 |
+
#endif
|
| 480 |
+
for (int64_t anchor = 0; anchor < total_anchors_; ++anchor) census_anchor(anchor, counts_);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
uint32_t self_version(uint64_t key) const {
|
| 484 |
+
auto found = self_versions_.find(key);
|
| 485 |
+
return found == self_versions_.end() ? 0u : found->second;
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
void accumulate_incident(const std::vector<std::pair<int32_t, int64_t>>& instances,
|
| 489 |
+
int64_t multiplier, ac2::CounterMap& deltas) {
|
| 490 |
+
const auto started = std::chrono::steady_clock::now();
|
| 491 |
+
#ifdef _OPENMP
|
| 492 |
+
if (threads_ > 1 && instances.size() >= 1024) {
|
| 493 |
+
const int workers = std::min<int>(threads_, static_cast<int>(instances.size() / 512));
|
| 494 |
+
const size_t hint = std::min(
|
| 495 |
+
instances.size() * 8 / static_cast<size_t>(workers) + 64,
|
| 496 |
+
incident_key_capacity(token_count_));
|
| 497 |
+
std::vector<std::unique_ptr<ac2::CounterMap>> local;
|
| 498 |
+
local.reserve(workers);
|
| 499 |
+
for (int worker = 0; worker < workers; ++worker) {
|
| 500 |
+
local.push_back(std::make_unique<ac2::CounterMap>(hint));
|
| 501 |
+
}
|
| 502 |
+
const auto parallel_started = std::chrono::steady_clock::now();
|
| 503 |
+
#pragma omp parallel for schedule(static) num_threads(workers)
|
| 504 |
+
for (int64_t index = 0; index < static_cast<int64_t>(instances.size()); ++index) {
|
| 505 |
+
accumulate_one(instances[static_cast<size_t>(index)], multiplier,
|
| 506 |
+
*local[omp_get_thread_num()]);
|
| 507 |
+
}
|
| 508 |
+
incident_parallel_seconds_ += elapsed(parallel_started);
|
| 509 |
+
const auto merge_started = std::chrono::steady_clock::now();
|
| 510 |
+
for (const auto& counter : local) {
|
| 511 |
+
counter->for_each([&](uint64_t key, int64_t value) { deltas.add(key, value); });
|
| 512 |
+
}
|
| 513 |
+
incident_merge_seconds_ += elapsed(merge_started);
|
| 514 |
+
incident_seconds_ += elapsed(started);
|
| 515 |
+
return;
|
| 516 |
+
}
|
| 517 |
+
#endif
|
| 518 |
+
for (const auto& instance : instances) {
|
| 519 |
+
accumulate_one(instance, multiplier, deltas);
|
| 520 |
+
}
|
| 521 |
+
incident_seconds_ += elapsed(started);
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
void accumulate_one(const std::pair<int32_t, int64_t>& instance, int64_t multiplier,
|
| 525 |
+
ac2::CounterMap& deltas) const {
|
| 526 |
+
const int32_t token = instance.first;
|
| 527 |
+
const int64_t anchor = instance.second;
|
| 528 |
+
for (int32_t offset = 1; offset < lookahead_; ++offset) {
|
| 529 |
+
const int64_t forward = anchor + offset;
|
| 530 |
+
if (forward < total_anchors_) {
|
| 531 |
+
const int32_t* row = &member_[static_cast<size_t>(forward) * dimension_];
|
| 532 |
+
for (int32_t slot = 0; slot < dimension_; ++slot) {
|
| 533 |
+
const int32_t other = row[slot];
|
| 534 |
+
if (other >= 0 && legal(token, other, offset)) {
|
| 535 |
+
deltas.add(rule_key(token, other, offset), multiplier);
|
| 536 |
+
}
|
| 537 |
+
}
|
| 538 |
+
}
|
| 539 |
+
const int64_t backward = anchor - offset;
|
| 540 |
+
if (backward >= 0) {
|
| 541 |
+
const int32_t* row = &member_[static_cast<size_t>(backward) * dimension_];
|
| 542 |
+
for (int32_t slot = 0; slot < dimension_; ++slot) {
|
| 543 |
+
const int32_t other = row[slot];
|
| 544 |
+
if (other >= 0 && legal(other, token, offset)) {
|
| 545 |
+
deltas.add(rule_key(other, token, offset), multiplier);
|
| 546 |
+
}
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
}
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
// Incident aggregation sees a changed-to-changed edge once from each end.
|
| 553 |
+
void correct_internal(const std::vector<std::pair<int32_t, int64_t>>& instances,
|
| 554 |
+
int64_t correction, ac2::CounterMap& deltas) {
|
| 555 |
+
const auto started = std::chrono::steady_clock::now();
|
| 556 |
+
if (instances.empty()) return;
|
| 557 |
+
std::vector<size_t> groups{0};
|
| 558 |
+
groups.reserve(instances.size());
|
| 559 |
+
for (size_t index = 1; index < instances.size(); ++index) {
|
| 560 |
+
if (instances[index].second < instances[index - 1].second) {
|
| 561 |
+
throw std::runtime_error("changed temporal instances must be anchor-sorted");
|
| 562 |
+
}
|
| 563 |
+
if (instances[index].second != instances[index - 1].second) groups.push_back(index);
|
| 564 |
+
}
|
| 565 |
+
groups.push_back(instances.size());
|
| 566 |
+
const size_t group_count = groups.size() - 1;
|
| 567 |
+
#ifdef _OPENMP
|
| 568 |
+
if (threads_ > 1 && group_count >= 1024) {
|
| 569 |
+
const int workers = std::min<int>(threads_, static_cast<int>(group_count / 512));
|
| 570 |
+
const size_t hint = std::min(group_count * 2 / static_cast<size_t>(workers) + 64,
|
| 571 |
+
incident_key_capacity(token_count_));
|
| 572 |
+
std::vector<std::unique_ptr<ac2::CounterMap>> local;
|
| 573 |
+
local.reserve(workers);
|
| 574 |
+
for (int worker = 0; worker < workers; ++worker) {
|
| 575 |
+
local.push_back(std::make_unique<ac2::CounterMap>(hint));
|
| 576 |
+
}
|
| 577 |
+
const auto parallel_started = std::chrono::steady_clock::now();
|
| 578 |
+
#pragma omp parallel for schedule(static) num_threads(workers)
|
| 579 |
+
for (int64_t index = 0; index < static_cast<int64_t>(group_count); ++index) {
|
| 580 |
+
correct_group(static_cast<size_t>(index), groups, instances, correction,
|
| 581 |
+
*local[omp_get_thread_num()]);
|
| 582 |
+
}
|
| 583 |
+
correction_parallel_seconds_ += elapsed(parallel_started);
|
| 584 |
+
for (const auto& counter : local) {
|
| 585 |
+
counter->for_each([&](uint64_t key, int64_t value) { deltas.add(key, value); });
|
| 586 |
+
}
|
| 587 |
+
} else
|
| 588 |
+
#endif
|
| 589 |
+
{
|
| 590 |
+
for (size_t group = 0; group < group_count; ++group) {
|
| 591 |
+
correct_group(group, groups, instances, correction, deltas);
|
| 592 |
+
}
|
| 593 |
+
}
|
| 594 |
+
correction_seconds_ += elapsed(started);
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
void correct_group(size_t group, const std::vector<size_t>& groups,
|
| 598 |
+
const std::vector<std::pair<int32_t, int64_t>>& instances,
|
| 599 |
+
int64_t correction, ac2::CounterMap& deltas) const {
|
| 600 |
+
const size_t left_start = groups[group];
|
| 601 |
+
const size_t left_stop = groups[group + 1];
|
| 602 |
+
const int64_t anchor = instances[left_start].second;
|
| 603 |
+
for (size_t other = group + 1; other + 1 < groups.size(); ++other) {
|
| 604 |
+
const size_t right_start = groups[other];
|
| 605 |
+
const int64_t distance = instances[right_start].second - anchor;
|
| 606 |
+
if (distance >= lookahead_) break;
|
| 607 |
+
const int32_t offset = static_cast<int32_t>(distance);
|
| 608 |
+
for (size_t left_index = left_start; left_index < left_stop; ++left_index) {
|
| 609 |
+
const int32_t left = instances[left_index].first;
|
| 610 |
+
for (size_t right_index = right_start; right_index < groups[other + 1]; ++right_index) {
|
| 611 |
+
const int32_t right = instances[right_index].first;
|
| 612 |
+
if (legal(left, right, offset)) {
|
| 613 |
+
deltas.add(rule_key(left, right, offset), correction);
|
| 614 |
+
}
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
|
| 620 |
+
void apply(const ac2::CounterMap& deltas, uint64_t selected) {
|
| 621 |
+
const auto started = std::chrono::steady_clock::now();
|
| 622 |
+
deltas.for_each([&](uint64_t key, int64_t delta) {
|
| 623 |
+
const auto [previous, count] = counts_.add_with_previous(key, delta);
|
| 624 |
+
if (count < 0) {
|
| 625 |
+
int32_t bl = 0, br = 0, bo = 0;
|
| 626 |
+
unpack(key, bl, br, bo);
|
| 627 |
+
throw std::runtime_error("temporal edge counter became negative: rule=(" +
|
| 628 |
+
std::to_string(bl) + "," + std::to_string(br) + "," +
|
| 629 |
+
std::to_string(bo) + ") previous=" + std::to_string(previous) +
|
| 630 |
+
" delta=" + std::to_string(delta) + " at merge " +
|
| 631 |
+
std::to_string(merges_.size()));
|
| 632 |
+
}
|
| 633 |
+
counter_updates_ += delta < 0 ? -delta : delta;
|
| 634 |
+
int32_t left = 0;
|
| 635 |
+
int32_t right = 0;
|
| 636 |
+
int32_t offset = 0;
|
| 637 |
+
unpack(key, left, right, offset);
|
| 638 |
+
uint32_t version = 0;
|
| 639 |
+
if (left == right) {
|
| 640 |
+
version = self_version(key) + 1;
|
| 641 |
+
self_versions_[key] = version;
|
| 642 |
+
self_cache_.erase(key);
|
| 643 |
+
}
|
| 644 |
+
// Every edge a commit adds contains the freshly minted child token, so a
|
| 645 |
+
// pre-existing key can only lose mass; its stale heap priority stays an
|
| 646 |
+
// admissible upper bound and is repaired when popped. New keys and the
|
| 647 |
+
// key just consumed need an entry right away.
|
| 648 |
+
if (count > 0 && (previous == 0 || key == selected)) {
|
| 649 |
+
heap_.push_back({count, key, version, 0});
|
| 650 |
+
std::push_heap(heap_.begin(), heap_.end(), TemporalOrder{});
|
| 651 |
+
}
|
| 652 |
+
});
|
| 653 |
+
apply_seconds_ += elapsed(started);
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
int32_t base_vocab_;
|
| 657 |
+
int32_t dimension_;
|
| 658 |
+
int32_t lookahead_;
|
| 659 |
+
int64_t total_anchors_;
|
| 660 |
+
int threads_ = 1;
|
| 661 |
+
int32_t token_count_ = 0;
|
| 662 |
+
|
| 663 |
+
std::vector<uint64_t> masks_;
|
| 664 |
+
std::vector<int32_t> spans_;
|
| 665 |
+
std::vector<int32_t> root_anchor_;
|
| 666 |
+
std::vector<std::pair<int32_t, int32_t>> pattern_; // (time, spatial token)
|
| 667 |
+
std::vector<int32_t> pattern_start_;
|
| 668 |
+
|
| 669 |
+
std::vector<int32_t> member_;
|
| 670 |
+
std::vector<std::vector<int64_t>> occurrence_;
|
| 671 |
+
std::vector<uint8_t> dirty_;
|
| 672 |
+
|
| 673 |
+
ac2::CounterMap counts_;
|
| 674 |
+
std::vector<TemporalEntry> heap_;
|
| 675 |
+
std::unordered_map<uint64_t, uint32_t> self_versions_;
|
| 676 |
+
std::unordered_map<uint64_t, std::pair<uint32_t, std::vector<std::pair<int64_t, int64_t>>>>
|
| 677 |
+
self_cache_;
|
| 678 |
+
|
| 679 |
+
std::vector<std::array<int32_t, 3>> merges_;
|
| 680 |
+
int64_t active_count_ = 0;
|
| 681 |
+
int64_t initial_count_ = 0;
|
| 682 |
+
int64_t heap_pops_ = 0;
|
| 683 |
+
int64_t counter_updates_ = 0;
|
| 684 |
+
int64_t self_certifications_ = 0;
|
| 685 |
+
size_t initial_edge_types_ = 0;
|
| 686 |
+
double census_seconds_ = 0.0;
|
| 687 |
+
double incident_seconds_ = 0.0;
|
| 688 |
+
double incident_parallel_seconds_ = 0.0;
|
| 689 |
+
double incident_merge_seconds_ = 0.0;
|
| 690 |
+
double correction_seconds_ = 0.0;
|
| 691 |
+
double correction_parallel_seconds_ = 0.0;
|
| 692 |
+
double apply_seconds_ = 0.0;
|
| 693 |
+
};
|
| 694 |
+
|
| 695 |
+
} // namespace
|
| 696 |
+
|
| 697 |
+
py::tuple train_temporal_native(
|
| 698 |
+
py::iterable instance_chunks, int64_t total_anchors,
|
| 699 |
+
py::array_t<uint64_t, py::array::c_style | py::array::forcecast> spatial_support,
|
| 700 |
+
py::array_t<int32_t, py::array::c_style | py::array::forcecast> spatial_anchor,
|
| 701 |
+
int32_t dimension, int32_t lookahead, int64_t vocab_size, int64_t action_count, int threads) {
|
| 702 |
+
if (lookahead < 1) throw std::invalid_argument("lookahead must be positive");
|
| 703 |
+
const int32_t base_vocab = static_cast<int32_t>(spatial_support.size());
|
| 704 |
+
if (vocab_size < base_vocab) {
|
| 705 |
+
throw std::invalid_argument("vocab_size is below the spatial vocabulary size");
|
| 706 |
+
}
|
| 707 |
+
std::unique_ptr<Trainer> trainer;
|
| 708 |
+
{
|
| 709 |
+
py::gil_scoped_release release;
|
| 710 |
+
trainer = std::make_unique<Trainer>(
|
| 711 |
+
total_anchors, action_count, spatial_support.data(),
|
| 712 |
+
spatial_anchor.data(), base_vocab, dimension, lookahead, vocab_size, threads);
|
| 713 |
+
}
|
| 714 |
+
int64_t instance_count = 0;
|
| 715 |
+
for (py::handle raw_chunk : instance_chunks) {
|
| 716 |
+
py::tuple chunk = py::cast<py::tuple>(raw_chunk);
|
| 717 |
+
if (chunk.size() != 2) {
|
| 718 |
+
throw std::invalid_argument("every temporal instance chunk must be a token/anchor pair");
|
| 719 |
+
}
|
| 720 |
+
auto tokens = py::cast<
|
| 721 |
+
py::array_t<int32_t, py::array::c_style | py::array::forcecast>>(chunk[0]);
|
| 722 |
+
auto anchors = py::cast<
|
| 723 |
+
py::array_t<int64_t, py::array::c_style | py::array::forcecast>>(chunk[1]);
|
| 724 |
+
if (tokens.ndim() != 1 || anchors.ndim() != 1 || tokens.size() != anchors.size()) {
|
| 725 |
+
throw std::invalid_argument("every temporal token/anchor chunk must be an aligned vector");
|
| 726 |
+
}
|
| 727 |
+
for (py::ssize_t index = 0; index < tokens.size(); ++index) {
|
| 728 |
+
if (tokens.data()[index] < 0 || tokens.data()[index] >= base_vocab) {
|
| 729 |
+
throw std::invalid_argument("input temporal token is outside the spatial vocabulary");
|
| 730 |
+
}
|
| 731 |
+
}
|
| 732 |
+
const InstanceChunk view{tokens.data(), anchors.data(), tokens.size()};
|
| 733 |
+
{
|
| 734 |
+
py::gil_scoped_release release;
|
| 735 |
+
trainer->add_chunk(view);
|
| 736 |
+
}
|
| 737 |
+
instance_count += tokens.size();
|
| 738 |
+
}
|
| 739 |
+
if (instance_count <= 0) {
|
| 740 |
+
throw std::invalid_argument("temporal instance chunks must not be empty");
|
| 741 |
+
}
|
| 742 |
+
int32_t left = 0;
|
| 743 |
+
int32_t right = 0;
|
| 744 |
+
int32_t offset = 0;
|
| 745 |
+
std::vector<std::pair<int64_t, int64_t>> matches;
|
| 746 |
+
double selection_seconds = 0.0;
|
| 747 |
+
double commit_seconds = 0.0;
|
| 748 |
+
{
|
| 749 |
+
py::gil_scoped_release release;
|
| 750 |
+
trainer->finalize_instances();
|
| 751 |
+
while (static_cast<int64_t>(trainer->merges().size()) + base_vocab < vocab_size) {
|
| 752 |
+
auto started = std::chrono::steady_clock::now();
|
| 753 |
+
const bool found = trainer->best(left, right, offset, matches);
|
| 754 |
+
selection_seconds +=
|
| 755 |
+
std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
|
| 756 |
+
if (!found) break;
|
| 757 |
+
started = std::chrono::steady_clock::now();
|
| 758 |
+
trainer->commit(left, right, offset, matches);
|
| 759 |
+
commit_seconds +=
|
| 760 |
+
std::chrono::duration<double>(std::chrono::steady_clock::now() - started).count();
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
py::list merge_list;
|
| 765 |
+
for (const auto& merge : trainer->merges()) {
|
| 766 |
+
merge_list.append(py::make_tuple(merge[0], merge[1], merge[2]));
|
| 767 |
+
}
|
| 768 |
+
py::dict report;
|
| 769 |
+
report["lookahead"] = lookahead;
|
| 770 |
+
report["spatial_vocab_size"] = base_vocab;
|
| 771 |
+
report["merges"] = static_cast<int64_t>(trainer->merges().size());
|
| 772 |
+
report["vocab_size"] = base_vocab + static_cast<int64_t>(trainer->merges().size());
|
| 773 |
+
report["actions"] = action_count;
|
| 774 |
+
report["tokens_per_action_initial"] =
|
| 775 |
+
static_cast<float>(trainer->initial_count()) / static_cast<float>(action_count);
|
| 776 |
+
report["tokens_per_action"] =
|
| 777 |
+
static_cast<float>(trainer->active_count()) / static_cast<float>(action_count);
|
| 778 |
+
report["compression"] =
|
| 779 |
+
static_cast<float>(trainer->initial_count()) / static_cast<float>(trainer->active_count());
|
| 780 |
+
report["initial_edge_types"] = static_cast<int64_t>(trainer->initial_edge_types());
|
| 781 |
+
report["live_edge_types"] = static_cast<int64_t>(trainer->live_edge_types());
|
| 782 |
+
report["counter_buckets"] = static_cast<int64_t>(trainer->counter_buckets());
|
| 783 |
+
report["census_seconds"] = trainer->census_seconds();
|
| 784 |
+
report["incident_seconds"] = trainer->incident_seconds();
|
| 785 |
+
report["incident_parallel_seconds"] = trainer->incident_parallel_seconds();
|
| 786 |
+
report["incident_merge_seconds"] = trainer->incident_merge_seconds();
|
| 787 |
+
report["correction_seconds"] = trainer->correction_seconds();
|
| 788 |
+
report["correction_parallel_seconds"] = trainer->correction_parallel_seconds();
|
| 789 |
+
report["apply_seconds"] = trainer->apply_seconds();
|
| 790 |
+
report["selection_seconds"] = selection_seconds;
|
| 791 |
+
report["commit_seconds"] = commit_seconds;
|
| 792 |
+
report["heap_pops"] = trainer->heap_pops();
|
| 793 |
+
report["counter_updates"] = trainer->counter_updates();
|
| 794 |
+
report["self_certifications"] = trainer->self_certifications();
|
| 795 |
+
report["backend"] = "native_sparse_exact";
|
| 796 |
+
report["threads"] = trainer->threads();
|
| 797 |
+
report["threads_requested"] = threads;
|
| 798 |
+
return py::make_tuple(merge_list, report);
|
| 799 |
+
}
|
actioncodec2_kernels/token_streams.hpp
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#pragma once
|
| 2 |
+
|
| 3 |
+
#include <pybind11/pybind11.h>
|
| 4 |
+
#include <cstdint>
|
| 5 |
+
#include <stdexcept>
|
| 6 |
+
#include <string>
|
| 7 |
+
#include <vector>
|
| 8 |
+
|
| 9 |
+
namespace ac2 {
|
| 10 |
+
namespace py = pybind11;
|
| 11 |
+
|
| 12 |
+
// Preserve the public Python integer contract; never coerce floats or booleans.
|
| 13 |
+
inline std::vector<std::vector<int64_t>> checked_token_streams(
|
| 14 |
+
const py::iterable& streams, int64_t vocabulary, bool routed = false) {
|
| 15 |
+
const py::object numpy_integer = py::module_::import("numpy").attr("integer");
|
| 16 |
+
py::object integral;
|
| 17 |
+
std::vector<std::vector<int64_t>> output;
|
| 18 |
+
if (PyList_Check(streams.ptr())) output.reserve(PyList_GET_SIZE(streams.ptr()));
|
| 19 |
+
for (py::handle stream : streams) {
|
| 20 |
+
std::vector<int64_t> row;
|
| 21 |
+
if (PyList_Check(stream.ptr())) row.reserve(PyList_GET_SIZE(stream.ptr()));
|
| 22 |
+
for (py::handle raw : py::reinterpret_borrow<py::iterable>(stream)) {
|
| 23 |
+
bool integer = PyLong_Check(raw.ptr()) || py::isinstance(raw, numpy_integer);
|
| 24 |
+
if (!integer && !routed) {
|
| 25 |
+
if (!integral) integral = py::module_::import("numbers").attr("Integral");
|
| 26 |
+
integer = py::isinstance(raw, integral);
|
| 27 |
+
}
|
| 28 |
+
if (PyBool_Check(raw.ptr()) || !integer)
|
| 29 |
+
throw std::invalid_argument(routed ? "tokens[" + std::to_string(row.size()) + "] must be an integer"
|
| 30 |
+
: "tokens must be integers");
|
| 31 |
+
py::object number = py::reinterpret_steal<py::object>(PyNumber_Long(raw.ptr()));
|
| 32 |
+
if (!number) throw py::error_already_set();
|
| 33 |
+
int overflow = 0;
|
| 34 |
+
const int64_t token = PyLong_AsLongLongAndOverflow(number.ptr(), &overflow);
|
| 35 |
+
if (PyErr_Occurred()) throw py::error_already_set();
|
| 36 |
+
if (overflow || token < 0 || token >= vocabulary)
|
| 37 |
+
throw std::invalid_argument(routed ? "tokens[" + std::to_string(row.size()) +
|
| 38 |
+
"] is outside the routed profile vocabularies; out of range [0, " + std::to_string(vocabulary) + ")"
|
| 39 |
+
: "token is outside the vocabulary");
|
| 40 |
+
row.push_back(token);
|
| 41 |
+
}
|
| 42 |
+
if (row.empty()) throw std::invalid_argument("token stream must not be empty");
|
| 43 |
+
output.push_back(std::move(row));
|
| 44 |
+
}
|
| 45 |
+
if (output.empty()) throw std::invalid_argument("streams must not be empty");
|
| 46 |
+
return output;
|
| 47 |
+
}
|
| 48 |
+
} // namespace ac2
|
config.json
CHANGED
|
@@ -1,4 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"model_type": "actioncodec2",
|
| 3 |
-
"processor_class": "ActionCodec2"
|
|
|
|
|
|
|
|
|
|
| 4 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"model_type": "actioncodec2",
|
| 3 |
+
"processor_class": "ActionCodec2",
|
| 4 |
+
"auto_map": {
|
| 5 |
+
"AutoProcessor": "processing_actioncodec2.ActionCodec2"
|
| 6 |
+
}
|
| 7 |
}
|
processing_actioncodec2.py
CHANGED
|
@@ -21,11 +21,15 @@ SHARED_PATTERNS = (
|
|
| 21 |
|
| 22 |
def asset_inventory(directory: Path) -> dict[str, str]:
|
| 23 |
"""Hash the complete configuration and fitted assets for one variant."""
|
| 24 |
-
|
|
|
|
| 25 |
profiles = [name for name in ("eef", "joint") if (directory / "profiles" / name).is_dir()]
|
| 26 |
if not profiles:
|
| 27 |
raise ValueError("variant has no fitted profiles")
|
| 28 |
-
names.extend(f"profiles/{profile}/{name}" for profile in profiles
|
|
|
|
|
|
|
|
|
|
| 29 |
return {name: hashlib.sha256((directory / name).read_bytes()).hexdigest()
|
| 30 |
for name in sorted(names)}
|
| 31 |
|
|
@@ -62,11 +66,12 @@ class _VariantBinding:
|
|
| 62 |
|
| 63 |
def save_pretrained(self, save_directory, push_to_hub=False, **kwargs):
|
| 64 |
directory = Path(save_directory)
|
|
|
|
| 65 |
if (directory / "variants.json").is_file():
|
| 66 |
raise ValueError("save a selected variant to a separate directory, outside the catalog root")
|
| 67 |
if not push_to_hub and kwargs:
|
| 68 |
raise TypeError(f"unused save_pretrained keyword(s): {', '.join(sorted(kwargs))}")
|
| 69 |
-
files = super().save_pretrained(directory)
|
| 70 |
inventory = asset_inventory(directory)
|
| 71 |
manifest = directory / "artifact_manifest.json"
|
| 72 |
manifest.write_text(json.dumps({
|
|
@@ -77,10 +82,8 @@ class _VariantBinding:
|
|
| 77 |
}, indent=2) + "\n", encoding="utf-8")
|
| 78 |
files.append(str(manifest))
|
| 79 |
if push_to_hub:
|
| 80 |
-
package = type(self
|
| 81 |
-
importlib.import_module(f"{package}.
|
| 82 |
-
directory, **kwargs
|
| 83 |
-
)
|
| 84 |
return files
|
| 85 |
|
| 86 |
|
|
@@ -169,7 +172,7 @@ class ActionCodec2:
|
|
| 169 |
root = Path(snapshot_download(
|
| 170 |
str(pretrained_model_name_or_path),
|
| 171 |
allow_patterns=[*SHARED_PATTERNS, "router_config.yaml", "profiles/**",
|
| 172 |
-
"artifact_manifest.json"], **kwargs,
|
| 173 |
))
|
| 174 |
directory = root
|
| 175 |
manifest = root / "artifact_manifest.json"
|
|
@@ -185,33 +188,28 @@ class ActionCodec2:
|
|
| 185 |
if variant_fingerprint is not None and variant_fingerprint != origin.get("fingerprint"):
|
| 186 |
raise ValueError("variant fingerprint differs from the tokenizer bound to this policy")
|
| 187 |
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
if
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
|
|
|
| 203 |
spec.loader.exec_module(package)
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
raise
|
| 207 |
-
processor = importlib.import_module(f"{package_name}.processing_actioncodec2")
|
| 208 |
-
if (runtime / "hub_acceleration.py").is_file():
|
| 209 |
-
acceleration = importlib.import_module(f"{package_name}.hub_acceleration")
|
| 210 |
-
acceleration.install(package_name)
|
| 211 |
if not hasattr(processor, "_catalog_processor_class"):
|
| 212 |
processor._catalog_processor_class = type(
|
| 213 |
-
|
| 214 |
-
{"__module__": processor.__name__},
|
| 215 |
)
|
| 216 |
codec = processor._catalog_processor_class.from_pretrained(directory, action_space=action_space)
|
| 217 |
codec._variant_origin = origin
|
|
|
|
| 21 |
|
| 22 |
def asset_inventory(directory: Path) -> dict[str, str]:
|
| 23 |
"""Hash the complete configuration and fitted assets for one variant."""
|
| 24 |
+
sparse = (directory / "sparse_router_config.yaml").is_file()
|
| 25 |
+
names = ["sparse_router_config.yaml" if sparse else "router_config.yaml", "config.json", "processor_config.json"]
|
| 26 |
profiles = [name for name in ("eef", "joint") if (directory / "profiles" / name).is_dir()]
|
| 27 |
if not profiles:
|
| 28 |
raise ValueError("variant has no fitted profiles")
|
| 29 |
+
names.extend(f"profiles/{profile}/{name}" for profile in profiles
|
| 30 |
+
for name in (tuple("sparse_bpe.json" if n == "bpe.json" else n for n in PROFILE_FILES) if sparse else PROFILE_FILES))
|
| 31 |
+
names.extend(f"profiles/{profile}/preprocessing.json" for profile in profiles
|
| 32 |
+
if (directory / "profiles" / profile / "preprocessing.json").is_file())
|
| 33 |
return {name: hashlib.sha256((directory / name).read_bytes()).hexdigest()
|
| 34 |
for name in sorted(names)}
|
| 35 |
|
|
|
|
| 66 |
|
| 67 |
def save_pretrained(self, save_directory, push_to_hub=False, **kwargs):
|
| 68 |
directory = Path(save_directory)
|
| 69 |
+
include_training_report = kwargs.pop("include_training_report", False)
|
| 70 |
if (directory / "variants.json").is_file():
|
| 71 |
raise ValueError("save a selected variant to a separate directory, outside the catalog root")
|
| 72 |
if not push_to_hub and kwargs:
|
| 73 |
raise TypeError(f"unused save_pretrained keyword(s): {', '.join(sorted(kwargs))}")
|
| 74 |
+
files = super().save_pretrained(directory, include_training_report=include_training_report)
|
| 75 |
inventory = asset_inventory(directory)
|
| 76 |
manifest = directory / "artifact_manifest.json"
|
| 77 |
manifest.write_text(json.dumps({
|
|
|
|
| 82 |
}, indent=2) + "\n", encoding="utf-8")
|
| 83 |
files.append(str(manifest))
|
| 84 |
if push_to_hub:
|
| 85 |
+
package = type(self).__module__.split(".frontend.", 1)[0]
|
| 86 |
+
importlib.import_module(f"{package}.backends.actioncodec2.hf").upload_pretrained_directory(directory, **kwargs)
|
|
|
|
|
|
|
| 87 |
return files
|
| 88 |
|
| 89 |
|
|
|
|
| 172 |
root = Path(snapshot_download(
|
| 173 |
str(pretrained_model_name_or_path),
|
| 174 |
allow_patterns=[*SHARED_PATTERNS, "router_config.yaml", "profiles/**",
|
| 175 |
+
"sparse_router_config.yaml", "artifact_manifest.json"], **kwargs,
|
| 176 |
))
|
| 177 |
directory = root
|
| 178 |
manifest = root / "artifact_manifest.json"
|
|
|
|
| 188 |
if variant_fingerprint is not None and variant_fingerprint != origin.get("fingerprint"):
|
| 189 |
raise ValueError("variant fingerprint differs from the tokenizer bound to this policy")
|
| 190 |
|
| 191 |
+
family = "actioncodec2_sparse" if (directory / "sparse_router_config.yaml").is_file() else "actioncodec2"
|
| 192 |
+
if __package__ == "actioncodec2.core":
|
| 193 |
+
package_name = __package__
|
| 194 |
+
else:
|
| 195 |
+
runtime = root / "runtime"
|
| 196 |
+
package_file = runtime / "__init__.py"
|
| 197 |
+
if not package_file.is_file():
|
| 198 |
+
raise FileNotFoundError(f"ActionCodec2 runtime is missing from {root}")
|
| 199 |
+
identity = hashlib.sha256(str(runtime.resolve()).encode()).hexdigest()[:16]
|
| 200 |
+
package_name = f"_actioncodec2_artifact_{identity}"
|
| 201 |
+
if package_name not in sys.modules:
|
| 202 |
+
spec = importlib.util.spec_from_file_location(
|
| 203 |
+
package_name, package_file, submodule_search_locations=[str(runtime)]
|
| 204 |
+
)
|
| 205 |
+
package = importlib.util.module_from_spec(spec)
|
| 206 |
+
sys.modules[package_name] = package
|
| 207 |
spec.loader.exec_module(package)
|
| 208 |
+
processor = importlib.import_module(f"{package_name}.frontend.{family}.processor")
|
| 209 |
+
base = processor.ActionCodec2Sparse if family == "actioncodec2_sparse" else processor.ActionCodec2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
if not hasattr(processor, "_catalog_processor_class"):
|
| 211 |
processor._catalog_processor_class = type(
|
| 212 |
+
base.__name__, (_VariantBinding, base), {"__module__": processor.__name__}
|
|
|
|
| 213 |
)
|
| 214 |
codec = processor._catalog_processor_class.from_pretrained(directory, action_space=action_space)
|
| 215 |
codec._variant_origin = origin
|
pyproject.toml
CHANGED
|
@@ -1,16 +1,3 @@
|
|
| 1 |
[build-system]
|
| 2 |
requires = ["setuptools>=77", "wheel", "pybind11>=2.12"]
|
| 3 |
build-backend = "setuptools.build_meta"
|
| 4 |
-
|
| 5 |
-
[project]
|
| 6 |
-
name = "actioncodec2-kernels"
|
| 7 |
-
version = "0.1.0"
|
| 8 |
-
description = "Portable C++17 encode/decode kernels for ActionCodec2 tokenizer artifacts"
|
| 9 |
-
requires-python = ">=3.10"
|
| 10 |
-
license = "Apache-2.0"
|
| 11 |
-
|
| 12 |
-
[tool.setuptools]
|
| 13 |
-
packages = ["actioncodec2_kernels"]
|
| 14 |
-
|
| 15 |
-
[tool.setuptools.package-data]
|
| 16 |
-
actioncodec2_kernels = ["*.cpp", "*.hpp"]
|
|
|
|
| 1 |
[build-system]
|
| 2 |
requires = ["setuptools>=77", "wheel", "pybind11>=2.12"]
|
| 3 |
build-backend = "setuptools.build_meta"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
DELETED
|
@@ -1,6 +0,0 @@
|
|
| 1 |
-
numpy
|
| 2 |
-
scipy
|
| 3 |
-
torch>=2.3
|
| 4 |
-
transformers>=4.57,<6
|
| 5 |
-
huggingface-hub
|
| 6 |
-
pyyaml
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
runtime/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
"""
|
|
|
|
| 1 |
+
"""Shared ActionCodec2 and ActionCodec2Sparse runtime."""
|
runtime/backend/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
"""Internal backend components for ActionCodec2."""
|
|
|
|
|
|
runtime/backends/__init__.py
ADDED
|
File without changes
|
runtime/backends/actioncodec2/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared physical models, layouts, and dense and sparse routers."""
|
| 2 |
+
from .configuration import ActionCodec2Config, ConfigError
|
| 3 |
+
from .presets import ComponentSpec, PresetConfig
|
| 4 |
+
from .processor import ActionCodec2Processor
|
| 5 |
+
from .routing import ActionCodec2Router
|
| 6 |
+
from .trajectory import ActiveTrajectory
|
| 7 |
+
|
| 8 |
+
__all__ = ["ActionCodec2Config", "ConfigError", "ComponentSpec", "PresetConfig", "ActionCodec2Processor", "ActionCodec2Router", "ActiveTrajectory"]
|
runtime/{physical → backends/actioncodec2}/_so3.py
RENAMED
|
@@ -4,9 +4,22 @@ from __future__ import annotations
|
|
| 4 |
|
| 5 |
import numpy as np
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
def normalize_quaternion(values: np.ndarray) -> np.ndarray:
|
| 9 |
"""Normalize quaternion rows and reject an undefined zero rotation."""
|
|
|
|
|
|
|
|
|
|
| 10 |
quaternions = np.asarray(values, dtype=np.float64)
|
| 11 |
norms = np.linalg.norm(quaternions, axis=-1, keepdims=True)
|
| 12 |
if np.any(norms < 1e-12):
|
|
@@ -18,6 +31,10 @@ def multiply_quaternions(left: np.ndarray, right: np.ndarray) -> np.ndarray:
|
|
| 18 |
"""Return the Hamilton product of broadcastable xyzw arrays."""
|
| 19 |
first = np.asarray(left, dtype=np.float64)
|
| 20 |
second = np.asarray(right, dtype=np.float64)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
first_vector, first_scalar = first[..., :3], first[..., 3:4]
|
| 22 |
second_vector, second_scalar = second[..., :3], second[..., 3:4]
|
| 23 |
return normalize_quaternion(
|
|
@@ -36,6 +53,9 @@ def multiply_quaternions(left: np.ndarray, right: np.ndarray) -> np.ndarray:
|
|
| 36 |
|
| 37 |
def conjugate_quaternion(quaternion: np.ndarray) -> np.ndarray:
|
| 38 |
"""Return the conjugate of an xyzw quaternion array."""
|
|
|
|
|
|
|
|
|
|
| 39 |
result = np.asarray(quaternion, dtype=np.float64).copy()
|
| 40 |
result[..., :3] *= -1.0
|
| 41 |
return result
|
|
@@ -43,20 +63,24 @@ def conjugate_quaternion(quaternion: np.ndarray) -> np.ndarray:
|
|
| 43 |
|
| 44 |
def rotvec_to_quaternion(rotvec: np.ndarray) -> np.ndarray:
|
| 45 |
"""Convert rotation vectors to xyzw quaternions."""
|
|
|
|
|
|
|
|
|
|
| 46 |
values = np.asarray(rotvec, dtype=np.float64)
|
| 47 |
angle = np.linalg.norm(values, axis=-1)
|
| 48 |
scale = np.full_like(angle, 0.5)
|
| 49 |
regular = angle >= 1e-12
|
| 50 |
scale[regular] = np.sin(0.5 * angle[regular]) / angle[regular]
|
| 51 |
return normalize_quaternion(
|
| 52 |
-
np.concatenate(
|
| 53 |
-
(values * scale[..., None], np.cos(0.5 * angle)[..., None]), axis=-1
|
| 54 |
-
)
|
| 55 |
)
|
| 56 |
|
| 57 |
|
| 58 |
def quaternion_to_rotvec(quaternion: np.ndarray) -> np.ndarray:
|
| 59 |
"""Convert xyzw quaternions to shortest-path rotation vectors."""
|
|
|
|
|
|
|
|
|
|
| 60 |
values = normalize_quaternion(quaternion).copy()
|
| 61 |
values[values[..., 3] < 0.0] *= -1.0
|
| 62 |
vector_norm = np.linalg.norm(values[..., :3], axis=-1)
|
|
@@ -72,6 +96,9 @@ def euler_xyz_to_quaternion(euler: np.ndarray) -> np.ndarray:
|
|
| 72 |
values = np.asarray(euler, dtype=np.float64)
|
| 73 |
if values.ndim != 2 or values.shape[1] != 3:
|
| 74 |
raise ValueError("XYZ Euler angles must have shape [N, 3]")
|
|
|
|
|
|
|
|
|
|
| 75 |
half = 0.5 * values
|
| 76 |
sin_roll, sin_pitch, sin_yaw = np.sin(half).T
|
| 77 |
cos_roll, cos_pitch, cos_yaw = np.cos(half).T
|
|
@@ -88,6 +115,9 @@ def quaternion_to_euler_xyz(quaternion: np.ndarray) -> np.ndarray:
|
|
| 88 |
values = normalize_quaternion(quaternion)
|
| 89 |
if values.ndim != 2 or values.shape[1] != 4:
|
| 90 |
raise ValueError("xyzw quaternions must have shape [N, 4]")
|
|
|
|
|
|
|
|
|
|
| 91 |
x, y, z, w = values.T
|
| 92 |
return np.stack(
|
| 93 |
(
|
|
|
|
| 4 |
|
| 5 |
import numpy as np
|
| 6 |
|
| 7 |
+
from .native_backend import native_kernel
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def _mapped(values, operation, width):
|
| 11 |
+
kernel = native_kernel("quaternion_map")
|
| 12 |
+
if kernel is None:
|
| 13 |
+
return None
|
| 14 |
+
array = np.asarray(values, dtype=np.float64)
|
| 15 |
+
return kernel(array.reshape(-1, array.shape[-1]), operation).reshape(*array.shape[:-1], width)
|
| 16 |
+
|
| 17 |
|
| 18 |
def normalize_quaternion(values: np.ndarray) -> np.ndarray:
|
| 19 |
"""Normalize quaternion rows and reject an undefined zero rotation."""
|
| 20 |
+
result = _mapped(values, "normalize", 4)
|
| 21 |
+
if result is not None:
|
| 22 |
+
return result
|
| 23 |
quaternions = np.asarray(values, dtype=np.float64)
|
| 24 |
norms = np.linalg.norm(quaternions, axis=-1, keepdims=True)
|
| 25 |
if np.any(norms < 1e-12):
|
|
|
|
| 31 |
"""Return the Hamilton product of broadcastable xyzw arrays."""
|
| 32 |
first = np.asarray(left, dtype=np.float64)
|
| 33 |
second = np.asarray(right, dtype=np.float64)
|
| 34 |
+
kernel = native_kernel("quaternion_multiply")
|
| 35 |
+
if kernel is not None:
|
| 36 |
+
first, second = np.broadcast_arrays(first, second)
|
| 37 |
+
return kernel(first.reshape(-1, 4), second.reshape(-1, 4)).reshape(first.shape)
|
| 38 |
first_vector, first_scalar = first[..., :3], first[..., 3:4]
|
| 39 |
second_vector, second_scalar = second[..., :3], second[..., 3:4]
|
| 40 |
return normalize_quaternion(
|
|
|
|
| 53 |
|
| 54 |
def conjugate_quaternion(quaternion: np.ndarray) -> np.ndarray:
|
| 55 |
"""Return the conjugate of an xyzw quaternion array."""
|
| 56 |
+
native = _mapped(quaternion, "conjugate", 4)
|
| 57 |
+
if native is not None:
|
| 58 |
+
return native
|
| 59 |
result = np.asarray(quaternion, dtype=np.float64).copy()
|
| 60 |
result[..., :3] *= -1.0
|
| 61 |
return result
|
|
|
|
| 63 |
|
| 64 |
def rotvec_to_quaternion(rotvec: np.ndarray) -> np.ndarray:
|
| 65 |
"""Convert rotation vectors to xyzw quaternions."""
|
| 66 |
+
result = _mapped(rotvec, "from_rotvec", 4)
|
| 67 |
+
if result is not None:
|
| 68 |
+
return result
|
| 69 |
values = np.asarray(rotvec, dtype=np.float64)
|
| 70 |
angle = np.linalg.norm(values, axis=-1)
|
| 71 |
scale = np.full_like(angle, 0.5)
|
| 72 |
regular = angle >= 1e-12
|
| 73 |
scale[regular] = np.sin(0.5 * angle[regular]) / angle[regular]
|
| 74 |
return normalize_quaternion(
|
| 75 |
+
np.concatenate((values * scale[..., None], np.cos(0.5 * angle)[..., None]), axis=-1)
|
|
|
|
|
|
|
| 76 |
)
|
| 77 |
|
| 78 |
|
| 79 |
def quaternion_to_rotvec(quaternion: np.ndarray) -> np.ndarray:
|
| 80 |
"""Convert xyzw quaternions to shortest-path rotation vectors."""
|
| 81 |
+
result = _mapped(quaternion, "to_rotvec", 3)
|
| 82 |
+
if result is not None:
|
| 83 |
+
return result
|
| 84 |
values = normalize_quaternion(quaternion).copy()
|
| 85 |
values[values[..., 3] < 0.0] *= -1.0
|
| 86 |
vector_norm = np.linalg.norm(values[..., :3], axis=-1)
|
|
|
|
| 96 |
values = np.asarray(euler, dtype=np.float64)
|
| 97 |
if values.ndim != 2 or values.shape[1] != 3:
|
| 98 |
raise ValueError("XYZ Euler angles must have shape [N, 3]")
|
| 99 |
+
result = _mapped(values, "from_euler", 4)
|
| 100 |
+
if result is not None:
|
| 101 |
+
return result
|
| 102 |
half = 0.5 * values
|
| 103 |
sin_roll, sin_pitch, sin_yaw = np.sin(half).T
|
| 104 |
cos_roll, cos_pitch, cos_yaw = np.cos(half).T
|
|
|
|
| 115 |
values = normalize_quaternion(quaternion)
|
| 116 |
if values.ndim != 2 or values.shape[1] != 4:
|
| 117 |
raise ValueError("xyzw quaternions must have shape [N, 4]")
|
| 118 |
+
result = _mapped(values, "to_euler", 3)
|
| 119 |
+
if result is not None:
|
| 120 |
+
return result
|
| 121 |
x, y, z, w = values.T
|
| 122 |
return np.stack(
|
| 123 |
(
|
runtime/{common → backends/actioncodec2}/_validation.py
RENAMED
|
@@ -18,13 +18,11 @@ def active_dimensions(
|
|
| 18 |
) -> tuple[int, ...]:
|
| 19 |
"""Normalize and validate a non-empty increasing subset of dimensions."""
|
| 20 |
dims = (
|
| 21 |
-
tuple(range(dimension))
|
| 22 |
-
if active_dims is None
|
| 23 |
-
else tuple(int(item) for item in active_dims)
|
| 24 |
)
|
| 25 |
if not dims:
|
| 26 |
raise ValueError(f"{subject} must not be empty")
|
| 27 |
-
if any(left >= right for left, right in zip(dims, dims[1:])):
|
| 28 |
raise ValueError(f"{subject} must be strictly increasing")
|
| 29 |
if dims[0] < 0 or dims[-1] >= dimension:
|
| 30 |
raise ValueError(f"{subject} contains a dimension outside the vocabulary")
|
|
@@ -76,13 +74,9 @@ def validate_dimension_partition(
|
|
| 76 |
for name, dims in groups:
|
| 77 |
for index in dims:
|
| 78 |
if index < 0 or index >= dimension:
|
| 79 |
-
raise error_type(
|
| 80 |
-
f"{label} {index} used by {name!r} is outside input_dim"
|
| 81 |
-
)
|
| 82 |
if index in owners:
|
| 83 |
-
raise error_type(
|
| 84 |
-
f"{label} {index} belongs to both {owners[index]!r} and {name!r}"
|
| 85 |
-
)
|
| 86 |
owners[index] = name
|
| 87 |
missing = sorted(set(range(dimension)) - set(owners))
|
| 88 |
if missing:
|
|
|
|
| 18 |
) -> tuple[int, ...]:
|
| 19 |
"""Normalize and validate a non-empty increasing subset of dimensions."""
|
| 20 |
dims = (
|
| 21 |
+
tuple(range(dimension)) if active_dims is None else tuple(int(item) for item in active_dims)
|
|
|
|
|
|
|
| 22 |
)
|
| 23 |
if not dims:
|
| 24 |
raise ValueError(f"{subject} must not be empty")
|
| 25 |
+
if any(left >= right for left, right in zip(dims, dims[1:], strict=False)):
|
| 26 |
raise ValueError(f"{subject} must be strictly increasing")
|
| 27 |
if dims[0] < 0 or dims[-1] >= dimension:
|
| 28 |
raise ValueError(f"{subject} contains a dimension outside the vocabulary")
|
|
|
|
| 74 |
for name, dims in groups:
|
| 75 |
for index in dims:
|
| 76 |
if index < 0 or index >= dimension:
|
| 77 |
+
raise error_type(f"{label} {index} used by {name!r} is outside input_dim")
|
|
|
|
|
|
|
| 78 |
if index in owners:
|
| 79 |
+
raise error_type(f"{label} {index} belongs to both {owners[index]!r} and {name!r}")
|
|
|
|
|
|
|
| 80 |
owners[index] = name
|
| 81 |
missing = sorted(set(range(dimension)) - set(owners))
|
| 82 |
if missing:
|
runtime/backends/actioncodec2/assets/presets/dual_eef_abs.yaml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: dual_eef_abs
|
| 2 |
+
input_dim: 14
|
| 3 |
+
input: absolute
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
left_arm:
|
| 10 |
+
space: eef
|
| 11 |
+
position_dims: [0, 1, 2]
|
| 12 |
+
rotation_dims: [3, 4, 5]
|
| 13 |
+
rotation_format: rotation_vector
|
| 14 |
+
gripper_dim: 6
|
| 15 |
+
right_arm:
|
| 16 |
+
space: eef
|
| 17 |
+
position_dims: [7, 8, 9]
|
| 18 |
+
rotation_dims: [10, 11, 12]
|
| 19 |
+
rotation_format: rotation_vector
|
| 20 |
+
gripper_dim: 13
|
runtime/backends/actioncodec2/assets/presets/dual_eef_delta.yaml
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: dual_eef_delta
|
| 2 |
+
input_dim: 14
|
| 3 |
+
input: delta
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
left_arm:
|
| 10 |
+
space: eef
|
| 11 |
+
position_dims: [0, 1, 2]
|
| 12 |
+
rotation_dims: [3, 4, 5]
|
| 13 |
+
rotation_format: rotation_vector
|
| 14 |
+
gripper_dim: 6
|
| 15 |
+
right_arm:
|
| 16 |
+
space: eef
|
| 17 |
+
position_dims: [7, 8, 9]
|
| 18 |
+
rotation_dims: [10, 11, 12]
|
| 19 |
+
rotation_format: rotation_vector
|
| 20 |
+
gripper_dim: 13
|
runtime/backends/actioncodec2/assets/presets/dual_joint6_abs.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: dual_joint6_abs
|
| 2 |
+
input_dim: 14
|
| 3 |
+
input: absolute
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
left_arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5]
|
| 12 |
+
joint_wrap: pi
|
| 13 |
+
gripper_dim: 6
|
| 14 |
+
right_arm:
|
| 15 |
+
space: joint
|
| 16 |
+
joint_dims: [7, 8, 9, 10, 11, 12]
|
| 17 |
+
joint_wrap: pi
|
| 18 |
+
gripper_dim: 13
|
runtime/backends/actioncodec2/assets/presets/dual_joint6_delta.yaml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: dual_joint6_delta
|
| 2 |
+
input_dim: 14
|
| 3 |
+
input: delta
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
left_arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5]
|
| 12 |
+
gripper_dim: 6
|
| 13 |
+
right_arm:
|
| 14 |
+
space: joint
|
| 15 |
+
joint_dims: [7, 8, 9, 10, 11, 12]
|
| 16 |
+
gripper_dim: 13
|
runtime/backends/actioncodec2/assets/presets/dual_joint7_abs.yaml
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: dual_joint7_abs
|
| 2 |
+
input_dim: 16
|
| 3 |
+
input: absolute
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
left_arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5, 6]
|
| 12 |
+
joint_wrap: pi
|
| 13 |
+
gripper_dim: 7
|
| 14 |
+
right_arm:
|
| 15 |
+
space: joint
|
| 16 |
+
joint_dims: [8, 9, 10, 11, 12, 13, 14]
|
| 17 |
+
joint_wrap: pi
|
| 18 |
+
gripper_dim: 15
|
runtime/backends/actioncodec2/assets/presets/dual_joint7_delta.yaml
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: dual_joint7_delta
|
| 2 |
+
input_dim: 16
|
| 3 |
+
input: delta
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
left_arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5, 6]
|
| 12 |
+
gripper_dim: 7
|
| 13 |
+
right_arm:
|
| 14 |
+
space: joint
|
| 15 |
+
joint_dims: [8, 9, 10, 11, 12, 13, 14]
|
| 16 |
+
gripper_dim: 15
|
runtime/backends/actioncodec2/assets/presets/single_eef_abs.yaml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: single_eef_abs
|
| 2 |
+
input_dim: 7
|
| 3 |
+
input: absolute
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
arm:
|
| 10 |
+
space: eef
|
| 11 |
+
position_dims: [0, 1, 2]
|
| 12 |
+
rotation_dims: [3, 4, 5]
|
| 13 |
+
rotation_format: rotation_vector
|
| 14 |
+
gripper_dim: 6
|
runtime/backends/actioncodec2/assets/presets/single_eef_delta.yaml
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: single_eef_delta
|
| 2 |
+
input_dim: 7
|
| 3 |
+
input: delta
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
arm:
|
| 10 |
+
space: eef
|
| 11 |
+
position_dims: [0, 1, 2]
|
| 12 |
+
rotation_dims: [3, 4, 5]
|
| 13 |
+
rotation_format: rotation_vector
|
| 14 |
+
gripper_dim: 6
|
runtime/backends/actioncodec2/assets/presets/single_joint6_abs.yaml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: single_joint6_abs
|
| 2 |
+
input_dim: 7
|
| 3 |
+
input: absolute
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5]
|
| 12 |
+
joint_wrap: pi
|
| 13 |
+
gripper_dim: 6
|
runtime/backends/actioncodec2/assets/presets/single_joint6_abs_no_gripper.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: single_joint6_abs_no_gripper
|
| 2 |
+
input_dim: 6
|
| 3 |
+
input: absolute
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5]
|
| 12 |
+
joint_wrap: pi
|
runtime/backends/actioncodec2/assets/presets/single_joint6_delta.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: single_joint6_delta
|
| 2 |
+
input_dim: 7
|
| 3 |
+
input: delta
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5]
|
| 12 |
+
gripper_dim: 6
|
runtime/backends/actioncodec2/assets/presets/single_joint7_abs.yaml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: single_joint7_abs
|
| 2 |
+
input_dim: 8
|
| 3 |
+
input: absolute
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5, 6]
|
| 12 |
+
joint_wrap: pi
|
| 13 |
+
gripper_dim: 7
|
runtime/backends/actioncodec2/assets/presets/single_joint7_delta.yaml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: single_joint7_delta
|
| 2 |
+
input_dim: 8
|
| 3 |
+
input: delta
|
| 4 |
+
source_hz: 15.0
|
| 5 |
+
target_hz: 15.0
|
| 6 |
+
interpolation: cubic
|
| 7 |
+
token_order: time_major
|
| 8 |
+
components:
|
| 9 |
+
arm:
|
| 10 |
+
space: joint
|
| 11 |
+
joint_dims: [0, 1, 2, 3, 4, 5, 6]
|
| 12 |
+
gripper_dim: 7
|
runtime/backends/actioncodec2/assets/profiles/eef.yaml
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_type: actioncodec2_unified
|
| 2 |
+
config_version: 1
|
| 3 |
+
physical:
|
| 4 |
+
input_dim: 7
|
| 5 |
+
components:
|
| 6 |
+
translation_m:
|
| 7 |
+
dims:
|
| 8 |
+
start: 0
|
| 9 |
+
stop: 3
|
| 10 |
+
space: additive
|
| 11 |
+
input_order: 0
|
| 12 |
+
primitive_order: 1
|
| 13 |
+
input_transform:
|
| 14 |
+
scale: 1.0
|
| 15 |
+
clip: false
|
| 16 |
+
constraints:
|
| 17 |
+
order_0:
|
| 18 |
+
min: -2.0
|
| 19 |
+
max: 2.0
|
| 20 |
+
order_1:
|
| 21 |
+
min: -0.08
|
| 22 |
+
max: 0.08
|
| 23 |
+
distortion_limit: 0.005
|
| 24 |
+
primitive_selection: smooth
|
| 25 |
+
orientation_so3:
|
| 26 |
+
dims:
|
| 27 |
+
start: 3
|
| 28 |
+
stop: 6
|
| 29 |
+
space: so3
|
| 30 |
+
input_order: 0
|
| 31 |
+
primitive_order: 1
|
| 32 |
+
input_transform:
|
| 33 |
+
scale: 1.0
|
| 34 |
+
clip: false
|
| 35 |
+
constraints:
|
| 36 |
+
order_0:
|
| 37 |
+
min: -3.141592653589793
|
| 38 |
+
max: 3.141592653589793
|
| 39 |
+
order_1:
|
| 40 |
+
min: -0.25
|
| 41 |
+
max: 0.25
|
| 42 |
+
distortion_limit: 0.02
|
| 43 |
+
primitive_selection: smooth
|
| 44 |
+
representation: rotation_vector
|
| 45 |
+
multiplication: right
|
| 46 |
+
gripper_open_command:
|
| 47 |
+
dims:
|
| 48 |
+
- 6
|
| 49 |
+
space: additive
|
| 50 |
+
input_order: 0
|
| 51 |
+
primitive_order: 0
|
| 52 |
+
input_transform:
|
| 53 |
+
scale: 1.0
|
| 54 |
+
clip: true
|
| 55 |
+
constraints:
|
| 56 |
+
order_0:
|
| 57 |
+
min: 0.0
|
| 58 |
+
max: 1.0
|
| 59 |
+
distortion_limit: 0.5
|
| 60 |
+
primitive_selection: nearest
|
| 61 |
+
set_bpe:
|
| 62 |
+
lookahead: 8
|
| 63 |
+
allow_cross_component_merge: false
|
| 64 |
+
train:
|
| 65 |
+
primitives:
|
| 66 |
+
zero_primitive_components:
|
| 67 |
+
- translation_m
|
| 68 |
+
- orientation_so3
|
| 69 |
+
bpe:
|
| 70 |
+
spatial_vocab_size: 2048
|
| 71 |
+
vocab_size: 4096
|
| 72 |
+
quality_control:
|
| 73 |
+
hard_order_1_scale: 3.0
|
| 74 |
+
min_segment_length: 2
|
runtime/backends/actioncodec2/assets/profiles/eef_second_order.yaml
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_type: actioncodec2_unified
|
| 2 |
+
config_version: 1
|
| 3 |
+
physical:
|
| 4 |
+
input_dim: 7
|
| 5 |
+
components:
|
| 6 |
+
translation_m:
|
| 7 |
+
dims:
|
| 8 |
+
start: 0
|
| 9 |
+
stop: 3
|
| 10 |
+
space: additive
|
| 11 |
+
input_order: 0
|
| 12 |
+
primitive_order: 2
|
| 13 |
+
input_transform:
|
| 14 |
+
scale: 1.0
|
| 15 |
+
clip: false
|
| 16 |
+
constraints:
|
| 17 |
+
order_0:
|
| 18 |
+
min: -2.0
|
| 19 |
+
max: 2.0
|
| 20 |
+
order_2:
|
| 21 |
+
min: -0.025
|
| 22 |
+
max: 0.025
|
| 23 |
+
order_1:
|
| 24 |
+
min: -0.08
|
| 25 |
+
max: 0.08
|
| 26 |
+
distortion_limit: 0.005
|
| 27 |
+
primitive_selection: smooth
|
| 28 |
+
orientation_so3:
|
| 29 |
+
dims:
|
| 30 |
+
start: 3
|
| 31 |
+
stop: 6
|
| 32 |
+
space: so3
|
| 33 |
+
input_order: 0
|
| 34 |
+
primitive_order: 2
|
| 35 |
+
input_transform:
|
| 36 |
+
scale: 1.0
|
| 37 |
+
clip: false
|
| 38 |
+
constraints:
|
| 39 |
+
order_0:
|
| 40 |
+
min: -3.141592653589793
|
| 41 |
+
max: 3.141592653589793
|
| 42 |
+
order_2:
|
| 43 |
+
min: -0.08
|
| 44 |
+
max: 0.08
|
| 45 |
+
order_1:
|
| 46 |
+
min: -0.25
|
| 47 |
+
max: 0.25
|
| 48 |
+
distortion_limit: 0.02
|
| 49 |
+
primitive_selection: smooth
|
| 50 |
+
representation: rotation_vector
|
| 51 |
+
multiplication: right
|
| 52 |
+
gripper_open_command:
|
| 53 |
+
dims:
|
| 54 |
+
- 6
|
| 55 |
+
space: additive
|
| 56 |
+
input_order: 0
|
| 57 |
+
primitive_order: 0
|
| 58 |
+
input_transform:
|
| 59 |
+
scale: 1.0
|
| 60 |
+
clip: true
|
| 61 |
+
constraints:
|
| 62 |
+
order_0:
|
| 63 |
+
min: 0.0
|
| 64 |
+
max: 1.0
|
| 65 |
+
distortion_limit: 0.5
|
| 66 |
+
primitive_selection: nearest
|
| 67 |
+
set_bpe:
|
| 68 |
+
lookahead: 8
|
| 69 |
+
allow_cross_component_merge: false
|
| 70 |
+
train:
|
| 71 |
+
primitives:
|
| 72 |
+
zero_primitive_components:
|
| 73 |
+
- translation_m
|
| 74 |
+
- orientation_so3
|
| 75 |
+
bpe:
|
| 76 |
+
spatial_vocab_size: 2048
|
| 77 |
+
vocab_size: 4096
|
| 78 |
+
quality_control:
|
| 79 |
+
hard_order_1_scale: 3.0
|
| 80 |
+
min_segment_length: 2
|
runtime/backends/actioncodec2/assets/profiles/joint.yaml
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_type: actioncodec2_unified
|
| 2 |
+
config_version: 1
|
| 3 |
+
physical:
|
| 4 |
+
input_dim: 8
|
| 5 |
+
components:
|
| 6 |
+
generalized_joint_position:
|
| 7 |
+
dims:
|
| 8 |
+
start: 0
|
| 9 |
+
stop: 7
|
| 10 |
+
space: additive
|
| 11 |
+
input_order: 0
|
| 12 |
+
primitive_order: 1
|
| 13 |
+
input_transform:
|
| 14 |
+
scale: 1.0
|
| 15 |
+
clip: false
|
| 16 |
+
constraints:
|
| 17 |
+
order_0:
|
| 18 |
+
min: -6.283185307179586
|
| 19 |
+
max: 6.283185307179586
|
| 20 |
+
order_1:
|
| 21 |
+
min: -0.35
|
| 22 |
+
max: 0.35
|
| 23 |
+
distortion_limit: [0.004, 0.004, 0.006, 0.0075, 0.015, 0.020, 0.020]
|
| 24 |
+
primitive_selection: smooth
|
| 25 |
+
gripper_open_command:
|
| 26 |
+
dims:
|
| 27 |
+
- 7
|
| 28 |
+
space: additive
|
| 29 |
+
input_order: 0
|
| 30 |
+
primitive_order: 0
|
| 31 |
+
input_transform:
|
| 32 |
+
scale: 1.0
|
| 33 |
+
clip: true
|
| 34 |
+
constraints:
|
| 35 |
+
order_0:
|
| 36 |
+
min: 0.0
|
| 37 |
+
max: 1.0
|
| 38 |
+
distortion_limit: 0.5
|
| 39 |
+
primitive_selection: nearest
|
| 40 |
+
set_bpe:
|
| 41 |
+
lookahead: 8
|
| 42 |
+
allow_cross_component_merge: false
|
| 43 |
+
train:
|
| 44 |
+
primitives:
|
| 45 |
+
zero_primitive_components:
|
| 46 |
+
- generalized_joint_position
|
| 47 |
+
bpe:
|
| 48 |
+
spatial_vocab_size: 2048
|
| 49 |
+
vocab_size: 4096
|
| 50 |
+
quality_control:
|
| 51 |
+
hard_order_1_scale: 3.0
|
| 52 |
+
min_segment_length: 2
|
runtime/backends/actioncodec2/assets/profiles/joint_second_order.yaml
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model_type: actioncodec2_unified
|
| 2 |
+
config_version: 1
|
| 3 |
+
physical:
|
| 4 |
+
input_dim: 8
|
| 5 |
+
components:
|
| 6 |
+
generalized_joint_position:
|
| 7 |
+
dims:
|
| 8 |
+
start: 0
|
| 9 |
+
stop: 7
|
| 10 |
+
space: additive
|
| 11 |
+
input_order: 0
|
| 12 |
+
primitive_order: 2
|
| 13 |
+
input_transform:
|
| 14 |
+
scale: 1.0
|
| 15 |
+
clip: false
|
| 16 |
+
constraints:
|
| 17 |
+
order_0:
|
| 18 |
+
min: -6.283185307179586
|
| 19 |
+
max: 6.283185307179586
|
| 20 |
+
order_2:
|
| 21 |
+
min: -0.25
|
| 22 |
+
max: 0.25
|
| 23 |
+
order_1:
|
| 24 |
+
min: -0.35
|
| 25 |
+
max: 0.35
|
| 26 |
+
distortion_limit:
|
| 27 |
+
- 0.004
|
| 28 |
+
- 0.004
|
| 29 |
+
- 0.006
|
| 30 |
+
- 0.0075
|
| 31 |
+
- 0.015
|
| 32 |
+
- 0.02
|
| 33 |
+
- 0.02
|
| 34 |
+
primitive_selection: smooth
|
| 35 |
+
gripper_open_command:
|
| 36 |
+
dims:
|
| 37 |
+
- 7
|
| 38 |
+
space: additive
|
| 39 |
+
input_order: 0
|
| 40 |
+
primitive_order: 0
|
| 41 |
+
input_transform:
|
| 42 |
+
scale: 1.0
|
| 43 |
+
clip: true
|
| 44 |
+
constraints:
|
| 45 |
+
order_0:
|
| 46 |
+
min: 0.0
|
| 47 |
+
max: 1.0
|
| 48 |
+
distortion_limit: 0.5
|
| 49 |
+
primitive_selection: nearest
|
| 50 |
+
set_bpe:
|
| 51 |
+
lookahead: 8
|
| 52 |
+
allow_cross_component_merge: false
|
| 53 |
+
train:
|
| 54 |
+
primitives:
|
| 55 |
+
zero_primitive_components:
|
| 56 |
+
- generalized_joint_position
|
| 57 |
+
bpe:
|
| 58 |
+
spatial_vocab_size: 2048
|
| 59 |
+
vocab_size: 4096
|
| 60 |
+
quality_control:
|
| 61 |
+
hard_order_1_scale: 3.0
|
| 62 |
+
min_segment_length: 2
|
runtime/backends/actioncodec2/batch_semantics.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical adapters shared by episode fitting and batched inference."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
import numpy as np
|
| 8 |
+
|
| 9 |
+
from ._so3 import (
|
| 10 |
+
conjugate_quaternion,
|
| 11 |
+
multiply_quaternions,
|
| 12 |
+
normalize_quaternion,
|
| 13 |
+
quaternion_to_rotvec,
|
| 14 |
+
)
|
| 15 |
+
from .native_backend import native_kernel
|
| 16 |
+
from .semantic import (
|
| 17 |
+
difference_additive,
|
| 18 |
+
eef_pose_absolute,
|
| 19 |
+
eef_pose_components,
|
| 20 |
+
eef_pose_deltas,
|
| 21 |
+
eef_pose_from_components,
|
| 22 |
+
integrate_additive,
|
| 23 |
+
integrate_eef_deltas,
|
| 24 |
+
normalize_gripper_actions,
|
| 25 |
+
resample_uniform,
|
| 26 |
+
wrap_to_pi,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
if TYPE_CHECKING:
|
| 30 |
+
from .presets import ComponentSpec, PresetConfig
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def _resample(values, source_hz, target_hz, method, *, kind="linear", target_length=None):
|
| 34 |
+
source_hz, target_hz = float(source_hz), float(target_hz)
|
| 35 |
+
if source_hz == target_hz and target_length in (None, values.shape[1]):
|
| 36 |
+
return values
|
| 37 |
+
kernel = native_kernel("resample_uniform_batch")
|
| 38 |
+
if kernel is not None:
|
| 39 |
+
return kernel(
|
| 40 |
+
values,
|
| 41 |
+
source_hz,
|
| 42 |
+
target_hz,
|
| 43 |
+
kind,
|
| 44 |
+
-1 if target_length is None else target_length,
|
| 45 |
+
method,
|
| 46 |
+
)
|
| 47 |
+
return np.stack(
|
| 48 |
+
[
|
| 49 |
+
resample_uniform(
|
| 50 |
+
row,
|
| 51 |
+
source_hz=source_hz,
|
| 52 |
+
target_hz=target_hz,
|
| 53 |
+
kind=kind,
|
| 54 |
+
target_length=target_length,
|
| 55 |
+
method=method,
|
| 56 |
+
)
|
| 57 |
+
for row in values
|
| 58 |
+
]
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _pose_map(values, kernel_name, reference):
|
| 63 |
+
kernel = native_kernel(kernel_name)
|
| 64 |
+
return kernel(values) if kernel is not None else np.stack([reference(row) for row in values])
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _rotvec_poses(values):
|
| 68 |
+
kernel = native_kernel("eef_pose_from_rotvec_batch")
|
| 69 |
+
if kernel is not None:
|
| 70 |
+
return kernel(values)
|
| 71 |
+
flat = values.reshape(-1, 6)
|
| 72 |
+
return eef_pose_from_components(flat[:, :3], flat[:, 3:], "rotation_vector").reshape(
|
| 73 |
+
*values.shape[:2], 7
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def _poses(values: np.ndarray, component: ComponentSpec) -> np.ndarray:
|
| 78 |
+
position = np.take(values, component.position_dims, axis=-1)
|
| 79 |
+
rotation = np.take(values, component.rotation_dims, axis=-1)
|
| 80 |
+
if component.rotation_format == "rotation_vector":
|
| 81 |
+
return _rotvec_poses(np.concatenate((position, rotation), axis=-1))
|
| 82 |
+
return eef_pose_from_components(
|
| 83 |
+
position.reshape(-1, 3), rotation.reshape(-1, rotation.shape[-1]), component.rotation_format
|
| 84 |
+
).reshape(*values.shape[:2], 7)
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _local(poses, anchor):
|
| 88 |
+
quaternion = normalize_quaternion(poses[..., 3:7])
|
| 89 |
+
inverse = conjugate_quaternion(normalize_quaternion(anchor[..., 3:7]))
|
| 90 |
+
relative = multiply_quaternions(inverse, quaternion)
|
| 91 |
+
return np.concatenate(
|
| 92 |
+
(poses[..., :3].astype(np.float64) - anchor[..., :3], quaternion_to_rotvec(relative)),
|
| 93 |
+
axis=-1,
|
| 94 |
+
).astype(np.float32)
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _joint(preset, values, component, source_hz, current_state):
|
| 98 |
+
source = np.take(values, component.joint_dims, axis=-1)
|
| 99 |
+
if preset.input == "delta":
|
| 100 |
+
source = integrate_additive(source, 1)
|
| 101 |
+
anchor = np.zeros((len(source), 1, source.shape[-1]), np.float32)
|
| 102 |
+
else:
|
| 103 |
+
anchor = (
|
| 104 |
+
source[:, :1]
|
| 105 |
+
if current_state is None
|
| 106 |
+
else np.take(current_state, component.joint_dims, axis=-1)[:, None]
|
| 107 |
+
)
|
| 108 |
+
joined = np.concatenate((anchor, source), axis=1)
|
| 109 |
+
if component.joint_wrap == "pi":
|
| 110 |
+
outside = np.abs(joined) > np.pi + 1e-5
|
| 111 |
+
if outside.any():
|
| 112 |
+
row = int(np.flatnonzero(outside.any(axis=(1, 2)))[0])
|
| 113 |
+
column = component.joint_dims[int(np.argmax(outside[row].any(axis=0)))]
|
| 114 |
+
raise ValueError(
|
| 115 |
+
f"action space {preset.name!r} declares joint_wrap='pi' for component "
|
| 116 |
+
f"{component.name!r}, so its absolute joint columns must lie in (-pi, pi]; "
|
| 117 |
+
f"source column {column} reaches {joined[row][outside[row]][0]:.6g} rad. Wrap the "
|
| 118 |
+
"inputs, or register a layout with joint_wrap='none' for coordinates that "
|
| 119 |
+
"are continuous past one turn, such as a multi-turn or prismatic axis."
|
| 120 |
+
)
|
| 121 |
+
batch, steps, width = joined.shape
|
| 122 |
+
kernel = native_kernel("unwrap_angles")
|
| 123 |
+
unwrapped = (
|
| 124 |
+
(
|
| 125 |
+
kernel(joined.transpose(1, 0, 2).reshape(steps, batch * width), True)
|
| 126 |
+
.reshape(steps, batch, width)
|
| 127 |
+
.transpose(1, 0, 2)
|
| 128 |
+
)
|
| 129 |
+
if kernel is not None
|
| 130 |
+
else np.unwrap(joined, axis=1)
|
| 131 |
+
)
|
| 132 |
+
anchor, source = unwrapped[:, :1], unwrapped[:, 1:]
|
| 133 |
+
return np.asarray(
|
| 134 |
+
_resample(source, source_hz, preset.target_hz, preset.interpolation) - anchor,
|
| 135 |
+
dtype=np.float32,
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _eef(preset, values, component, source_hz, current_state):
|
| 140 |
+
if preset.input == "delta":
|
| 141 |
+
delta = np.concatenate(
|
| 142 |
+
(
|
| 143 |
+
np.take(values, component.position_dims, axis=-1),
|
| 144 |
+
np.take(values, component.rotation_dims, axis=-1),
|
| 145 |
+
),
|
| 146 |
+
axis=-1,
|
| 147 |
+
)
|
| 148 |
+
poses = _pose_map(delta, "integrate_eef_deltas_batch", integrate_eef_deltas)
|
| 149 |
+
sampled = _resample(
|
| 150 |
+
poses, source_hz, preset.target_hz, preset.interpolation, kind="pose_xyzw"
|
| 151 |
+
)
|
| 152 |
+
return _pose_map(sampled, "eef_pose_absolute_batch", eef_pose_absolute)
|
| 153 |
+
poses = _poses(values, component)
|
| 154 |
+
anchor = poses[:, :1] if current_state is None else _poses(current_state[:, None], component)
|
| 155 |
+
sampled = _resample(poses, source_hz, preset.target_hz, preset.interpolation, kind="pose_xyzw")
|
| 156 |
+
return _local(sampled, anchor)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def canonicalize_batch(preset: PresetConfig, values, *, source_hz, current_state):
|
| 160 |
+
"""Return component ``(B,T_codec,D_active)`` arrays from validated input."""
|
| 161 |
+
return {
|
| 162 |
+
component.name: canonicalize_component_batch(
|
| 163 |
+
preset, values, component, source_hz=source_hz, current_state=current_state
|
| 164 |
+
)
|
| 165 |
+
for component in preset.components
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
def canonicalize_component_batch(preset, values, component, *, source_hz, current_state):
|
| 170 |
+
"""Convert one component of validated ``(B,T,D)`` source actions."""
|
| 171 |
+
motion = (_joint if component.space == "joint" else _eef)(
|
| 172 |
+
preset, values, component, source_hz, current_state
|
| 173 |
+
)
|
| 174 |
+
if component.gripper_dim is None:
|
| 175 |
+
return motion
|
| 176 |
+
gripper = values[..., component.gripper_dim : component.gripper_dim + 1]
|
| 177 |
+
if component.gripper_threshold is not None:
|
| 178 |
+
gripper = ((gripper >= component.gripper_threshold) if component.gripper_open_is_high
|
| 179 |
+
else (gripper <= component.gripper_threshold)).astype(np.float32)
|
| 180 |
+
else:
|
| 181 |
+
gripper = normalize_gripper_actions(
|
| 182 |
+
gripper.reshape(-1, 1),
|
| 183 |
+
closed_value=component.gripper_closed_value,
|
| 184 |
+
open_value=component.gripper_open_value,
|
| 185 |
+
).reshape(*values.shape[:2], 1)
|
| 186 |
+
gripper = _resample(gripper, source_hz, preset.target_hz, preset.interpolation, kind="hold")
|
| 187 |
+
return np.concatenate((motion, gripper), axis=-1)
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def _external_eef(motion, component, current_state):
|
| 191 |
+
poses = _rotvec_poses(motion)
|
| 192 |
+
if current_state is None:
|
| 193 |
+
return _pose_map(poses, "eef_pose_deltas_batch", eef_pose_deltas)
|
| 194 |
+
anchor = _poses(current_state[:, None], component)
|
| 195 |
+
quaternion = multiply_quaternions(
|
| 196 |
+
normalize_quaternion(anchor[..., 3:7]), normalize_quaternion(poses[..., 3:7])
|
| 197 |
+
)
|
| 198 |
+
attached = np.concatenate(
|
| 199 |
+
(poses[..., :3].astype(np.float64) + anchor[..., :3], quaternion), axis=-1
|
| 200 |
+
).astype(np.float32)
|
| 201 |
+
return eef_pose_components(attached.reshape(-1, 7), component.rotation_format).reshape(
|
| 202 |
+
*attached.shape[:2], 3 + len(component.rotation_dims)
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def externalize_batch(
|
| 207 |
+
values, component: ComponentSpec, *, source_hz, target_hz, target_length, method, current_state
|
| 208 |
+
):
|
| 209 |
+
"""Resample decoded paths and return ``(B,T_output,D_component)`` arrays."""
|
| 210 |
+
sampled = resample_canonical_batch(
|
| 211 |
+
values,
|
| 212 |
+
component,
|
| 213 |
+
source_hz=source_hz,
|
| 214 |
+
target_hz=target_hz,
|
| 215 |
+
target_length=target_length,
|
| 216 |
+
method=method,
|
| 217 |
+
)
|
| 218 |
+
return externalize_canonical_batch(sampled, component, current_state=current_state)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def resample_canonical_batch(
|
| 222 |
+
values, component: ComponentSpec, *, source_hz, target_hz, target_length, method
|
| 223 |
+
):
|
| 224 |
+
"""Rate-convert local ``(B,T,D_active)`` paths, holding gripper commands."""
|
| 225 |
+
motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
|
| 226 |
+
motion = values[..., :motion_width]
|
| 227 |
+
if component.space == "eef":
|
| 228 |
+
poses = _rotvec_poses(motion)
|
| 229 |
+
poses = _resample(
|
| 230 |
+
poses, source_hz, target_hz, method, kind="pose_xyzw", target_length=target_length
|
| 231 |
+
)
|
| 232 |
+
motion = _pose_map(poses, "eef_pose_absolute_batch", eef_pose_absolute)
|
| 233 |
+
else:
|
| 234 |
+
motion = _resample(motion, source_hz, target_hz, method, target_length=target_length)
|
| 235 |
+
if component.gripper_dim is None:
|
| 236 |
+
return np.asarray(motion, dtype=np.float32)
|
| 237 |
+
gripper = _resample(
|
| 238 |
+
values[..., motion_width : motion_width + 1],
|
| 239 |
+
source_hz,
|
| 240 |
+
target_hz,
|
| 241 |
+
method,
|
| 242 |
+
kind="hold",
|
| 243 |
+
target_length=target_length,
|
| 244 |
+
)
|
| 245 |
+
return np.asarray(np.concatenate((motion, gripper), axis=-1), dtype=np.float32)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def externalize_canonical_batch(values, component: ComponentSpec, *, current_state):
|
| 249 |
+
"""Restore source coordinates or deltas from ``(B,T,D_active)`` local paths."""
|
| 250 |
+
motion_width = len(component.joint_dims or ()) if component.space == "joint" else 6
|
| 251 |
+
motion = values[..., :motion_width]
|
| 252 |
+
if component.space == "eef":
|
| 253 |
+
motion = _external_eef(motion, component, current_state)
|
| 254 |
+
elif current_state is None:
|
| 255 |
+
motion = difference_additive(motion, 1)
|
| 256 |
+
else:
|
| 257 |
+
motion = motion + np.take(current_state, component.joint_dims, axis=-1)[:, None]
|
| 258 |
+
if component.joint_wrap == "pi":
|
| 259 |
+
motion = wrap_to_pi(motion)
|
| 260 |
+
if component.gripper_dim is None:
|
| 261 |
+
return np.asarray(motion, dtype=np.float32)
|
| 262 |
+
gripper = values[..., motion_width : motion_width + 1]
|
| 263 |
+
if component.gripper_threshold is not None:
|
| 264 |
+
gripper = gripper if component.gripper_open_is_high else 1.0 - gripper
|
| 265 |
+
return np.asarray(np.concatenate((motion, gripper), axis=-1), dtype=np.float32)
|
| 266 |
+
gripper = component.gripper_closed_value + gripper * (
|
| 267 |
+
component.gripper_open_value - component.gripper_closed_value
|
| 268 |
+
)
|
| 269 |
+
return np.asarray(np.concatenate((motion, gripper), axis=-1), dtype=np.float32)
|