2026-08-16 00:02:58 +08:00
|
|
|
# Omni-only Turn-Taking Experiment
|
|
|
|
|
|
|
|
|
|
This project tests whether **Qwen2.5-Omni-3B can replace both the handcrafted
|
|
|
|
|
audio/visual feature extractors and the downstream RNN/Transformer used by
|
|
|
|
|
EgoSpeak** for egocentric turn-taking anticipation.
|
|
|
|
|
|
|
|
|
|
The main experiment is deliberately simple:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
causal audio + video up to time t
|
|
|
|
|
|
|
|
|
|
|
v
|
|
|
|
|
Qwen2.5-Omni-3B Thinker
|
|
|
|
|
|
|
|
|
|
|
fused state q_t [2048]
|
|
|
|
|
|
|
|
|
|
|
v
|
|
|
|
|
trainable prediction head
|
|
|
|
|
|
|
|
|
|
|
v
|
|
|
|
|
future logits [10 horizons, 3 classes]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
There is **no RNN after Qwen**. The Omni Thinker is the temporal model. The
|
|
|
|
|
only supervised component in the first experiment is a small `10 x 3`
|
|
|
|
|
classification head.
|
|
|
|
|
|
|
|
|
|
## Current status
|
|
|
|
|
|
|
|
|
|
| File | Status | Purpose |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| `smoke_test.py` | Verified on one NVIDIA L20 | Text and 12-second EasyCom audio-video inference |
|
2026-08-19 11:29:27 +08:00
|
|
|
| `easycom_manifest.py` | Verified on EasyCom | Discover one-minute clips, preserve session timecodes, and check 5 Hz targets |
|
2026-08-16 00:02:58 +08:00
|
|
|
| `qwen_omni_features.py` | Implemented, not executed here | Load only the 3B Thinker and extract fused states from one AV window |
|
2026-08-19 11:29:27 +08:00
|
|
|
| `streaming_turn_model.py` | Verified on EasyCom | Produce one causal Omni state every 200 ms, with causal-prefix and open-stream modes |
|
|
|
|
|
| `TurnPredictionHead` | Trained and evaluated | Map `[2048]` to `[10, 3]` logits |
|
|
|
|
|
| `train_turn_head.py` | Verified on EasyCom | Train the frozen-Omni anticipation head with the official EasyCom split |
|
|
|
|
|
| `infer_turn_head.py` | Verified on EasyCom | Save EgoSpeak-format predictions and reproduce per-horizon AP/mAP |
|
2026-08-24 00:27:42 +08:00
|
|
|
| `eval_omni_turn_lora.py` | Implemented, remote validation pending | Evaluate Thinker LoRA from dense AV caches with the same AP/mAP protocol |
|
2026-08-16 00:02:58 +08:00
|
|
|
|
2026-08-19 11:29:27 +08:00
|
|
|
The full Thinker input path, embedded audio, direct video decoding,
|
|
|
|
|
FlashAttention-2, 5 Hz streaming extraction, cache replay, target alignment,
|
|
|
|
|
head training, and EgoSpeak-compatible evaluation have now been exercised on
|
2026-08-23 15:29:26 +08:00
|
|
|
EasyCom. The first Ego4D 8-second versus 20-second context ablation has also
|
|
|
|
|
been recorded. Multi-seed evaluation remains in progress.
|
2026-08-19 11:29:27 +08:00
|
|
|
|
|
|
|
|
## Current EasyCom results
|
|
|
|
|
|
|
|
|
|
The current single-run comparison was recorded on 2026-08-19. Both modes use
|
|
|
|
|
the frozen Qwen2.5-Omni-3B Thinker, 2 FPS video, 5 Hz predictions, a 20-second
|
|
|
|
|
causal context budget, the same `10 x 3` head architecture, and EgoSpeak's
|
|
|
|
|
official EasyCom train/test split.
|
|
|
|
|
|
|
|
|
|
| Representation | EgoSpeak full-timeline mean mAP | Strict causal mean mAP |
|
|
|
|
|
|---|---:|---:|
|
|
|
|
|
| Open-stream replay | 58.316 | 58.326 |
|
|
|
|
|
| Causal-prefix | 58.830 | 58.841 |
|
|
|
|
|
|
|
|
|
|
Causal-prefix is 0.514 mAP points higher in the exact Table 2 protocol, but
|
|
|
|
|
the difference is concentrated at 0.2--0.6 seconds. Open-stream is slightly
|
|
|
|
|
higher at 1.6--2.0 seconds. These are not yet final paper values because the
|
|
|
|
|
head-training variance has not been measured across multiple seeds.
|
|
|
|
|
|
|
|
|
|
The full per-horizon and per-class snapshot, machine-readable CSV, provenance,
|
|
|
|
|
and follow-up checks are in
|
|
|
|
|
[`results/easycom_open_stream_vs_causal_prefix_2026-08-19/`](results/easycom_open_stream_vs_causal_prefix_2026-08-19/README.md).
|
2026-08-16 00:02:58 +08:00
|
|
|
|
2026-08-23 15:29:26 +08:00
|
|
|
### EasyCom 8-second versus 20-second context
|
|
|
|
|
|
|
|
|
|
An additional single-run open-stream comparison recorded on 2026-08-23 gives
|
|
|
|
|
**58.51937** full-timeline mean mAP with an 8-second context budget versus
|
|
|
|
|
**58.316** with 20 seconds, a difference of **+0.20337** points. The aggregate
|
|
|
|
|
difference is small and hides a clear horizon tradeoff: 8 seconds is stronger
|
|
|
|
|
at 0.2--0.8 seconds, whereas 20 seconds is stronger from 1.0 through 2.0
|
|
|
|
|
seconds. These runs must be repeated with identical additional seeds before
|
|
|
|
|
choosing a context budget for the paper.
|
|
|
|
|
|
|
|
|
|
The raw 8-second console output, machine-readable comparison, and detailed
|
|
|
|
|
interpretation are archived in
|
|
|
|
|
[`results/easycom_open_stream_8s_vs_20s_2026-08-23/`](results/easycom_open_stream_8s_vs_20s_2026-08-23/README.md).
|
|
|
|
|
|
|
|
|
|
## Current Ego4D context-length results
|
|
|
|
|
|
|
|
|
|
The following single-seed snapshot was recorded on 2026-08-22. Both variants
|
|
|
|
|
use open-stream replay, 2 FPS Decord video, 5 Hz predictions, the same frozen
|
|
|
|
|
Qwen2.5-Omni-3B Thinker and independently trained `10 x 3` heads. They differ
|
|
|
|
|
only in the rolling causal-memory budget: 8 seconds versus 20 seconds. Both
|
|
|
|
|
runs use the same EgoSpeak Ego4D train/test manifest and test timeline.
|
|
|
|
|
|
|
|
|
|
| Context | EgoSpeak full-timeline mean mAP | Strict causal mean mAP |
|
|
|
|
|
|---|---:|---:|
|
|
|
|
|
| 8 seconds | **65.5943** | **65.6920** |
|
|
|
|
|
| 20 seconds | 63.1408 | 63.2298 |
|
|
|
|
|
| 20s minus 8s | -2.4535 | -2.4622 |
|
|
|
|
|
|
|
|
|
|
The strict-causal per-horizon comparison is:
|
|
|
|
|
|
|
|
|
|
| Horizon | 8s mAP | 20s mAP | 20s minus 8s |
|
|
|
|
|
|---:|---:|---:|---:|
|
|
|
|
|
| 0.2s | **74.2213** | 71.7403 | -2.4810 |
|
|
|
|
|
| 0.4s | **71.0180** | 68.5945 | -2.4234 |
|
|
|
|
|
| 0.6s | **68.3746** | 65.9287 | -2.4459 |
|
|
|
|
|
| 0.8s | **66.3190** | 63.8606 | -2.4584 |
|
|
|
|
|
| 1.0s | **64.7470** | 62.3329 | -2.4140 |
|
|
|
|
|
| 1.2s | **63.6416** | 61.2290 | -2.4126 |
|
|
|
|
|
| 1.4s | **62.8697** | 60.4035 | -2.4662 |
|
|
|
|
|
| 1.6s | **62.3142** | 59.7870 | -2.5272 |
|
|
|
|
|
| 1.8s | **61.8705** | 59.3678 | -2.5027 |
|
|
|
|
|
| 2.0s | **61.5444** | 59.0541 | -2.4903 |
|
|
|
|
|
| Mean | **65.6920** | 63.2298 | -2.4622 |
|
|
|
|
|
|
|
|
|
|
For the exact full-timeline protocol, averaging class AP over the ten
|
|
|
|
|
horizons gives:
|
|
|
|
|
|
|
|
|
|
| Class | 8s mean AP | 20s mean AP | 20s minus 8s |
|
|
|
|
|
|---|---:|---:|---:|
|
|
|
|
|
| Background | **69.5063** | 67.2409 | -2.2653 |
|
|
|
|
|
| Camera wearer speaking | **59.4005** | 56.5784 | -2.8222 |
|
|
|
|
|
| Other people speaking | **67.8760** | 65.6030 | -2.2730 |
|
|
|
|
|
|
|
|
|
|
The 8-second representation is better at every horizon and for every class.
|
|
|
|
|
The approximately 2.4--2.5 point gap is nearly constant across anticipation
|
|
|
|
|
horizons, while the largest class degradation is for camera-wearer speech.
|
|
|
|
|
This pattern does not support a long-horizon benefit from the additional 12
|
|
|
|
|
seconds of context. A plausible hypothesis is that excessive history dilutes
|
|
|
|
|
locally predictive turn-taking cues in the final frozen Thinker state, but the
|
|
|
|
|
single-seed comparison cannot establish that mechanism. Both context settings
|
|
|
|
|
must be repeated with the same additional seeds before treating the difference
|
|
|
|
|
as a final paper result.
|
|
|
|
|
|
2026-08-16 00:02:58 +08:00
|
|
|
## Why Omni-only instead of Omni + RNN
|
|
|
|
|
|
|
|
|
|
EgoSpeak separately extracts visual and audio features and then uses GRU,
|
|
|
|
|
LSTR, or Mamba for temporal modeling:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
ResNet50 video feature + wav2vec2 audio feature -> temporal model -> prediction
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Qwen2.5-Omni already contains:
|
|
|
|
|
|
|
|
|
|
- audio and visual encoders;
|
|
|
|
|
- joint audio-video temporal positions;
|
|
|
|
|
- a causal decoder-only Thinker with KV cache;
|
|
|
|
|
- pretrained acoustic, visual, and semantic knowledge.
|
|
|
|
|
|
|
|
|
|
Adding a GRU after a continuously processed Omni sequence would model history
|
|
|
|
|
twice and would make it harder to attribute an improvement to Omni. The primary
|
|
|
|
|
comparison is therefore:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
EgoSpeak: ResNet50 + wav2vec2 + GRU
|
|
|
|
|
Ours: Qwen2.5-Omni Thinker + small prediction head
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
An Omni-feature-plus-GRU model can be kept as an ablation, but it is not the
|
|
|
|
|
main architecture.
|
|
|
|
|
|
|
|
|
|
## EgoSpeak contract that this project reproduces
|
|
|
|
|
|
|
|
|
|
The inspected reference repository is `/Volumes/760P/Code/EgoSpeak`.
|
|
|
|
|
It contains no Qwen or Omni implementation and no raw-media feature extraction
|
|
|
|
|
scripts. It distributes precomputed arrays instead:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
DATASET/
|
|
|
|
|
|- rgb_kinetics_resnet50/ # [T, 2048]
|
|
|
|
|
|- audio_wav2vec2/ # [T, 5120]
|
|
|
|
|
`- target_perframe/ # [T, 3]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The useful part for this project is the exact task interface.
|
|
|
|
|
|
|
|
|
|
### Time and classes
|
|
|
|
|
|
|
|
|
|
Both EasyCom and the EgoSpeak `EGO4DSHUFFLE` split use **5 FPS**, so one task
|
|
|
|
|
timestep is **0.2 seconds**. The three output columns are:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
0: background
|
|
|
|
|
1: camera wearer speaking
|
|
|
|
|
2: other people speaking
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
They are not strictly one-hot. Background is exclusive, but simultaneous
|
|
|
|
|
camera-wearer and other-person speech is stored as `[0, 1, 1]`. The inspected
|
|
|
|
|
EasyCom features contain 7,711 such overlap steps. Preserve the full
|
|
|
|
|
multi-positive target rather than applying `argmax`.
|
|
|
|
|
|
|
|
|
|
Relevant EgoSpeak files:
|
|
|
|
|
|
|
|
|
|
- `LSTR/data/data_info.json`: FPS, classes, and session splits;
|
|
|
|
|
- `MiniROAD/configs/EasyCom/miniroad_aa_easycom.yaml`: 128-step training
|
|
|
|
|
windows, stride 4, three classes, and anticipation length 10;
|
|
|
|
|
- `MiniROAD/datasets/dataset.py`: feature loading and future-label indexing;
|
|
|
|
|
- `MiniROAD/criterions/loss.py`: anticipation loss;
|
|
|
|
|
- `MiniROAD/trainer/eval.py`: per-horizon AP and mean anticipation mAP.
|
|
|
|
|
|
|
|
|
|
### Future target alignment
|
|
|
|
|
|
|
|
|
|
For current task index `i`, the prediction target is:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
target[i + 1 : i + 11]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Thus the ten output rows represent:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
t+0.2, t+0.4, t+0.6, ..., t+2.0 seconds
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If `states[i]` is verified to correspond to EgoSpeak task index `i`, the
|
|
|
|
|
offline head-training tensors should be constructed as follows:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
target = target_perframe # [T, 3], multi-positive
|
|
|
|
|
usable = min(len(states), len(target)) - 10
|
|
|
|
|
|
|
|
|
|
x = states[:usable] # [N, 2048]
|
|
|
|
|
y = stack(
|
|
|
|
|
[target[h : h + usable] for h in range(1, 11)],
|
|
|
|
|
axis=1,
|
|
|
|
|
) # [N, 10, 3]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Do not silently assume that the first saved Omni state matches label index
|
|
|
|
|
zero. EgoSpeak does not publish the script that generated its original
|
|
|
|
|
ResNet/wav2vec2 features, so the exact zero-time convention is not documented.
|
|
|
|
|
Before training, inspect a short clip with known speech boundaries and test the
|
|
|
|
|
candidate mappings `states[i] -> target[i]` and `states[i] -> target[i+1]`.
|
|
|
|
|
Store the selected offset in every feature file/checkpoint.
|
|
|
|
|
|
|
|
|
|
### Splits
|
|
|
|
|
|
|
|
|
|
Use the published split, not a random frame split.
|
|
|
|
|
|
|
|
|
|
EasyCom:
|
|
|
|
|
|
|
|
|
|
- train, preserving EgoSpeak's list: `Session_7`, `Session_11`, `Session_10`,
|
|
|
|
|
`Session_6`, `Session_9`, `Session_4`, `Session_8`, `Session_5`, `Session_12`;
|
|
|
|
|
- test, preserving EgoSpeak's list: `Session_3`, `Session_2`, `Session_1`.
|
|
|
|
|
|
|
|
|
|
This is the **EgoSpeak comparison split**. The EasyCom dataset repository
|
|
|
|
|
itself recommends a different train/validation/test split; do not substitute
|
|
|
|
|
that split when reproducing EgoSpeak Table 2.
|
|
|
|
|
|
|
|
|
|
Ego4D Audio-Visual Diarization:
|
|
|
|
|
|
|
|
|
|
- use the `EGO4DSHUFFLE` key;
|
|
|
|
|
- 346 training clips;
|
|
|
|
|
- 87 test clips;
|
|
|
|
|
- each clip is approximately five minutes.
|
|
|
|
|
|
|
|
|
|
Never mix timesteps from the same session across training and validation.
|
|
|
|
|
|
|
|
|
|
## EasyCom raw-media layout and manifest
|
|
|
|
|
|
|
|
|
|
The extracted release used here has this relevant structure:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
extracted_easycom/
|
|
|
|
|
`- Main/
|
|
|
|
|
|- Video_Compressed/
|
|
|
|
|
| |- Session_1/
|
|
|
|
|
| | |- 00-00-000.mp4
|
|
|
|
|
| | |- 01-00-000.mp4
|
|
|
|
|
| | `- ...
|
|
|
|
|
| `- Session_2 ... Session_12/
|
|
|
|
|
|- Glasses_Microphone_Array_Audio/
|
|
|
|
|
|- Close_Microphone_Audio/
|
|
|
|
|
|- Speech_Transcriptions/
|
|
|
|
|
`- Metadata/
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`Video_Compressed` is the AR-glasses wearer's egocentric stream with embedded
|
|
|
|
|
stereo AAC audio. Each session is divided into approximately one-minute clips;
|
|
|
|
|
the `mm-ss-SSS` filename is the clip's start time on the original recording
|
|
|
|
|
timeline. Some intervals are absent because EasyCom redacted one-minute
|
|
|
|
|
segments. These raw time gaps are expected.
|
|
|
|
|
|
|
|
|
|
EgoSpeak does not preserve those empty intervals. Its appendix states that it
|
|
|
|
|
crops every EasyCom clip to retain only the 1920-pixel-wide video region and
|
|
|
|
|
then merges the retained clips into one dense video per session. Therefore the
|
|
|
|
|
EgoSpeak-compatible label time for a clip is its cumulative position in the
|
|
|
|
|
concatenation, not the `mm-ss-SSS` source time. The manifest stores both.
|
|
|
|
|
|
|
|
|
|
Build a raw-video manifest and, when the EgoSpeak labels are available, check
|
|
|
|
|
their duration at 5 Hz:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python easycom_manifest.py \
|
|
|
|
|
--dataset-root /data1/yfzx/wangx/Datasets/easycom/extracted_easycom \
|
|
|
|
|
--target-root /path/to/EasyCom/target_perframe \
|
|
|
|
|
--output manifests/easycom.json
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Omit `--target-root` for the first media-only inventory. The manifest reports
|
|
|
|
|
raw-source discontinuities as informational and computes each clip's dense
|
|
|
|
|
EgoSpeak concatenation offset and corresponding 5 Hz target index. For the
|
|
|
|
|
audio-only ablation, use the same embedded glasses audio as A+V (extract it to
|
|
|
|
|
mono WAV if needed); do not switch to close microphones, because that changes
|
|
|
|
|
the sensor and makes the modality ablation unfair.
|
|
|
|
|
|
|
|
|
|
Audit downloaded EgoSpeak arrays, and optionally compare every session length
|
|
|
|
|
against the raw manifest:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python audit_egospeak_features.py \
|
|
|
|
|
--feature-root /path/to/EgoSpeak/dataset/EasyCom \
|
|
|
|
|
--raw-manifest manifests/easycom.json \
|
|
|
|
|
--output reports/egospeak_easycom_audit.json
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The inspected release contains 95,453 synchronized steps across all 12
|
|
|
|
|
sessions: 5.302944 hours at 5 Hz. Every session has identical RGB, flow, audio,
|
|
|
|
|
and target length. Shapes are `[T,2048]`, `[T,2048]`, `[T,5120]`, and `[T,3]`.
|
|
|
|
|
|
|
|
|
|
## Recommended comparison matrix
|
|
|
|
|
|
|
|
|
|
| Experiment | Visual sampling | Audio | Output rate |
|
|
|
|
|
|---|---:|---|---:|
|
|
|
|
|
| EgoSpeak official A+V | 5 Hz ResNet features | 5 Hz wav2vec2 features | 5 Hz |
|
|
|
|
|
| Omni audio-only | none | raw glasses-wearer waveform | 5 Hz |
|
|
|
|
|
| Omni A+V primary | 2 FPS | raw embedded waveform | 5 Hz |
|
|
|
|
|
| Omni A+V FPS ablation | 5 FPS | raw embedded waveform | 5 Hz |
|
|
|
|
|
|
|
|
|
|
Two FPS is the primary Omni setting because it matches Qwen's default video
|
|
|
|
|
sampling and permits longer context. Five FPS is the visual-rate ablation.
|
|
|
|
|
Input feature rates need not match EgoSpeak; the split, causal prediction
|
|
|
|
|
times, three classes, ten target horizons, and evaluator must match exactly.
|
|
|
|
|
|
|
|
|
|
## Prepare an EgoSpeak-compatible EasyCom session
|
|
|
|
|
|
|
|
|
|
Do not pass the raw one-minute files independently to Omni and do not preserve
|
|
|
|
|
the gaps implied by their filenames. EgoSpeak concatenates the retained clips
|
|
|
|
|
into a dense session timeline and crops the participant-ID banner on the left.
|
|
|
|
|
|
|
|
|
|
`prepare_easycom_session.py` reproduces this policy and creates videos whose
|
|
|
|
|
stored frames already lie on a fixed session-global 2 FPS or 5 FPS grid. This
|
|
|
|
|
prevents Qwen's per-window video loader from choosing a new adaptive set of
|
|
|
|
|
frames for every overlapping prefix.
|
|
|
|
|
|
|
|
|
|
Session 1 has been checked against the local release:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
24 retained clips
|
|
|
|
|
27,768 source frames at 20 FPS
|
|
|
|
|
/ 4 frames per task step
|
|
|
|
|
= 6,942 steps at 5 Hz
|
|
|
|
|
= target_perframe/Session_1.npy length exactly
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Preview the commands and validate frames against the target without encoding:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python prepare_easycom_session.py \
|
|
|
|
|
--session-dir /Volumes/MX500B/Code/easycom/Main/Video_Compressed/Session_1 \
|
|
|
|
|
--output-dir prepared_easycom/Session_1 \
|
|
|
|
|
--target /Volumes/MX500B/Code/EgoSpeak/dataset/EasyCom/target_perframe/Session_1.npy \
|
|
|
|
|
--fps 2 5 \
|
|
|
|
|
--dry-run
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Remove `--dry-run` to run FFmpeg. The outputs are:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Session_1_2fps.mp4 primary Omni A+V input
|
|
|
|
|
Session_1_5fps.mp4 visual-rate ablation
|
|
|
|
|
Session_1.json clip order, exact T, crop and sampling provenance
|
|
|
|
|
Session_1.ffconcat reproducible dense concatenation order
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-16 22:19:43 +08:00
|
|
|
The target length must equal the number of complete 5 Hz intervals,
|
|
|
|
|
`floor(sum(source_frames) / 4)`. If a session ends with one to three residual
|
|
|
|
|
20 Hz frames, the target-derived duration discards only that incomplete final
|
|
|
|
|
interval; the discarded frame count is recorded in the sidecar.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
Each clip's decoded video and audio are trimmed to the exact duration implied
|
|
|
|
|
by its video frame count before concatenation. This prevents AAC priming/tail
|
|
|
|
|
padding from accumulating at the 24 clip boundaries and causing progressive
|
2026-08-16 22:19:43 +08:00
|
|
|
audio/video drift. The session-global visual grid is padded defensively at EOF
|
|
|
|
|
and then trimmed by frame index, so it contains exactly
|
|
|
|
|
`ceil(target_steps / 5 * video_fps)` frames on every FFmpeg version.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
On the remote L20 machine, use its own dataset paths. For example:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python prepare_easycom_session.py \
|
|
|
|
|
--session-dir /data1/yfzx/wangx/Datasets/easycom/extracted_easycom/Main/Video_Compressed/Session_1 \
|
|
|
|
|
--output-dir /data1/yfzx/wangx/Datasets/easycom/omni_prepared/Session_1 \
|
|
|
|
|
--target /path/to/EgoSpeak/dataset/EasyCom/target_perframe/Session_1.npy \
|
|
|
|
|
--fps 2 5
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This is the command that performs the potentially long FFmpeg processing; it
|
|
|
|
|
has not been run by the code authoring workflow.
|
|
|
|
|
|
|
|
|
|
If encoding completed but a later validation/metadata step failed, update the
|
|
|
|
|
script and reuse the existing MP4 without encoding it again:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python prepare_easycom_session.py ... --fps 2 --reuse-existing
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
## What Qwen's native 2-second streaming design means
|
2026-08-16 00:02:58 +08:00
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
Qwen2.5-Omni was designed for **block-wise multimodal prefill**. Its
|
|
|
|
|
[technical report](https://arxiv.org/html/2503.20215) states that the audio
|
|
|
|
|
and visual encoders are modified for block-wise processing and that audio
|
|
|
|
|
self-attention is performed in independent two-second blocks. The released
|
|
|
|
|
[3B configuration](https://huggingface.co/Qwen/Qwen2.5-Omni-3B/raw/main/config.json)
|
|
|
|
|
uses `seconds_per_chunk=2`, `position_id_per_seconds=25`, and audio
|
|
|
|
|
`n_window=100`.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
Conceptually, a native online input is organized as follows:
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
```text
|
2026-08-17 15:17:11 +08:00
|
|
|
open one user/media turn
|
|
|
|
|
encode/append AV [0,2] -> commit Thinker KV
|
|
|
|
|
encode/append AV [2,4] -> commit Thinker KV
|
|
|
|
|
encode/append AV [4,6] -> commit Thinker KV
|
|
|
|
|
...
|
|
|
|
|
close the media turn -> generate text and/or speech
|
2026-08-16 00:02:58 +08:00
|
|
|
```
|
|
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
Within each temporal group, video tokens precede the aligned audio tokens.
|
|
|
|
|
TMRoPE assigns 25 temporal positions per second, or 50 positions per native
|
|
|
|
|
two-second group, so audio and video share a physical-time coordinate system.
|
|
|
|
|
Completed blocks are appended to one causal Thinker sequence; the long-range
|
|
|
|
|
history is modeled by the Thinker rather than by cross-block full attention in
|
|
|
|
|
the modality encoders.
|
|
|
|
|
|
|
|
|
|
This architecture is block-causal, not frame-causal. Attention inside an
|
|
|
|
|
audio block is non-causal. It is therefore invalid to encode the complete
|
|
|
|
|
`[0,2]` block and claim that its representation at 0.2 seconds saw no future:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
encode complete [0,2] AV -> read state at 0.2 s # future leakage
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For this project's 5 Hz targets, the current incomplete block must instead be
|
|
|
|
|
encoded only through the prediction time. These 0.2-second partial-block
|
|
|
|
|
readouts are an extension made for EgoSpeak; the Qwen paper does not define a
|
|
|
|
|
canonical hidden state every 200 ms.
|
|
|
|
|
|
|
|
|
|
The public Transformers and local Qwen web-demo APIs normally receive a
|
|
|
|
|
completed media object and call the processor/model once. They retain the
|
|
|
|
|
model's block-wise masks and token organization, but do not expose the full
|
|
|
|
|
persistent live-session implementation used by the hosted real-time product.
|
|
|
|
|
|
|
|
|
|
## Native Qwen streaming versus `causal-prefix`
|
|
|
|
|
|
|
|
|
|
The two extraction modes are both causal with respect to raw observations,
|
|
|
|
|
but they define different model inputs. `causal-prefix` is a sliding-window
|
|
|
|
|
reference invented for this experiment; it is not Qwen's native streaming
|
|
|
|
|
execution model.
|
|
|
|
|
|
|
|
|
|
| Property | `causal-prefix` | Native Qwen streaming idea |
|
|
|
|
|
|---|---|---|
|
|
|
|
|
| Session | New inference at every 0.2 s | One open media turn |
|
|
|
|
|
| Media organization | One complete `[max(0,t-M),t]` item | Fixed source-aligned 2 s blocks |
|
|
|
|
|
| Modality encoding | Re-encode the complete window | Encode/commit each completed block once |
|
|
|
|
|
| Thinker KV | Start empty for every state | Append to persistent KV |
|
|
|
|
|
| TMRoPE | Recomputed from each window origin | Advances with the stream |
|
|
|
|
|
| History | Exact rolling `M` seconds | Accumulates until a serving/context policy intervenes |
|
|
|
|
|
| 5 Hz readout | Close/read a new turn each time | Not specified by the original model |
|
|
|
|
|
|
|
|
|
|
The difference becomes especially important after a sliding window fills. At
|
|
|
|
|
`t=20.8` with `M=20`, `causal-prefix` constructs one new media item covering:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
[0.8,20.8]
|
|
|
|
|
-> encoder blocks [0.8,2.8], [2.8,4.8], ..., [18.8,20.8]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
At `t=21.0`, every encoder boundary moves again:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
[1.0,21.0]
|
|
|
|
|
-> encoder blocks [1.0,3.0], [3.0,5.0], ..., [19.0,21.0]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Native streaming keeps the source-aligned partition instead:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
[0,2], [2,4], ..., [18,20], current partial [20,20.8]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Because Qwen's audio encoder uses block-local non-causal attention, moving a
|
|
|
|
|
block boundary changes the representation even when most raw samples are the
|
|
|
|
|
same. Window-relative TMRoPE, convolution/padding boundaries, chat closure,
|
|
|
|
|
and BF16/attention execution add further differences. This is why exact KV
|
|
|
|
|
replay can be numerically identical to its own from-scratch replay while still
|
|
|
|
|
differing from `causal-prefix`; the modes are evaluating different token and
|
|
|
|
|
encoder histories.
|
|
|
|
|
|
|
|
|
|
Use the modes according to the scientific question:
|
|
|
|
|
|
|
|
|
|
- use `open-stream-replay` for the primary online experiment, training, and
|
|
|
|
|
deployment-aligned evaluation;
|
|
|
|
|
- use `causal-prefix` as an expensive continuous-window reference and
|
|
|
|
|
short-clip ablation;
|
|
|
|
|
- never train a head on one representation and deploy it on the other without
|
|
|
|
|
retraining, because their feature distributions are not interchangeable.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
## Causal 5 Hz extraction modes
|
|
|
|
|
|
2026-08-16 22:19:43 +08:00
|
|
|
`streaming_turn_model.py` implements two modes.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
### 1. `causal-prefix`: continuous-window reference
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
At every 200 ms, independently process the causal context ending at the
|
|
|
|
|
current time:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
[max(0, t-20 s), t] AV -> Omni -> q_t
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Properties:
|
|
|
|
|
|
|
|
|
|
- strict no-future input;
|
|
|
|
|
- one continuous media window per prediction;
|
|
|
|
|
- no cross-forward cache;
|
|
|
|
|
- very expensive because overlapping context is recomputed;
|
2026-08-17 15:17:11 +08:00
|
|
|
- useful for checking continuous sliding-window semantics and causality;
|
|
|
|
|
- not the native Qwen block-streaming execution model.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python streaming_turn_model.py \
|
|
|
|
|
--video /path/to/easycom_session.mp4 \
|
|
|
|
|
--use-audio-in-video \
|
|
|
|
|
--mode causal-prefix \
|
|
|
|
|
--start 0 \
|
|
|
|
|
--end 20 \
|
|
|
|
|
--memory-seconds 20 \
|
|
|
|
|
--output features/easycom_20s_prefix.pt
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-16 22:19:43 +08:00
|
|
|
### 2. `open-stream-replay`: recommended efficient mode
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
This mode keeps one user/media turn open across the whole bounded-memory
|
|
|
|
|
segment. The first block contributes the chat and AV opening markers once;
|
|
|
|
|
later completed two-second blocks contribute only interleaved audio/video
|
|
|
|
|
payload tokens:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
open AV prefix
|
|
|
|
|
completed [0,2] payload -> commit KV
|
|
|
|
|
current [2,2.2] payload -> temporary branch -> close/read q_t
|
|
|
|
|
current [2,2.4] payload -> temporary branch -> close/read q_t
|
|
|
|
|
...
|
|
|
|
|
completed [2,4] payload -> commit KV
|
|
|
|
|
AV/chat closure -> never committed; readout branch only
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
This matches the core Qwen design choices: fixed two-second AV groups, one
|
|
|
|
|
open media turn, completed-block commits, and incremental Thinker KV. It makes
|
|
|
|
|
two practical changes for this dataset:
|
|
|
|
|
|
|
|
|
|
1. the current 0--2 second partial block is evaluated speculatively every
|
|
|
|
|
0.2 seconds and discarded unless it reaches a complete block boundary;
|
|
|
|
|
2. the KV history is bounded and rebuilt from recent complete blocks rather
|
2026-08-18 11:02:42 +08:00
|
|
|
than accumulating for an entire multi-minute session. Complete-block AV
|
|
|
|
|
embeddings are retained in a bounded ring, so rolling rebuilds rerun only
|
|
|
|
|
the Thinker and do not rerun the processor, audio tower, or video tower.
|
2026-08-17 15:17:11 +08:00
|
|
|
|
|
|
|
|
It should therefore be described as **bounded Qwen-style open streaming**, not
|
|
|
|
|
as bit-for-bit reproduction of an unbounded official serving session.
|
|
|
|
|
|
2026-08-16 00:02:58 +08:00
|
|
|
The audio encoder accepts a shorter final chunk with padding and an
|
|
|
|
|
actual-length mask. For the two-frame visual patch requirement, strict-grid
|
|
|
|
|
mode duplicates only the most recent past frame when necessary.
|
|
|
|
|
|
|
|
|
|
Run a four-second validation before using it for extraction:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python streaming_turn_model.py \
|
|
|
|
|
--model-id /path/to/Qwen2.5-Omni-3B \
|
|
|
|
|
--video prepared_easycom/Session_1/Session_1_2fps.mp4 \
|
|
|
|
|
--use-audio-in-video \
|
|
|
|
|
--mode open-stream-replay \
|
|
|
|
|
--start 0 --end 4 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--strict-fixed-video-grid \
|
2026-08-17 22:45:17 +08:00
|
|
|
--verify-every 1 \
|
|
|
|
|
--verify-continuous-every 1 \
|
2026-08-16 00:02:58 +08:00
|
|
|
--flash-attention-2 \
|
|
|
|
|
--output features/Session_1_open_first_4s.pt
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`cache_replay` must be identical to replaying the same open stream from an
|
|
|
|
|
empty cache. `continuous_window_semantics` compares it with one normal
|
|
|
|
|
continuous Qwen AV input. Do not assume equivalence: the modality encoders are
|
|
|
|
|
still invoked per tail block in the efficient path, so this numerical check is
|
|
|
|
|
required even though artificial user-turn boundaries have been removed.
|
|
|
|
|
|
|
|
|
|
## Whole-session prediction and the 20-second limit
|
|
|
|
|
|
|
|
|
|
Twenty seconds is a practical memory/context setting for the 3090, not the
|
2026-08-17 15:17:11 +08:00
|
|
|
maximum video duration and not a limit specified by Qwen. The original
|
|
|
|
|
append-only idea eventually reaches both linear KV-memory growth and the
|
|
|
|
|
Thinker's 32,768-token context limit, so an unbounded multi-minute stream is
|
|
|
|
|
not practical. The complete session is processed sequentially and a
|
2026-08-16 00:02:58 +08:00
|
|
|
prediction is emitted every 0.2 seconds.
|
|
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
`open-stream-replay` uses blockwise rolling memory. With a 20-second
|
2026-08-16 00:02:58 +08:00
|
|
|
budget it rebuilds the open cache from the latest nine completed blocks
|
|
|
|
|
(18 seconds) when a new block starts, then adds the current 0--2 second tail.
|
|
|
|
|
Its effective history therefore varies from 18 to 20 seconds and never drops
|
2026-08-18 11:02:42 +08:00
|
|
|
to an empty 0.2-second context at a 20-second boundary. The nine blocks'
|
|
|
|
|
prepared AV embeddings and local TMRoPE coordinates are cached once; strict
|
|
|
|
|
window replay still recomputes the Thinker KV so retained blocks no longer
|
|
|
|
|
carry indirect influence from observations that fell out of the window.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
For a short native-stream fidelity check, set `--memory-seconds` to at least
|
|
|
|
|
the tested clip duration. For example, the options
|
|
|
|
|
`--start 0 --end 40 --memory-seconds 40` prevent a rolling rebuild during that
|
|
|
|
|
40-second clip.
|
|
|
|
|
Do not extend this policy to complete EasyCom/Ego4D sessions: KV memory grows
|
|
|
|
|
linearly, and this implementation clones the cache for speculative partial
|
|
|
|
|
blocks and readout branches.
|
|
|
|
|
|
2026-08-16 00:02:58 +08:00
|
|
|
For the paper experiment, drive the output length from the preparation sidecar
|
|
|
|
|
rather than a floating MP4 duration:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python streaming_turn_model.py \
|
|
|
|
|
--session-metadata prepared_easycom/Session_1/Session_1.json \
|
|
|
|
|
--use-audio-in-video \
|
|
|
|
|
--mode open-stream-replay \
|
|
|
|
|
--memory-seconds 20 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--strict-fixed-video-grid \
|
|
|
|
|
--verify-every 25 \
|
|
|
|
|
--verify-continuous-every 25 \
|
|
|
|
|
--output features/Session_1_omni_2fps.pt
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The sidecar selects the matching fixed-FPS MP4 and sets `N` to its exact
|
|
|
|
|
`num_steps`. Alternatively pass `--video ... --target ...` explicitly. A raw
|
|
|
|
|
20 FPS MP4 with `--video-fps 2` is rejected when
|
|
|
|
|
`--strict-fixed-video-grid` is enabled.
|
|
|
|
|
|
|
|
|
|
In strict A+V mode the extractor does not delegate temporal frame selection to
|
2026-08-17 22:45:17 +08:00
|
|
|
the generic video utility. It reads the already-fixed MP4 grid explicitly and
|
|
|
|
|
assigns both video frames and audio samples with half-open intervals
|
|
|
|
|
`[start, end)`. Thus a frame exactly on a two-second boundary belongs only to
|
|
|
|
|
the following block. It duplicates the most recent available frame when the
|
|
|
|
|
visual tower needs an even frame count. Audio is loaded once at 16 kHz and
|
|
|
|
|
sliced at the same causal boundary. The processor still receives
|
2026-08-16 00:02:58 +08:00
|
|
|
`use_audio_in_video=True`, so Qwen's audio/video token interleaving is retained.
|
2026-08-16 22:19:43 +08:00
|
|
|
Long runs use `tqdm` and update the progress bar after every completed state,
|
|
|
|
|
showing percentage, media time, elapsed time, ETA, and throughput. Pass
|
|
|
|
|
`--no-progress` to disable it. The Thinker-only path also suppresses Qwen's
|
|
|
|
|
irrelevant speech-output system prompt warning.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
Before a full session, run only the first four seconds and inspect both check
|
|
|
|
|
types:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python streaming_turn_model.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--video /data1/yfzx/wangx/Datasets/easycom/omni_prepared/Session_1/Session_1_2fps.mp4 \
|
|
|
|
|
--use-audio-in-video \
|
|
|
|
|
--mode open-stream-replay \
|
|
|
|
|
--start 0 --end 4 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--strict-fixed-video-grid \
|
2026-08-17 22:45:17 +08:00
|
|
|
--verify-every 1 \
|
|
|
|
|
--verify-continuous-every 1 \
|
2026-08-16 00:02:58 +08:00
|
|
|
--flash-attention-2 \
|
|
|
|
|
--output features/Session_1_first_4s_check.pt
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-17 22:45:17 +08:00
|
|
|
Do not launch all sessions unless `cache_replay` cosine is nearly 1. For
|
|
|
|
|
`open-stream-replay`, a materially lower `continuous_window_semantics` cosine
|
|
|
|
|
means that the custom payload stitching is not equivalent to one continuous
|
|
|
|
|
Qwen input; it must not be dismissed as a media-turn-boundary effect because
|
|
|
|
|
this mode intentionally keeps a single media turn open.
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
Video can be sampled at 2 FPS while predictions remain at 5 Hz. Audio and the
|
|
|
|
|
causal boundary update every 200 ms; consecutive prediction times may use the
|
|
|
|
|
same most-recent visual frame. A frame with timestamp later than `t` must never
|
|
|
|
|
be included.
|
|
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
## Direct extraction without a prepared 2 FPS MP4
|
|
|
|
|
|
|
|
|
|
`streaming_turn_model_directly.py` removes the offline video-transcoding
|
|
|
|
|
requirement. It reads original frame PTS with Decord, constructs a global
|
|
|
|
|
2 FPS lattice, maps every lattice point to the latest source frame whose PTS
|
|
|
|
|
is not later than that point, and passes explicit frame lists to Qwen. Audio is
|
|
|
|
|
decoded once to 16-kHz mono and sliced at exact causal boundaries. Therefore
|
|
|
|
|
no FFmpeg `fps` filter or second H.264/AAC encode can alter the experiment.
|
|
|
|
|
|
|
|
|
|
Validate one Ego4D clip without loading Qwen:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python streaming_turn_model_directly.py \
|
|
|
|
|
--video /data/wangx/Datasets/ego4d/v1/clips/CLIP_UID.mp4 \
|
|
|
|
|
--target /data/wangx/Datasets/ego4d/ego4d_target_frame/CLIP_UID.npy \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--media-only
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Run a short native persistent-KV check:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python streaming_turn_model_directly.py \
|
|
|
|
|
--model-id /data/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--video /data/wangx/Datasets/ego4d/v1/clips/CLIP_UID.mp4 \
|
|
|
|
|
--end 40 \
|
|
|
|
|
--mode open-stream-replay \
|
|
|
|
|
--memory-seconds 0 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--device cuda:0 \
|
|
|
|
|
--output features/direct/CLIP_UID_first_40s.pt
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Here `--memory-seconds 0` means that completed-block Thinker KV persists for
|
|
|
|
|
the entire requested interval. Do not use this setting blindly for complete
|
|
|
|
|
multi-minute recordings: it has linear KV growth and remains subject to the
|
|
|
|
|
Thinker's context limit. A full bounded-memory dataset run should explicitly
|
|
|
|
|
use the experimental budget, for example `--memory-seconds 20`.
|
|
|
|
|
|
|
|
|
|
EasyCom can be read directly as the dense concatenation of retained clips;
|
|
|
|
|
filename timecode gaps are ignored and the rightmost 1920x1080 region is kept:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python streaming_turn_model_directly.py \
|
|
|
|
|
--model-id /path/to/Qwen2.5-Omni-3B \
|
|
|
|
|
--easycom-session-dir /path/to/Main/Video_Compressed/Session_1 \
|
|
|
|
|
--target target_frame/Session_1.npy \
|
|
|
|
|
--mode open-stream-replay \
|
|
|
|
|
--memory-seconds 20 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--device cuda:0 \
|
|
|
|
|
--output features/direct/Session_1.pt
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-18 08:41:54 +08:00
|
|
|
The direct reader defaults to Decord. On NVIDIA systems, install
|
|
|
|
|
`PyNvVideoCodec` and select the NVDEC backend explicitly:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python -m pip install PyNvVideoCodec
|
|
|
|
|
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0 python streaming_turn_model_directly.py \
|
|
|
|
|
--model-id /path/to/Qwen2.5-Omni-3B \
|
|
|
|
|
--easycom-session-dir /path/to/Main/Video_Compressed/Session_1 \
|
|
|
|
|
--end 24 \
|
|
|
|
|
--mode open-stream-replay \
|
|
|
|
|
--memory-seconds 20 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--frame-cache-size 64 \
|
|
|
|
|
--decoder pynvcodec \
|
|
|
|
|
--decoder-gpu-id 0 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--device cuda:0 \
|
|
|
|
|
--output features/direct/Session_1_24s_pynvcodec.pt
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The NVDEC backend uses ffprobe for exact display-order PTS indexing, one
|
|
|
|
|
reconfigurable `SimpleDecoder` for pixel decode, and the existing ffmpeg PCM
|
|
|
|
|
audio path. `--decoder-gpu-id` is relative to `CUDA_VISIBLE_DEVICES`, so it is
|
|
|
|
|
normally `0` even when the worker is assigned a nonzero host GPU. Decoder RGB
|
|
|
|
|
conversion can differ slightly; validate a short Decord/PyNvVideoCodec pair
|
|
|
|
|
before extracting every session, and do not mix decoder backends across
|
|
|
|
|
train/test splits.
|
|
|
|
|
|
2026-08-18 22:22:37 +08:00
|
|
|
### Persistent-model Ego4D batch extraction
|
|
|
|
|
|
|
|
|
|
`extract_ego4d_batch.py` loads Qwen and its processor once, then extracts the
|
|
|
|
|
selected EgoSpeak manifest rows sequentially. Each clip receives a new media
|
|
|
|
|
reader, Thinker KV cache, TMRoPE origin, and rolling-memory state; only the
|
|
|
|
|
immutable model weights and processor are reused. Manifest indices are
|
|
|
|
|
one-based data-row indices and do not count the TSV header.
|
|
|
|
|
|
|
|
|
|
For example, process manifest samples 81 through the end on visible GPU 0:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0 \
|
|
|
|
|
OMP_NUM_THREADS=4 \
|
|
|
|
|
MKL_NUM_THREADS=4 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=4 \
|
|
|
|
|
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
python extract_ego4d_batch.py \
|
|
|
|
|
--data-root /data1/yfzx/wangx/Datasets/ego4d \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--start-index 81 \
|
|
|
|
|
--memory-seconds 20 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--decoder decord \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--device cuda:0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Completed non-empty `.pt` files are skipped. Each new output is first written
|
|
|
|
|
to a process-specific `.partial` path and atomically renamed after extraction,
|
|
|
|
|
so rerunning the same range resumes safely.
|
|
|
|
|
|
2026-08-18 10:30:19 +08:00
|
|
|
For CPU Decord, the direct reader intentionally indexes each clip and releases
|
|
|
|
|
that temporary reader, then keeps an LRU of at most two live readers during
|
|
|
|
|
extraction. Each reader defaults to one decode thread. These limits avoid one
|
|
|
|
|
worker retaining a thread pool for every one-minute EasyCom clip and can be
|
|
|
|
|
overridden with `--decoder-threads` and `--max-open-readers`. The recommended
|
|
|
|
|
full-run settings are:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
--decoder decord --decoder-threads 1 --max-open-readers 2
|
|
|
|
|
--frame-cache-size 64
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-17 15:17:11 +08:00
|
|
|
Matching `states.shape[0]` to the target is necessary but not sufficient.
|
|
|
|
|
The direct reader also validates EasyCom's 20-Hz source inventory, source PTS
|
|
|
|
|
monotonicity, dense duration, crop bounds, audio sample count, and causal
|
|
|
|
|
frame-to-grid mapping. The original prepare scripts remain useful only as an
|
|
|
|
|
optional decode-speed/reproducibility cache.
|
|
|
|
|
|
2026-08-16 00:02:58 +08:00
|
|
|
## Saved streaming tensors
|
|
|
|
|
|
|
|
|
|
Each `.pt` file contains:
|
|
|
|
|
|
|
|
|
|
- `times`: `[N]`, prediction times in seconds;
|
|
|
|
|
- `states`: `[N, 2048]`, causal fused Thinker states;
|
|
|
|
|
- `label_indices`: `[0, ..., N-1]`, direct `target_perframe` row alignment;
|
|
|
|
|
- `valid_10_horizon_steps`: `N-10`, because the last ten current rows do not
|
|
|
|
|
have all future labels;
|
2026-08-16 22:19:43 +08:00
|
|
|
- `mode`: `causal-prefix` or `open-stream-replay`;
|
2026-08-16 00:02:58 +08:00
|
|
|
- `stream_representation`: explicit input-organization metadata;
|
|
|
|
|
- `config`: step, native block, memory, FPS, and pixel settings;
|
|
|
|
|
- `verification`: cached-versus-exact numerical checks;
|
|
|
|
|
- `input_video_fps` and `video_sampling_grid`: sampling provenance;
|
2026-08-17 22:45:17 +08:00
|
|
|
- `media_interval_policy`: fixed-grid AV uses half-open `[start, end)` ownership;
|
2026-08-16 00:02:58 +08:00
|
|
|
- `logits`: optional `[N, 10, 3]` output if a trained head is supplied;
|
|
|
|
|
- `probabilities`: optional softmax probabilities.
|
|
|
|
|
|
|
|
|
|
## Training the `10 x 3` head
|
|
|
|
|
|
|
|
|
|
The head must be trained. It is not part of pretrained Qwen.
|
|
|
|
|
|
|
|
|
|
The first experiment should freeze all Qwen parameters and train only:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
LayerNorm(2048)
|
|
|
|
|
-> Linear(2048, 1024)
|
|
|
|
|
-> GELU
|
|
|
|
|
-> Dropout
|
|
|
|
|
-> Linear(1024, 30)
|
|
|
|
|
-> reshape [10, 3]
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-16 22:19:43 +08:00
|
|
|
`easycom_head.py` defines the head and loss; they remain re-exported by
|
|
|
|
|
`streaming_turn_model.py` for compatibility:
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
from streaming_turn_model import (
|
|
|
|
|
TurnPredictionHead,
|
|
|
|
|
egospeak_multihorizon_loss,
|
|
|
|
|
)
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-16 22:19:43 +08:00
|
|
|
The supplied LSTR config uses Adam, 50 epochs, learning rate `7e-5`, weight
|
|
|
|
|
decay `5e-5`, dropout `0.2`, and 10 warmup epochs followed by cosine decay.
|
|
|
|
|
`train_turn_head.py` uses those defaults. Its batch size is 512 causal states,
|
|
|
|
|
not LSTR's 16 temporal windows, because the two model inputs have different
|
|
|
|
|
shapes.
|
|
|
|
|
|
|
|
|
|
Train once on the official nine sessions with a fixed 50-epoch protocol:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python train_turn_head.py \
|
2026-08-17 15:17:11 +08:00
|
|
|
--feature-root features/open_2fps \
|
2026-08-16 22:19:43 +08:00
|
|
|
--target-root target_frame \
|
2026-08-17 15:17:11 +08:00
|
|
|
--output checkpoints/open_2fps_head.pth \
|
2026-08-16 22:19:43 +08:00
|
|
|
--device cuda:0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The default loss is `lstr-mce`, exactly matching LSTR's MCE treatment of a
|
|
|
|
|
multi-positive `[0,1,1]` row: it sums both active negative-log-softmax terms
|
|
|
|
|
without normalizing the target. The previous L2-normalized objective belongs
|
|
|
|
|
to MiniROAD and is available only as the explicit
|
|
|
|
|
`--loss miniroad-normalized` ablation.
|
|
|
|
|
|
|
|
|
|
Recommended experimental procedure:
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
1. Freeze Qwen and extract all training-session states offline.
|
|
|
|
|
2. Verify state/label time offset and truncate only the final ten invalid
|
|
|
|
|
current positions.
|
|
|
|
|
3. Keep EgoSpeak targets as float multi-positive tensors `[N, 10, 3]`; never
|
|
|
|
|
convert them with `argmax`.
|
2026-08-16 22:19:43 +08:00
|
|
|
4. For comparison with the supplied LSTR shell, train with the default
|
|
|
|
|
`egospeak_multihorizon_loss`/`lstr-mce` objective.
|
2026-08-16 00:02:58 +08:00
|
|
|
5. Select hyperparameters using only training sessions. For EasyCom, reserve a
|
|
|
|
|
training session for development or use session-level cross-validation;
|
|
|
|
|
never tune on Sessions 1--3.
|
|
|
|
|
6. After choosing the epoch/hyperparameters, optionally retrain on all nine
|
|
|
|
|
EasyCom training sessions and evaluate the fixed model once on the test set.
|
|
|
|
|
|
|
|
|
|
Class-balanced/focal loss, a learned turn-query token, and LoRA are later
|
|
|
|
|
ablations. They should not replace the frozen-Omni linear/MLP-head baseline.
|
|
|
|
|
|
2026-08-24 11:45:11 +08:00
|
|
|
### Offline frozen states with transition-sparse supervision
|
|
|
|
|
|
|
|
|
|
This is the clean sampling-only control. It does not reconstruct cached AV
|
|
|
|
|
tokens and does not run the Thinker during training. It loads the same dense
|
|
|
|
|
5 Hz frozen states used by the standard head baseline, then retains only the
|
|
|
|
|
`target_index` values listed by the existing transition-sparse cache
|
|
|
|
|
manifests. The representation, head architecture, optimizer, 50 epochs, loss,
|
|
|
|
|
targets, and final dense test evaluator are otherwise unchanged.
|
|
|
|
|
|
|
|
|
|
Set `FROZEN_FEATURE_ROOT` to the exact feature directory used by the matching
|
|
|
|
|
dense-supervision baseline. Do not compare sparse 8-second states with a
|
|
|
|
|
dense 20-second baseline (or vice versa):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
FROZEN_FEATURE_ROOT=/data1/yfzx/wangx/Datasets/easycom/omni_features/open_stream_2fps_20s_decord
|
|
|
|
|
TARGET_ROOT=/data1/yfzx/wangx/Datasets/easycom/easycom_target_frame
|
|
|
|
|
SPARSE_ROOT=/data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s
|
|
|
|
|
|
|
|
|
|
CUDA_VISIBLE_DEVICES=3 \
|
|
|
|
|
python train_turn_head.py \
|
|
|
|
|
--feature-root "$FROZEN_FEATURE_ROOT" \
|
|
|
|
|
--target-root "$TARGET_ROOT" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_7" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_11" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_10" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_6" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_9" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_4" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_8" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_5" \
|
|
|
|
|
--sparse-anchor-cache-dir "$SPARSE_ROOT/Session_12" \
|
|
|
|
|
--output checkpoints/easycom_frozen_states_sparse_supervision_50ep.pth \
|
|
|
|
|
--epochs 50 \
|
|
|
|
|
--batch-size 512 \
|
|
|
|
|
--expected-mode any \
|
|
|
|
|
--device cuda:0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Evaluation remains dense 5 Hz on the official test sessions:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
CUDA_VISIBLE_DEVICES=3 \
|
|
|
|
|
python infer_turn_head.py \
|
|
|
|
|
--feature-root "$FROZEN_FEATURE_ROOT" \
|
|
|
|
|
--target-root "$TARGET_ROOT" \
|
|
|
|
|
--checkpoint checkpoints/easycom_frozen_states_sparse_supervision_50ep.pth \
|
|
|
|
|
--output-dir results/easycom_frozen_states_sparse_supervision_50ep \
|
|
|
|
|
--expected-mode any \
|
|
|
|
|
--device cuda:0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Compare this result against a head trained for the same 50 epochs on every
|
|
|
|
|
dense 5 Hz anchor from the same `FROZEN_FEATURE_ROOT`. A large drop isolates
|
|
|
|
|
the problem to sparse supervision; a close result means sparse supervision is
|
|
|
|
|
not sufficient to explain the LoRA regression.
|
|
|
|
|
|
2026-08-23 18:28:07 +08:00
|
|
|
## Thinker-LoRA sampling and cache policy
|
2026-08-23 15:29:26 +08:00
|
|
|
|
2026-08-23 18:28:07 +08:00
|
|
|
This is an implemented second-stage experiment, not yet a reported benchmark
|
|
|
|
|
result.
|
2026-08-23 15:29:26 +08:00
|
|
|
Dense 5 Hz partial-prefix caching is fast during training but does not fit the
|
|
|
|
|
combined EasyCom and Ego4D storage budget. The 24-second Session 7 pilot used
|
2026-08-23 18:28:07 +08:00
|
|
|
0.115 GiB, which extrapolates to about 17.25 GiB per media hour. In the latest
|
|
|
|
|
controlled single-L20 test without gradient checkpointing, 200 updates took
|
|
|
|
|
57.718 s (3.465 update/s) with every partial cached, 130.151 s (1.537
|
|
|
|
|
update/s) with current partials recomputed using processor defaults, and
|
|
|
|
|
111.146 s (1.799 update/s) after CUDA log-Mel and bounded-audio-padding
|
|
|
|
|
optimization. The optimized online run spent 53.408 s in 163 frozen-AV calls;
|
|
|
|
|
the cached run therefore remains the preferred training path when storage is
|
|
|
|
|
available.
|
2026-08-23 15:29:26 +08:00
|
|
|
|
|
|
|
|
The proposed training sampler is therefore:
|
|
|
|
|
|
|
|
|
|
1. retain a uniform low-rate set of ordinary anchors, initially 1 Hz;
|
|
|
|
|
2. retain every 5 Hz anchor within a two-second band around annotated speaker
|
|
|
|
|
onset, offset, handover, and overlap-state transitions;
|
|
|
|
|
3. optionally add high-loss or high-uncertainty anchors after a first training
|
|
|
|
|
pass, while selecting them only from the training split;
|
|
|
|
|
4. cache exact partial AV embeddings only for these selected anchors, plus one
|
|
|
|
|
reusable complete embedding for every required 2-second history block;
|
|
|
|
|
5. enforce a 240 GiB cache target and a hard 300 GB upper bound, leaving room
|
|
|
|
|
for manifests, checkpoints, and extracted evaluation states.
|
|
|
|
|
|
|
|
|
|
Transition-centered selection is training-example sampling, not inference
|
|
|
|
|
look-ahead: model inputs must still end at the current time. Final extraction,
|
|
|
|
|
head training, and Table 2 evaluation remain on the complete 5 Hz timeline.
|
|
|
|
|
The paper must report the sampling rule and compare uniform-only sampling with
|
|
|
|
|
transition-enhanced sampling so that any gain is not attributed solely to
|
|
|
|
|
additional transition examples.
|
|
|
|
|
|
2026-08-23 18:28:07 +08:00
|
|
|
`cache_omni_av_pieces.py --anchor-policy transition-sparse` implements this
|
|
|
|
|
policy without changing the trainer cache schema. A transition is any change
|
|
|
|
|
in the three-class EasyCom target vector. The default two-second dense band is
|
|
|
|
|
defined explicitly as one second before through one second after that target
|
|
|
|
|
row. Every complete two-second source block is still cached once as reusable
|
|
|
|
|
history. Ordinary selected anchors and transition-band anchors cache only
|
|
|
|
|
their exact current causal phase. Label information selects training examples
|
|
|
|
|
offline but never changes the media endpoint supplied to Qwen.
|
|
|
|
|
|
|
|
|
|
Inspect the selection counts for the 24-second pilot without loading media or
|
|
|
|
|
the model:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python cache_omni_av_pieces.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--easycom-session-dir /data1/yfzx/wangx/Datasets/easycom/extracted_easycom/Main/Video_Compressed/Session_7 \
|
|
|
|
|
--target /data1/yfzx/wangx/Datasets/easycom/easycom_target_frame/Session_7.npy \
|
|
|
|
|
--output-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_7_24s \
|
|
|
|
|
--end 24 \
|
|
|
|
|
--anchor-policy transition-sparse \
|
|
|
|
|
--ordinary-anchor-hz 1 \
|
|
|
|
|
--transition-before-seconds 1 \
|
|
|
|
|
--transition-after-seconds 1 \
|
|
|
|
|
--selection-only
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
After auditing the counts, remove `--selection-only` and add the normal AV
|
|
|
|
|
extraction options. Use `--end 0` only after the 24-second cache and replay
|
|
|
|
|
verification pass; zero selects the complete target timeline. The resulting
|
|
|
|
|
manifest contains only selected training anchors, so `train_omni_turn_lora.py`
|
|
|
|
|
uses the sparse dataset automatically and must be run without
|
|
|
|
|
`--online-current-tail`.
|
|
|
|
|
|
2026-08-23 22:41:39 +08:00
|
|
|
### Multi-GPU Thinker-LoRA training
|
|
|
|
|
|
|
|
|
|
Fully cached AV training supports native single-node DDP through `torchrun`.
|
|
|
|
|
Each rank holds one complete 3B Thinker replica, reads a disjoint shard of the
|
|
|
|
|
selected cache anchors, and synchronizes only the trainable LoRA and turn-head
|
|
|
|
|
gradients. Model checkpoint loading is deliberately staggered by rank so four
|
|
|
|
|
processes do not read all checkpoint shards simultaneously. During gradient
|
|
|
|
|
accumulation, DDP synchronization occurs only on the final microbatch. Only
|
|
|
|
|
rank zero writes the adapter, head, summary, and progress log.
|
|
|
|
|
|
|
|
|
|
For four GPUs, use per-GPU batch 2 and accumulation 2 for global batch 16:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0,1,2,3 \
|
|
|
|
|
OMP_NUM_THREADS=2 \
|
|
|
|
|
MKL_NUM_THREADS=2 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=2 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
torchrun --standalone --nproc_per_node=4 train_omni_turn_lora.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_7 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_11 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_10 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_6 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_9 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_4 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_8 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_5 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_12 \
|
|
|
|
|
--output-dir checkpoints/omni_turn_lora_easycom_sparse_8s_ddp4 \
|
|
|
|
|
--max-steps 10170 \
|
|
|
|
|
--batch-size 2 \
|
|
|
|
|
--gradient-accumulation-steps 2 \
|
|
|
|
|
--num-workers 0 \
|
|
|
|
|
--block-cache-size 32 \
|
|
|
|
|
--learning-rate 2e-5 \
|
|
|
|
|
--head-learning-rate 1e-4 \
|
|
|
|
|
--warmup-steps 500 \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--log-every 20
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`10170` is only the current three-epoch estimate for roughly 54,000 selected
|
|
|
|
|
anchors. Recompute it from the completed manifests as
|
|
|
|
|
`ceil(num_examples / 16) * 3`. Do not pass `--device`: under `torchrun`, each
|
|
|
|
|
process uses its `LOCAL_RANK`. DDP intentionally rejects
|
|
|
|
|
`--online-current-tail`; the online producer-consumer path remains a separate
|
|
|
|
|
benchmark.
|
|
|
|
|
|
2026-08-24 15:24:53 +08:00
|
|
|
### Dense low-drift LoRA probe
|
|
|
|
|
|
|
|
|
|
The first follow-up to the sparse-LoRA regression should remove sparse
|
|
|
|
|
supervision as a variable and constrain adaptation near the Thinker output.
|
|
|
|
|
Use dense 5-Hz caches, reserve `Session_12` as development data, and train on
|
|
|
|
|
`Session_4` through `Session_11`. `--lora-last-n-layers 8` discovers the
|
|
|
|
|
language-layer indices and stores PEFT `layers_to_transform=[28,...,35]` for
|
|
|
|
|
the 36-layer Qwen2.5-Omni-3B Thinker. Combining it with
|
|
|
|
|
`--lora-target-modules q_proj,v_proj,o_proj` reduces the adapter from all 252
|
|
|
|
|
attention/MLP projections to 24 output-side attention projections.
|
|
|
|
|
|
|
|
|
|
Start with the successful frozen 8-second head fixed. `--freeze-head`
|
|
|
|
|
requires `--head-checkpoint`, excludes the head from DDP and AdamW, but still
|
|
|
|
|
backpropagates through it into the Thinker LoRA. This isolates representation
|
|
|
|
|
adaptation from head decision-boundary drift. Do not continue from a regressed
|
|
|
|
|
adapter: create this adapter from the original Thinker.
|
|
|
|
|
|
|
|
|
|
For four GPUs with global batch 16, compute exactly one epoch from the dense
|
|
|
|
|
manifest anchor counts and run:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cd /data1/yfzx/wangx/Code/icassp2027-experiment
|
|
|
|
|
|
|
|
|
|
MAX_STEPS=$(python - <<'PY'
|
|
|
|
|
import json
|
|
|
|
|
import math
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
root = Path("/data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s")
|
|
|
|
|
sessions = (4, 5, 6, 7, 8, 9, 10, 11)
|
|
|
|
|
examples = sum(len(json.loads((root / f"Session_{s}" / "manifest.json").read_text())["anchors"])
|
|
|
|
|
for s in sessions)
|
|
|
|
|
print(math.ceil(examples / 16))
|
|
|
|
|
PY
|
|
|
|
|
)
|
|
|
|
|
WARMUP_STEPS=$(( (MAX_STEPS + 19) / 20 ))
|
|
|
|
|
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0,1,2,3 \
|
|
|
|
|
OMP_NUM_THREADS=2 \
|
|
|
|
|
MKL_NUM_THREADS=2 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=2 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
torchrun --standalone --nproc_per_node=4 train_omni_turn_lora.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_4 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_5 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_6 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_7 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_8 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_9 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_10 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_11 \
|
|
|
|
|
--head-checkpoint checkpoints/easycom_open_stream_2fps_8s_head_seed0.pth \
|
|
|
|
|
--freeze-head \
|
|
|
|
|
--lora-target-modules q_proj,v_proj,o_proj \
|
|
|
|
|
--lora-last-n-layers 8 \
|
|
|
|
|
--lora-rank 8 \
|
|
|
|
|
--lora-alpha 16 \
|
|
|
|
|
--learning-rate 5e-6 \
|
|
|
|
|
--output-dir checkpoints/omni_turn_lora_dense_8s_last8_qvo_frozen_head_epoch1 \
|
|
|
|
|
--max-steps "${MAX_STEPS}" \
|
|
|
|
|
--batch-size 2 \
|
|
|
|
|
--gradient-accumulation-steps 2 \
|
|
|
|
|
--num-workers 0 \
|
|
|
|
|
--block-cache-size 32 \
|
|
|
|
|
--warmup-steps "${WARMUP_STEPS}" \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--log-every 20
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Select this recipe on dense `Session_12` using 0.2-second mAP as the primary
|
|
|
|
|
metric and mean mAP as the tie-breaker. Only after the recipe is fixed should
|
|
|
|
|
it be evaluated on official test `Sessions_1--3`. A second controlled run may
|
|
|
|
|
omit `--freeze-head` and use `--head-learning-rate 1e-5`; all other settings
|
|
|
|
|
must remain identical.
|
|
|
|
|
|
2026-08-24 15:31:44 +08:00
|
|
|
For a continuous five-epoch run, set
|
|
|
|
|
`TOTAL_STEPS=$((5 * MAX_STEPS))` and pass
|
|
|
|
|
`--max-steps "${TOTAL_STEPS}" --save-every-steps "${MAX_STEPS}"`. The trainer
|
|
|
|
|
keeps one AdamW/cosine-scheduler trajectory and writes evaluation-ready
|
|
|
|
|
`adapter/` plus `turn_head.pt` pairs under `checkpoint-step-MAX_STEPS`,
|
|
|
|
|
`checkpoint-step-2*MAX_STEPS`, through the fifth epoch. The top-level adapter
|
|
|
|
|
and head duplicate the final fifth checkpoint. Evaluate all five intermediate
|
|
|
|
|
checkpoints only on held-out `Session_12`, select one, then run that checkpoint
|
|
|
|
|
once on `Sessions_1--3`.
|
|
|
|
|
|
2026-08-24 11:32:03 +08:00
|
|
|
### Frozen-Thinker sparse head-only control
|
|
|
|
|
|
|
|
|
|
To determine whether the regression comes from transition-sparse sampling or
|
|
|
|
|
from updating the Thinker representation, rerun the same sparse-anchor
|
|
|
|
|
training with `--freeze-thinker`. This still forwards every reconstructed
|
|
|
|
|
8-second sequence through the original Thinker, but runs that forward under
|
|
|
|
|
`no_grad` and updates only the turn head initialized from the frozen 8-second
|
|
|
|
|
checkpoint. It does not create an adapter directory.
|
|
|
|
|
|
|
|
|
|
For a schedule-matched comparison with a one-epoch LoRA run at global batch
|
|
|
|
|
16, use the same cache directories, head learning rate, warmup, seed, and
|
|
|
|
|
approximately `ceil(num_examples / 16)` updates:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0,1,2,3 \
|
|
|
|
|
OMP_NUM_THREADS=2 \
|
|
|
|
|
MKL_NUM_THREADS=2 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=2 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
torchrun --standalone --nproc_per_node=4 train_omni_turn_lora.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_7 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_11 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_10 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_6 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_9 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_4 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_8 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_5 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_sparse_8s/Session_12 \
|
|
|
|
|
--freeze-thinker \
|
|
|
|
|
--head-checkpoint checkpoints/easycom_open_stream_2fps_8s_head_seed0.pth \
|
|
|
|
|
--output-dir checkpoints/easycom_sparse_head_only_from_frozen_8s_ddp4_epoch1 \
|
|
|
|
|
--max-steps 3390 \
|
|
|
|
|
--batch-size 2 \
|
|
|
|
|
--gradient-accumulation-steps 2 \
|
|
|
|
|
--num-workers 0 \
|
|
|
|
|
--block-cache-size 32 \
|
|
|
|
|
--head-learning-rate 1e-4 \
|
|
|
|
|
--warmup-steps 500 \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--log-every 20
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Evaluate the resulting head on the same dense test caches without an adapter:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0,1,2 \
|
|
|
|
|
OMP_NUM_THREADS=1 \
|
|
|
|
|
MKL_NUM_THREADS=1 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=1 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
TORCH_NCCL_ASYNC_ERROR_HANDLING=1 \
|
|
|
|
|
torchrun --standalone --nproc_per_node=3 eval_omni_turn_lora.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_3 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_2 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_1 \
|
|
|
|
|
--head-checkpoint checkpoints/easycom_sparse_head_only_from_frozen_8s_ddp4_epoch1/turn_head.pt \
|
|
|
|
|
--output-dir results/easycom_sparse_head_only_dense_eval_epoch1 \
|
|
|
|
|
--batch-size 2 \
|
|
|
|
|
--block-cache-size 32 \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--ddp-timeout-minutes 60
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If this head-only control also falls well below the frozen `58.16` dense-cache
|
|
|
|
|
control, sparse sampling/head optimization is sufficient to explain the
|
|
|
|
|
regression. If it stays near `58.16` while LoRA falls, the main failure is
|
|
|
|
|
Thinker representation drift.
|
|
|
|
|
|
2026-08-23 23:49:57 +08:00
|
|
|
To continue from a completed run, pass both its `adapter/` directory through
|
|
|
|
|
`--adapter-checkpoint` and its `turn_head.pt` through `--head-checkpoint` while
|
|
|
|
|
writing to a new output directory. This is weights-only continuation: the
|
|
|
|
|
adapter and head weights resume, but AdamW moments and the cosine scheduler are
|
|
|
|
|
reinitialized. Preserve the previous global batch when changing GPU count;
|
|
|
|
|
for example, change 8 GPUs with batch 2 and accumulation 1 to 4 GPUs with batch
|
|
|
|
|
2 and accumulation 2. Use zero warmup and a reduced learning rate for a short
|
|
|
|
|
post-hoc continuation because a one-epoch cosine schedule has already reached
|
|
|
|
|
zero.
|
|
|
|
|
|
2026-08-24 00:27:42 +08:00
|
|
|
### Evaluate a Thinker-LoRA checkpoint
|
|
|
|
|
|
|
|
|
|
Use `eval_omni_turn_lora.py` for the formal EasyCom Table 2 evaluation. It
|
|
|
|
|
loads the trained Thinker LoRA and `10 x 3` turn head, reconstructs every causal
|
|
|
|
|
state from dense AV caches, and reports both the EgoSpeak full-timeline and
|
|
|
|
|
strict causal-valid protocols. Sparse training caches are rejected because
|
|
|
|
|
they do not contain predictions at every 200 ms timestamp.
|
|
|
|
|
|
|
|
|
|
The following command evaluates the official EasyCom test sessions on three
|
|
|
|
|
GPUs, with one complete session assigned to each rank:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
CHECKPOINT=/data1/yfzx/wangx/Code/icassp2027-experiment/checkpoints/omni_turn_lora_easycom_sparse_8s_ddp8_epoch1
|
|
|
|
|
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0,1,2 \
|
|
|
|
|
OMP_NUM_THREADS=1 \
|
|
|
|
|
MKL_NUM_THREADS=1 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=1 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
TORCH_NCCL_ASYNC_ERROR_HANDLING=1 \
|
|
|
|
|
torchrun --standalone --nproc_per_node=3 eval_omni_turn_lora.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_3 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_2 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_1 \
|
|
|
|
|
--adapter-checkpoint "${CHECKPOINT}/adapter" \
|
|
|
|
|
--head-checkpoint "${CHECKPOINT}/turn_head.pt" \
|
|
|
|
|
--output-dir results/omni_turn_lora_easycom_sparse_8s_ddp8_epoch1 \
|
|
|
|
|
--batch-size 2 \
|
|
|
|
|
--block-cache-size 32 \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--ddp-timeout-minutes 60
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The evaluator saves per-session probabilities and valid masks under `pred/`
|
|
|
|
|
and `valid/`, and writes the aggregate metrics to `metrics.json`. Add
|
|
|
|
|
`--overwrite` only when intentionally replacing an existing result directory.
|
|
|
|
|
Treat Sessions 1, 2, and 3 as the official test set: inspect their metrics only
|
|
|
|
|
after the training recipe and hyperparameters have been fixed.
|
|
|
|
|
|
2026-08-24 01:07:42 +08:00
|
|
|
To isolate whether a score change comes from LoRA or from the dense-cache
|
|
|
|
|
reconstruction path, run the same evaluator without `--adapter-checkpoint` and
|
|
|
|
|
load the original frozen 8-second head:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
FROZEN_HEAD=checkpoints/easycom_open_stream_2fps_8s_head_seed0.pth
|
|
|
|
|
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0,1,2 \
|
|
|
|
|
OMP_NUM_THREADS=1 \
|
|
|
|
|
MKL_NUM_THREADS=1 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=1 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
TORCH_NCCL_ASYNC_ERROR_HANDLING=1 \
|
|
|
|
|
torchrun --standalone --nproc_per_node=3 eval_omni_turn_lora.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_3 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_2 \
|
|
|
|
|
--cache-dir /data1/yfzx/wangx/Datasets/easycom/omni_lora_cache_dense_8s/Session_1 \
|
|
|
|
|
--head-checkpoint "${FROZEN_HEAD}" \
|
|
|
|
|
--output-dir results/easycom_frozen_8s_dense_cache_control \
|
|
|
|
|
--batch-size 2 \
|
|
|
|
|
--block-cache-size 32 \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--ddp-timeout-minutes 60
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This control should approach the previously reported frozen 8-second score of
|
|
|
|
|
58.51937 full-timeline mean mAP. A material mismatch indicates a representation
|
|
|
|
|
difference between dense-cache reconstruction and the earlier open-stream
|
|
|
|
|
feature files; a close match isolates the regression to LoRA/head training.
|
|
|
|
|
|
2026-08-24 09:52:56 +08:00
|
|
|
### Recorded sparse-LoRA results
|
|
|
|
|
|
|
|
|
|
The current single-run EasyCom results confirm negative transfer from this
|
|
|
|
|
sparse LoRA recipe:
|
|
|
|
|
|
|
|
|
|
| Checkpoint | Full-timeline mean mAP | Strict causal mean mAP |
|
|
|
|
|
|---|---:|---:|
|
|
|
|
|
| Frozen Thinker dense-cache control | 58.16091 | 58.17146 |
|
2026-08-24 12:02:49 +08:00
|
|
|
| Frozen states + sparse supervision, 50 epochs | 57.20379 | 57.21367 |
|
2026-08-24 09:52:56 +08:00
|
|
|
| Sparse LoRA epoch1 | 56.52480 | 56.53450 |
|
2026-08-24 12:02:49 +08:00
|
|
|
| Frozen-head initialized LoRA, 3 epochs | 56.19608 | 56.20535 |
|
2026-08-24 09:52:56 +08:00
|
|
|
| Sparse LoRA weights-only epoch2 | 54.19850 | 54.20711 |
|
|
|
|
|
|
|
|
|
|
Epoch2 is a weights-only continuation: adapter/head weights resume, but AdamW
|
|
|
|
|
and the scheduler restart. It is 2.32630 mAP points below epoch1 and 3.96241
|
|
|
|
|
below the frozen dense-cache control in the full-timeline protocol. The largest
|
|
|
|
|
regression is camera-wearer speaking. Do not continue the same recipe to a
|
|
|
|
|
third run. The complete snapshot and per-class metrics are archived in
|
|
|
|
|
[`results/easycom_omni_lora_sparse_epoch2_2026-08-24/`](results/easycom_omni_lora_sparse_epoch2_2026-08-24/).
|
|
|
|
|
|
2026-08-24 12:02:49 +08:00
|
|
|
The frozen-state sparse-supervision and frozen-head initialized three-epoch
|
|
|
|
|
LoRA tables are archived in
|
|
|
|
|
[`results/easycom_sparse_supervision_vs_lora_2026-08-24/`](results/easycom_sparse_supervision_vs_lora_2026-08-24/README.md).
|
|
|
|
|
At 0.2 seconds, sparse supervision alone changes mAP from `71.69695` to
|
|
|
|
|
`71.69194` (effectively unchanged), while the three-epoch LoRA result is
|
|
|
|
|
`71.08589`. This isolates the short-horizon regression primarily to Thinker
|
|
|
|
|
updating rather than sparse target selection.
|
|
|
|
|
|
2026-08-23 15:29:26 +08:00
|
|
|
For a speed-only alternative, `train_omni_turn_lora.py` supports a two-GPU
|
|
|
|
|
producer-consumer benchmark. The producer GPU runs the frozen AV towers and
|
|
|
|
|
places current-tail embeddings in a bounded CPU queue while the consumer GPU
|
|
|
|
|
runs Thinker LoRA. This preserves exact online partial semantics; it changes
|
|
|
|
|
only scheduling. Compare its reported update rate and consumer queue-wait
|
|
|
|
|
time with the synchronous 0.787 update/s pilot before adopting it.
|
|
|
|
|
|
2026-08-16 00:02:58 +08:00
|
|
|
## EgoSpeak-compatible evaluation
|
|
|
|
|
|
2026-08-16 22:19:43 +08:00
|
|
|
Run inference on the official test sessions (`Session_3`, `Session_2`, and
|
|
|
|
|
`Session_1`):
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python infer_turn_head.py \
|
2026-08-17 15:17:11 +08:00
|
|
|
--feature-root features/open_2fps \
|
2026-08-16 22:19:43 +08:00
|
|
|
--target-root target_frame \
|
2026-08-17 15:17:11 +08:00
|
|
|
--checkpoint checkpoints/open_2fps_head.pth \
|
|
|
|
|
--output-dir results/open_2fps_head \
|
2026-08-16 22:19:43 +08:00
|
|
|
--device cuda:0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
This writes `pred/Session_X.npy` with the exact LSTR/EgoSpeak layout
|
|
|
|
|
`[target_time, class, future_horizon]`, i.e. `[T,3,10]`, plus `metrics.json`.
|
|
|
|
|
For every valid current timestep, the head itself produces probabilities
|
|
|
|
|
shaped:
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
[number of timesteps, 10 future horizons, 3 classes]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then reproduce EgoSpeak's Table 2 procedure:
|
|
|
|
|
|
|
|
|
|
1. for each horizon independently, concatenate test-session predictions and
|
|
|
|
|
ground truth;
|
|
|
|
|
2. compute per-class frame-level average precision;
|
|
|
|
|
3. average the three class AP values to obtain horizon mAP;
|
|
|
|
|
4. report all ten horizon mAP values;
|
|
|
|
|
5. average the ten horizon mAP values for the final score.
|
|
|
|
|
|
|
|
|
|
With EgoSpeak's `ignore_index=-100`, the released evaluator does not remove
|
2026-08-16 22:19:43 +08:00
|
|
|
class index zero, so background is included. The script reports two protocols:
|
|
|
|
|
|
|
|
|
|
- `egospeak_full_timeline`: the same full `[T,3,10]` array and evaluator as
|
|
|
|
|
LSTR, with zeros where a prediction would require pre-session context;
|
|
|
|
|
- `causal_valid_only`: excludes only those unavailable first `h` rows for
|
|
|
|
|
horizon `h`. This is the primary strictly causal score.
|
|
|
|
|
|
|
|
|
|
The difference involves at most the first ten rows of each session, but both
|
|
|
|
|
are saved so the comparison is explicit.
|
|
|
|
|
|
|
|
|
|
The convenience wrapper mirrors EgoSpeak's train/inference shells:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
chmod +x run_easycom_head_experiment.sh
|
|
|
|
|
CUDA_VISIBLE_DEVICES=0 ./run_easycom_head_experiment.sh all
|
|
|
|
|
```
|
2026-08-16 00:02:58 +08:00
|
|
|
|
|
|
|
|
Reference Table 2 targets from the EgoSpeak paper:
|
|
|
|
|
|
|
|
|
|
- EasyCom GRU A+V: **60.6 mean mAP**;
|
|
|
|
|
- EasyCom Transformer A+V: **58.7 mean mAP**;
|
|
|
|
|
- Ego4D Transformer audio: **69.2 mean mAP**;
|
|
|
|
|
- Ego4D Transformer A+V: **69.0 mean mAP**;
|
|
|
|
|
- Ego4D GRU audio: **69.2 mean mAP**;
|
|
|
|
|
- Ego4D GRU A+V: **68.2 mean mAP**.
|
|
|
|
|
|
|
|
|
|
Do not compare these numbers with EgoSpeak Table 3 target-speaker-only AP;
|
|
|
|
|
Table 3 uses a different metric.
|
|
|
|
|
|
|
|
|
|
## Experiments that demonstrate Omni's semantic advantage
|
|
|
|
|
|
|
|
|
|
The headline comparison alone cannot establish that improvements come from
|
|
|
|
|
semantics. Run the following controlled experiments:
|
|
|
|
|
|
|
|
|
|
1. **Omni audio vs Omni audio+video**: measure whether visual grounding helps.
|
|
|
|
|
2. **Per-horizon curve**: semantic completion should be especially visible at
|
|
|
|
|
longer horizons such as 1.2--2.0 seconds.
|
|
|
|
|
3. **Context length**: compare 4, 10, 15, and 20 seconds without adding an RNN.
|
|
|
|
|
4. **Layer ablation**: compare audio-encoder output and several Thinker layers,
|
|
|
|
|
because an intermediate layer may be more discriminative than the final
|
|
|
|
|
generation-oriented layer.
|
|
|
|
|
5. **Semantic-completion subset**: contrast completed utterances with
|
|
|
|
|
hesitation/incomplete pauses while matching pause duration and basic
|
|
|
|
|
prosody as closely as possible.
|
|
|
|
|
6. **Question/statement/backchannel subsets**: report target-speaker AP and
|
|
|
|
|
premature-interruption rate on linguistically meaningful events.
|
|
|
|
|
7. **Causal transcript baseline**: compare Omni audio with causal ASR text plus
|
|
|
|
|
a language model to isolate lexical/semantic contribution.
|
|
|
|
|
|
|
|
|
|
The frozen-Omni head is the cleanest representation test. LoRA can be reported
|
|
|
|
|
as a second-stage ceiling, not as the only result.
|
|
|
|
|
|
2026-08-23 23:49:57 +08:00
|
|
|
## Candidate improvement: explicit working and long memory
|
|
|
|
|
|
|
|
|
|
Motivated by EgoSpeak's separation of long-term and working memory, test an
|
|
|
|
|
explicit two-timescale representation instead of compressing the entire causal
|
|
|
|
|
window into one undifferentiated final Thinker state. At prediction time `t`,
|
|
|
|
|
align long memory to fixed global 2-second boundaries and let working memory
|
|
|
|
|
slide with the current prediction time:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
b = floor(t / 2 s) * 2 s
|
|
|
|
|
long memory: complete 2-second chunks ending at b, ..., [b-4, b-2], [b-2, b]
|
|
|
|
|
working memory: the sliding recent interval [t-2, t], updated every 0.2 s
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For example, at `t = 20.4 s`, long memory contains the fixed chunks
|
|
|
|
|
`[16,18]`, `[18,20]`, and any earlier retained chunks, while the working
|
|
|
|
|
partial is `[18.4,20.4]`. The overlap between `[18,20]` and `[18.4,20.4]` is
|
|
|
|
|
intentional: long memory provides a stable completed-chunk representation,
|
|
|
|
|
whereas working memory preserves the exact high-resolution context ending at
|
|
|
|
|
the current prediction time. Cache one representation per completed
|
|
|
|
|
long-memory chunk, recompute the working-memory representation at each 5 Hz
|
|
|
|
|
prediction step, and fuse the two branches with a small gated/attention
|
|
|
|
|
pooling module before the same `10 x 3` prediction head. Keep the Omni
|
|
|
|
|
backbone frozen in the first test so that any gain can be attributed to memory
|
|
|
|
|
organization rather than additional backbone capacity.
|
|
|
|
|
|
|
|
|
|
This is a candidate score-improvement idea, not yet an experimental result.
|
|
|
|
|
The key hypothesis is that a dedicated recent branch preserves local prosody,
|
|
|
|
|
speaker-change, and utterance-completion cues while long memory supplies
|
|
|
|
|
conversation context without diluting them in one final state. Evaluate it
|
|
|
|
|
against matched-compute baselines: (1) the current single-state 8-second model,
|
|
|
|
|
(2) working-memory-only `[t-2,t]`, and (3) long+working memory with the same
|
|
|
|
|
total causal context. Report parameter count, throughput, and per-horizon and
|
|
|
|
|
per-class AP over identical seeds. A useful result requires the fused model to
|
|
|
|
|
beat both controls consistently; otherwise a gain could merely come from the
|
|
|
|
|
extra fusion parameters or the stronger recent-window shortcut.
|
|
|
|
|
|
2026-08-24 09:52:56 +08:00
|
|
|
### Frozen serialized-recap ablation (implemented)
|
|
|
|
|
|
|
|
|
|
Before adding a learned two-branch fusion module, test a stricter frozen-Omni
|
|
|
|
|
variant with exactly the same 2048-dimensional readout and prediction head as
|
|
|
|
|
the current 8-second baseline. Mode `overlap-working-memory` serializes the two
|
|
|
|
|
timescales inside one still-open Thinker media turn:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
total AV presentation budget = 8 s
|
|
|
|
|
fixed history budget = 6 s = three cached 2 s blocks
|
|
|
|
|
working recap = 2 s = [t-2,t]
|
|
|
|
|
|
|
|
|
|
t = 20.4 s:
|
|
|
|
|
fixed: [14,16] [16,18] [18,20]
|
|
|
|
|
working: [18.4,20.4]
|
|
|
|
|
-> one frozen Thinker state [2048]
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The overlap is intentional. It presents recent evidence twice but does not add
|
|
|
|
|
AV-token duration beyond the matched 8-second budget. Thus this ablation trades
|
|
|
|
|
older unique context for stronger recent evidence instead of silently giving
|
|
|
|
|
the candidate more input tokens. Fixed blocks retain the global half-open 2 FPS
|
2026-08-24 11:17:39 +08:00
|
|
|
grid. The working recap reuses three causal global-grid history frames and
|
|
|
|
|
replaces the oldest visual slot with the current causal frame:
|
2026-08-24 09:52:56 +08:00
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
fixed [18,20): 18.0 18.5 19.0 19.5
|
2026-08-24 11:17:39 +08:00
|
|
|
working [18.4,20.4]: 18.5 19.0 19.5 20.4
|
2026-08-24 09:52:56 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Keeping four frames means the oldest visual tick is replaced by the current
|
|
|
|
|
frame rather than adding a fifth frame; this isolates endpoint recency without
|
2026-08-24 11:17:39 +08:00
|
|
|
increasing visual tokens. Historical target times are causally floored to the
|
|
|
|
|
session-global 2 FPS grid. This lets them share the complete-block frame cache
|
|
|
|
|
and avoids making Decord seek backward through a long-GOP H.264 stream for four
|
|
|
|
|
new endpoint-shifted timestamps every 200 ms. Audio remains the full, exactly
|
|
|
|
|
half-open `[start,end)` slice. Consequently, call this a global-history plus
|
|
|
|
|
current-frame recent-video intervention, not an exact timestamp-preserving
|
|
|
|
|
resampling of both modalities. The recap is serialized as the next TMRoPE
|
|
|
|
|
chunk; it is deliberately a recent-evidence recap, not a claim that duplicated
|
|
|
|
|
samples occur later in physical time. This mode is an ablation, not native Qwen
|
|
|
|
|
streaming semantics, and the Omni backbone stays frozen.
|
2026-08-24 09:52:56 +08:00
|
|
|
|
|
|
|
|
One deliberate side effect is a 2-second phase cycle: overlap falls toward
|
|
|
|
|
0.2 seconds just before a fixed boundary and jumps to 2.0 seconds at the
|
|
|
|
|
boundary. Every output stores this value in
|
|
|
|
|
`overlap_working_memory_policy.layout`. If aggregate mAP changes, also inspect
|
|
|
|
|
performance by `step_index % 10`; a boundary-only gain or failure is a chunk
|
|
|
|
|
phase artifact rather than general evidence for better recent semantics.
|
|
|
|
|
|
|
|
|
|
Run a 24-second L20 smoke test first:
|
|
|
|
|
|
2026-08-24 10:18:55 +08:00
|
|
|
The sliding 2-second recap otherwise invokes Whisper's model-maximum audio
|
|
|
|
|
padding 120 times. Run log-Mel extraction on the model GPU and set explicit
|
|
|
|
|
right padding to zero. Every EasyCom 0.2-second increment contains 3200 samples
|
|
|
|
|
at 16 kHz and is already divisible by Whisper's 160-sample hop, so no external
|
|
|
|
|
silence is needed. This preserves the feature extractor's own STFT boundary
|
|
|
|
|
handling while avoiding repeated processing of tens of seconds of zeros.
|
|
|
|
|
|
2026-08-24 09:52:56 +08:00
|
|
|
```bash
|
|
|
|
|
mkdir -p features/easycom_overlap_working_8s_smoke
|
|
|
|
|
|
|
|
|
|
CUDA_VISIBLE_DEVICES=3 \
|
|
|
|
|
OMP_NUM_THREADS=4 \
|
|
|
|
|
MKL_NUM_THREADS=4 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=4 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
python streaming_turn_model_directly.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--easycom-session-dir /data1/yfzx/wangx/Datasets/easycom/extracted_easycom/Main/Video_Compressed/Session_7 \
|
|
|
|
|
--end 24 \
|
|
|
|
|
--mode overlap-working-memory \
|
|
|
|
|
--memory-seconds 8 \
|
|
|
|
|
--working-memory-seconds 2 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--frame-cache-size 128 \
|
2026-08-24 10:18:55 +08:00
|
|
|
--audio-feature-device model \
|
|
|
|
|
--audio-right-padding-samples 0 \
|
2026-08-24 09:52:56 +08:00
|
|
|
--decoder decord \
|
|
|
|
|
--decoder-threads 1 \
|
|
|
|
|
--max-open-readers 1 \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--device cuda:0 \
|
2026-08-24 11:17:39 +08:00
|
|
|
--verify-every 0 \
|
|
|
|
|
--output features/easycom_overlap_working_8s_smoke/Session_7_24s_global_history_current.pt
|
2026-08-24 09:52:56 +08:00
|
|
|
```
|
|
|
|
|
|
2026-08-24 11:17:39 +08:00
|
|
|
Expected shape is `(120, 2048)`. This speed smoke test disables replay
|
|
|
|
|
verification; after its throughput is acceptable, rerun a short four-second
|
|
|
|
|
check with `--verify-every 5`, where `overlap_cache_replay` should be
|
|
|
|
|
numerically exact or effectively exact. Inspect the saved timing policy and
|
|
|
|
|
decoder cache counters with:
|
2026-08-24 09:52:56 +08:00
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python - <<'PY'
|
|
|
|
|
import torch
|
|
|
|
|
|
|
|
|
|
p = torch.load(
|
2026-08-24 11:17:39 +08:00
|
|
|
"features/easycom_overlap_working_8s_smoke/Session_7_24s_global_history_current.pt",
|
2026-08-24 09:52:56 +08:00
|
|
|
map_location="cpu",
|
|
|
|
|
weights_only=False,
|
|
|
|
|
)
|
|
|
|
|
print(p["mode"], tuple(p["states"].shape))
|
|
|
|
|
print(p["overlap_working_memory_policy"]["working_media"])
|
2026-08-24 11:17:39 +08:00
|
|
|
print(p["direct_media"]["runtime_counters"])
|
2026-08-24 09:52:56 +08:00
|
|
|
for row in p["overlap_working_memory_policy"]["layout"][98:102]:
|
|
|
|
|
print(row)
|
|
|
|
|
PY
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For one complete session, replace `--end 24` with its target and use a
|
|
|
|
|
session-level output filename:
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-08-24 11:17:39 +08:00
|
|
|
mkdir -p /data1/yfzx/wangx/Datasets/easycom/omni_features/overlap_global_history_current_2fps_8s
|
2026-08-24 09:52:56 +08:00
|
|
|
|
|
|
|
|
CUDA_VISIBLE_DEVICES=3 \
|
|
|
|
|
OMP_NUM_THREADS=4 \
|
|
|
|
|
MKL_NUM_THREADS=4 \
|
|
|
|
|
OPENBLAS_NUM_THREADS=4 \
|
|
|
|
|
PYTORCH_ALLOC_CONF=expandable_segments:True \
|
|
|
|
|
python streaming_turn_model_directly.py \
|
|
|
|
|
--model-id /data1/yfzx/wangx/Models/Qwen2.5-Omni-3B \
|
|
|
|
|
--easycom-session-dir /data1/yfzx/wangx/Datasets/easycom/extracted_easycom/Main/Video_Compressed/Session_7 \
|
|
|
|
|
--target /data1/yfzx/wangx/Datasets/easycom/easycom_target_frame/Session_7.npy \
|
|
|
|
|
--mode overlap-working-memory \
|
|
|
|
|
--memory-seconds 8 \
|
|
|
|
|
--working-memory-seconds 2 \
|
|
|
|
|
--video-fps 2 \
|
|
|
|
|
--frame-cache-size 128 \
|
2026-08-24 10:18:55 +08:00
|
|
|
--audio-feature-device model \
|
|
|
|
|
--audio-right-padding-samples 0 \
|
2026-08-24 09:52:56 +08:00
|
|
|
--decoder decord \
|
|
|
|
|
--decoder-threads 1 \
|
|
|
|
|
--max-open-readers 1 \
|
|
|
|
|
--dtype bfloat16 \
|
|
|
|
|
--flash-attention-2 \
|
|
|
|
|
--device cuda:0 \
|
2026-08-24 11:17:39 +08:00
|
|
|
--output /data1/yfzx/wangx/Datasets/easycom/omni_features/overlap_global_history_current_2fps_8s/Session_7.pt
|
2026-08-24 09:52:56 +08:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
After all train/test sessions have been extracted, train the unchanged frozen
|
|
|
|
|
head by setting only the expected feature mode:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python train_turn_head.py \
|
2026-08-24 11:17:39 +08:00
|
|
|
--feature-root /data1/yfzx/wangx/Datasets/easycom/omni_features/overlap_global_history_current_2fps_8s \
|
2026-08-24 09:52:56 +08:00
|
|
|
--target-root /data1/yfzx/wangx/Datasets/easycom/easycom_target_frame \
|
|
|
|
|
--output checkpoints/easycom_overlap_working_frozen_8s.pt \
|
|
|
|
|
--expected-mode overlap-working-memory \
|
|
|
|
|
--epochs 50 \
|
|
|
|
|
--batch-size 512 \
|
|
|
|
|
--device cuda:0
|
|
|
|
|
|
|
|
|
|
python infer_turn_head.py \
|
2026-08-24 11:17:39 +08:00
|
|
|
--feature-root /data1/yfzx/wangx/Datasets/easycom/omni_features/overlap_global_history_current_2fps_8s \
|
2026-08-24 09:52:56 +08:00
|
|
|
--target-root /data1/yfzx/wangx/Datasets/easycom/easycom_target_frame \
|
|
|
|
|
--checkpoint checkpoints/easycom_overlap_working_frozen_8s.pt \
|
|
|
|
|
--output-dir results/easycom_overlap_working_frozen_8s \
|
|
|
|
|
--expected-mode overlap-working-memory \
|
|
|
|
|
--device cuda:0
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The decisive comparison is against the frozen single-state 8-second baseline
|
|
|
|
|
using the same split, head, optimization, and seeds. Report the full
|
|
|
|
|
per-horizon curve: the hypothesis predicts the clearest gain at 0.2--1.0
|
|
|
|
|
seconds; degradation at 1.2--2.0 seconds would indicate that repeated local
|
|
|
|
|
cues displaced too much useful long context. Only if this parameter-matched
|
|
|
|
|
test is positive should the learned two-branch fusion experiment be added.
|
|
|
|
|
|
2026-08-16 00:02:58 +08:00
|
|
|
## Single-window feature inspection
|
|
|
|
|
|
|
|
|
|
`qwen_omni_features.py` is useful for inspecting a short window or layer, but
|
|
|
|
|
it is not the main whole-session experiment.
|
|
|
|
|
|
|
|
|
|
If audio is embedded in the video:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python qwen_omni_features.py \
|
|
|
|
|
--video /path/to/window.mp4 \
|
|
|
|
|
--use-audio-in-video \
|
|
|
|
|
--output features/window.pt
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If audio and video are separate:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
python qwen_omni_features.py \
|
|
|
|
|
--video /path/to/window.mp4 \
|
|
|
|
|
--audio /path/to/window.wav \
|
|
|
|
|
--output features/window.pt
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Do not provide the same audio both as a separate file and via
|
|
|
|
|
`--use-audio-in-video`.
|
|
|
|
|
|
|
|
|
|
Saved single-window fields are:
|
|
|
|
|
|
|
|
|
|
- `token_features`: `[B, token_length, 2048]`;
|
|
|
|
|
- `last_token`: `[B, 2048]`;
|
|
|
|
|
- `tail_mean`: `[B, 2048]`;
|
|
|
|
|
- `attention_mask`: valid-token mask.
|
|
|
|
|
|
|
|
|
|
## Installation
|
|
|
|
|
|
|
|
|
|
Install `ffmpeg`, then:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
pip install -r requirements.txt
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The implementation is pinned to `transformers==4.52.3` because the
|
2026-08-16 22:19:43 +08:00
|
|
|
open-stream replay path uses version-specific Thinker internals. On Linux,
|
2026-08-16 00:02:58 +08:00
|
|
|
`qwen-omni-utils[decord]` is recommended for faster video decoding. On a
|
|
|
|
|
compatible NVIDIA GPU, optionally install `flash-attn` and pass
|
|
|
|
|
`--flash-attention-2`.
|
|
|
|
|
|
|
|
|
|
## Ego4D Audio-Visual Diarization download
|
|
|
|
|
|
|
|
|
|
Do not download the complete Ego4D release for this experiment. Download only
|
|
|
|
|
the compressed Audio-Visual Diarization benchmark clips and annotations:
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
ego4d \
|
|
|
|
|
--output_directory=/path/to/ego4d \
|
|
|
|
|
--datasets clips annotations \
|
|
|
|
|
--benchmarks AV
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The approximately 1 TB figure on the Ego4D download page refers to all
|
|
|
|
|
benchmark clips combined, not only the AV subset. The annotated AVD subset has
|
|
|
|
|
about 572 five-minute clips (roughly 50 hours). EgoSpeak uses a custom 346/87
|
|
|
|
|
train/test split, about 36.1 hours total. Reproducing its Table 2 requires the
|
|
|
|
|
exact IDs in EgoSpeak's `EGO4DSHUFFLE` split.
|
|
|
|
|
|
|
|
|
|
## Required validation before a full run
|
|
|
|
|
|
|
|
|
|
Do not launch full EasyCom/Ego4D extraction until all of these pass:
|
|
|
|
|
|
|
|
|
|
- no input audio sample or video frame has timestamp later than prediction
|
|
|
|
|
time `t`;
|
|
|
|
|
- `states` and `target_perframe` lengths/time origins are reconciled and the
|
|
|
|
|
chosen label offset is saved;
|
2026-08-16 22:19:43 +08:00
|
|
|
- open-stream cache replay is exactly equivalent to its from-scratch replay on
|
|
|
|
|
short clips;
|
|
|
|
|
- open-stream and causal-prefix predictions are compared on identical clips;
|
2026-08-16 00:02:58 +08:00
|
|
|
- a 20-second run fits the 3090 with the selected pixel budget and attention
|
|
|
|
|
implementation;
|
|
|
|
|
- saved outputs contain exactly ten future horizons in the order 0.2--2.0 s;
|
|
|
|
|
- the local metric reproduces EgoSpeak's evaluator on known predictions;
|
|
|
|
|
- test session IDs never enter training, validation, normalization statistics,
|
|
|
|
|
or hyperparameter selection.
|
|
|
|
|
|
|
|
|
|
Once these checks pass, the first publishable experiment is:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
frozen Qwen2.5-Omni-3B Thinker
|
|
|
|
|
+ trained 10x3 head
|
2026-08-17 15:17:11 +08:00
|
|
|
+ bounded Qwen-style 2-second open streaming
|
|
|
|
|
+ 20-second rolling causal memory
|
2026-08-16 00:02:58 +08:00
|
|
|
+ full-session 5 Hz output
|
|
|
|
|
+ exact EgoSpeak split and Table 2 metric
|
|
|
|
|
```
|
2026-08-19 23:09:56 +08:00
|
|
|
|
|
|
|
|
Repository workflow verified on 2026-08-19.
|