Lab051: accelerate 500 ksymbol/s streaming receiver

This commit is contained in:
LittleSam129
2026-09-08 16:04:09 +03:00
parent 60691a09f1
commit 9f33607d3a
13 changed files with 9695 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
# Lab051 — вычислительная архитектура потокового BPSK-приёмника
Дата: 8 сентября 2026 года. SOFTWARE ONLY. RF TX = 0.
## Результат и граница вывода
**Программный эксперимент завершён; цели реального времени и свежести не достигнуты.**
В полном шестисекундном потоке получены 240/240 физических пакетов и 30/30 JPEG
с точным содержимым. Обработка 6,044416 с синтетического IQ заняла **38,630132 с**,
отношение **6,391045**. При MAX_VIDEO_AGE=200 мс **FRESH DISPLAY FPS=0**;
не выполнен даже минимальный критерий >0.
Короткий шумный сценарий Lab050 ускорен примерно в **48,21 раза** относительно
исторического замера: 55,157553 с → медиана **1,144176 с** трёх проходов.
Его processing/RF = **3,534193**, поэтому и этот вариант не работает в реальном времени.
Среднее успешного кандидата — **17,7675 мс**; полная стоимость короткого опыта —
**47,674 мс/пакет**, а длинного потока — **160,959 мс/пакет**.
Последние величины включают обработку ложных кандидатов и фона.
Модуляция BPSK, PRBS7, PHY-заголовок, фрагментация, интерливер, FEC R=1/2 K=7,
171/133 octal, soft-метрика и CRC не менялись. GPU и multiprocessing не использовались.
Новых зависимостей нет. Аппаратура не перечислялась и не запускалась.
## Git и аудит до реализации
Исходный main: `cdf5396e807b83f82a264632dc46f71428fcd616`.
Проверены HEAD/status/diff stat/diff check, затем показан точный staged scope.
По прямому разрешению пункта 0 задания создан коммит:
`60691a09f193f36983c7174c5252ed0897c559e9`
`Lab050: model fresh-frame SDR video stream`
В него вошли ровно девять файлов: PROJECT_LOG.md, docs/LABS_INDEX.md,
experiments/lab046c_offline_recovery.py, protocol/video_frame_scheduler.py,
docs/lab050_current_state.md, experiments/lab050_streaming_receiver.py,
experiments/lab050_video_stream.py, tests/test_lab050_video_stream.py,
data/processed/lab050/receiver_benchmarks.json. 1769 добавлений, 12 удалений.
Большой result.json, профили JPEG и посторонние локальные файлы не добавлялись.
Lab051 начата от этого HEAD. Изучены Lab046B/C, Lab047, Lab048, Lab050,
Lab043/044/045 и свёрточный декодер. Ядро protocol в Lab051 не менялось.
Планировщик LATEST_ONLY и ObjectAssembler переиспользованы. Новая видеочередь не создана.
## Профиль до оптимизации
Рабочая точка: 500000 символов/с, Fs=8000000 отсчётов/с, 16 отсчётов/символ.
Профиль C: 128×96 RGB, JPEG quality 25. Короткий контроль: три изображения,
24 пакета, seed=50050, coded-bit Eb/N0=7 дБ, CFO=+10 Гц, фаза 0,73 рад,
чередование полярности, исходное CS8-квантование. Генератор точно прежний, из Lab050.
Путь кандидата: IQ → грубая нормированная корреляция → уточнение старта →
согласованный RRC-фильтр → timing → оценка частотного рассогласования CFO →
компенсация CFO и фазы → мягкие решения → PHY header → deinterleave →
soft Viterbi → protocol parser → CRC. Только полный PHY/FEC/CRC PASS выдаёт пакет.
TX-границы, ожидаемое число кадров и содержимое не входят в RX API.
Историческая база Lab050: 55,157553 / 0,323745 = 170,373647.
Свежий инструментированный baseline Lab051: **49,614327 с**, processing/RF=153,251430.
Настольная нагрузка и инструментация влияют на время; эти замеры не подменяют друг друга.
Все 2374 кандидата: 24 valid, 2350 false. Средние времена — **72,967 / 20,099 мс**.
**Главная причина замедления — fallback timing.** Номинальное положение равно 416,
а диапазон остался −800…+800 отсчётов. Начало выходит за ноль, и прежний быстрый
путь возвращается к Python-перебору 1601 смещения. У ложных кандидатов timing
занял 41,833 с из 47,232 с строгой обработки. Их matched filter — 0,934 с,
уточнение — 2,797 с. Прежняя реализация уже останавливала кандидата при PHY FAIL:
тело, Viterbi и CRC на таких входах не запускались.
Для преамбулы и заголовка достаточно 23456 входных отсчётов и 23616 отсчётов
после RRC. Полный физический пакет с payload 256 байт требует 92256 входных
отсчётов. Длина тела определяется только проверенным PHY-заголовком.
## Изменения вычислений
1. Децимация сохраняет внутреннюю часть конечного импульсного отклика FIR.
При сдвиге буфера заново считаются левый нулевой край и новые правые отсчёты.
Коэффициенты resample_poly/kaiser(8) прежние; сравнение с исходной функцией
побитно точное. Среднее по всему прежнему окну сохраняется. Корреляция считается
только для принадлежащих блоку стартов плюс правый сосед максимума.
2. Корреляция известного prefix для уточнения считается один раз для общего участка.
Кандидаты используют срезы, с прежней поправкой локального DC.
Постоянный probe и неразделимые FFT-пики сохраняют исходный выбор первого
максимума; при необходимости вызывается старое уточнение.
3. Компенсация известного +50 кГц и RRC выполняются для общего участка.
Локальное вычитание среднего и оба нулевых края изолированного окна
восстановлены отдельно. Без этих поправок общая фильтрация меняла бы алгоритм.
Тело за пределами общего участка фильтруется локально только после PHY PASS.
4. Извлечение символов кэшируется по уточнённому положению и длине внутри одного блока.
Усечённое окно не подменяется ранее сохранённым полным окном.
Кэш уничтожается вместе с блоком; история сессии не сохраняется.
5. Timing сохраняет весь прежний диапазон 1601 смещения. Корреляция с разреженной
известной преамбулой и разности накопленных энергий заменяют Python-перебор.
Тело за последним нужным отсчётом не участвует в этом расчёте. При пиках,
неразделимых с точностью float32, возвращается исходный расчёт и его tie-правило.
6. CFO по прежним преамбульным символам не изменён. Векторы переноса кэшируются;
вектор остаточной частоты строится на символах локального окна. Gain/фаза для
probe и полного кадра не объединены: у них различается локальное среднее IQ.
7. Viterbi после устранения главного повтора остался существенной долей принятого
пакета. Предвычислены четыре различные пары евклидовых branch metrics;
две ветви сравниваются непосредственно, с прежним выбором branch 0 при равенстве.
Решётка, старт/финиш 0, шесть хвостовых битов и декодированные биты сохранены.
Дополнительный порог преамбулы/новый критерий отсева не введён. Фиксированный
порог первого каскада **0.030964473262429237** сохранён. В коротком опыте совпали
число кандидатов и все причины отказа с исходным RX. Параллелизм и CUDA не добавлялись.
## Профиль стадий
Ячейка: **число вызовов / суммарные миллисекунды** по соответствующей группе.
Это не длительность одного вызова. Общая работа front-end учитывается в том
кандидате, который впервые запросил её. Фаза показана без вложенного CFO;
время body pipeline не складывается повторно со временем Viterbi/CRC.
| Стадия | Baseline valid (24) | Optimized valid (24) | Baseline false (2350) | Optimized false (2350) |
|---|---:|---:|---:|---:|
| Точное уточнение | 24 / 28.974 | 24 / 93.878 | 2350 / 2797.404 | 2350 / 119.743 |
| Согласованная RRC-фильтрация | 48 / 190.656 | 48 / 102.875 | 2336 / 933.965 | 174 / 26.502 |
| Timing: весь диапазон | 48 / 914.632 | 48 / 43.535 | 2336 / 41833.166 | 174 / 138.588 |
| CFO и комплексное усиление | 48 / 10.838 | 48 / 8.442 | 2336 / 485.772 | 174 / 26.385 |
| Фаза/полярность и диагностика | 48 / 8.994 | 48 / 8.324 | 2336 / 424.756 | 174 / 28.438 |
| PHY header | 48 / 1.711 | 48 / 1.452 | 2298 / 67.136 | 2298 / 32.044 |
| Viterbi | 24 / 484.381 | 24 / 132.119 | 0 / 0 | 0 / 0 |
| Protocol parser / CRC | 24 / 0.270 | 24 / 0.211 | 0 / 0 | 0 / 0 |
Грубая локализация — общая стадия: 36 вызовов, baseline 498,040 мс,
optimized **89,183 мс**. На один false candidate среднее **0,187317 мс**,
медиана **0,060500 мс**; это включает cache hits. Холодное новое окно дороже.
## Массивы и память
Число всех аллокаций внутри NumPy/SciPy C не измерено. Line tracer наблюдает
массивы в локальных переменных проектного Python: число ниже — **нижняя граница**,
включающая views. Владельцы данных выделены отдельно. Временные выражения и
внутренние C-массивы не перечислены. Отдельный tracemalloc-проход измеряет пик
отслеживаемой памяти. Эти медленные проходы не включены в CPU benchmark.
| Кандидат / алгоритм | Наблюдаемые ndarray | Из них владельцы | Суммарные байты наблюдаемых владельцев | Пик tracemalloc, байт |
|---|---:|---:|---:|---:|
| false / baseline | 2888 | 61 | 6612268 | 2104129 |
| false / optimized | 76 | 61 | 5906101 | 5524392 |
| valid / baseline | 14361 | 8704 | 16681890 | 5885299 |
| valid / optimized | 6594 | 6560 | 12345244 | 7771675 |
Пик холодного кандидата вырос из-за общих промежуточных данных: ускорение не
объявляется уменьшением каждого временного массива. В рабочем потоке:
- Пик удерживаемых IQ: **3177728 байт = 3,031 МиБ**.
- Проверяемый предел IQ: 3398912 байт; кэш децимации хранится отдельно.
- Инженерная оценка рабочих массивов: **136,161 МиБ**. Использован запас прежних
22 размеров IQ-окна плюс 20 размеров на новые массивы и кэши. Это оценка для
данного опыта, не доказанный универсальный предел всех аллокаций и не RSS.
- Windows process RSS после потока: **154,105 МиБ**; peak RSS процесса —
**178,004 МиБ**. В него входят Python, библиотеки, генератор, предыдущие замеры и аллокатор.
- Текущий объект ограничен 65536 байтами / 269 фрагментами; C фактически ≤1893 байт.
Длинный тест: **4096 блоков, 33,554432 с модельного времени**. Фон нулевой;
шум отдельно проверен в основном опыте. После прогрева удерживаются 2653440 байт
IQ и 397216 байт децимированных данных; значения не растут.
RSS контрольных точек: **156,199–156,250 МиБ**, без роста вместе с длительностью.
После finish эти буферы освобождены. История JSON относится к отчёту, не к RX.
## Непрерывный поток 5 изображений/с
30 воспроизводимых сдвигов исходного data/raw/lab011_scene.jpg, профиль C,
1740–1893 байта. Это искусственная последовательность, не видео камеры.
Видеопланировщик LATEST_ONLY использует время capture и завершает активное JPEG.
Передатчик знает нижнюю задержку радио/буфера; он не получает будущую стоимость
CPU или результаты приёма. Приёмник получает лишь IQ-блоки по 65536 отсчётов.
Шум, фаза, CFO и полярность заданы, но готовность JPEG берётся из измеренного encode.
Поэтому точное размещение пакетов и число фоновых кандидатов длинного опыта могут
немного меняться между запусками. Короткий baseline использует точно прежний
детерминированный сценарий Lab050.
Время CPU-работника для каждого блока:
`start=max(previous_end, block_arrival); end=start+measured_push_time`.
Сборка/JPEG decode добавляют своё измеренное время. Генерация TX/IQ, SHA-сравнение
с передатчиком и запись JSON исключены. Длительность RX не заменена нулём.
Все данные реально прошли независимый PHY/FEC/CRC и JPEG decode.
| Метрика | Итог |
|---|---:|
| INPUT FPS / FRAMES | 5 / 30 |
| TX IMAGES / PHYSICAL PACKETS | 30 / 240 |
| DROPPED BEFORE TX | 0 |
| MAX WAITING VIDEO FRAMES | 1 |
| RX PHYSICAL PACKETS / IMAGES | 240 / 30 |
| JPEG SHA MATCH | 30 / 30 |
| DROPPED STALE AT DISPLAY | 30 |
| DISPLAYED / FRESH DISPLAYED | 0 / 0 |
| RAW RECEIVED FPS в первых 6 с CPU-шкалы | 0.833333 |
| RAW DISPLAY FPS / FRESH DISPLAY FPS | 0 / 0 |
| Возраст декодированных JPEG: min / median / max | 276.515 / 15691.203 / 31150.009 мс |
| MAX_VIDEO_AGE | 200 мс |
| Строгих кандидатов | 147082 |
| Полное время обработки / IQ | 38.630132 / 6.044416 с |
| PROCESSING / RF TIME | 6.391045 |
Возраст относится к **декодированным, затем отброшенным JPEG**. Показанных изображений нет.
RAW DISPLAY и FRESH DISPLAY совпадают, потому что политика не выводит просроченные
изображения; RAW RECEIVED приведён отдельно.
Физическое время одного JPEG C: 8×5766/500000 = **92,256 мс**. Очередь ожидающих
JPEG в этом режиме не задерживает готовые изображения. Время RX за опыт:
38,619085 с; сборка 5,451 мс; JPEG decode 5,596 мс суммарно на 30 JPEG.
RF и CPU перекрываются по событийной шкале: эти суммы нельзя просто сложить
и назвать измеренной задержкой камеры→экрана. Camera capture latency и display
latency **UNKNOWN**; экран в отчётной модели мгновенный.
Capture lookup используется только **после** независимой сборки JPEG для метрики
возраста/решения модели вывода. В существующем fragment format нет времени съёмки.
Готовый независимый аппаратный контроль возраста этим не доказан; формат в Lab051
не расширялся. ID монотонны в одной конечной сессии; wrap/restart не реализованы.
Внешняя IQ-очередь также не реализована. Офлайн-источник подаёт следующий блок
по запросу и физически не хранит многосекундный backlog. Если бы вход шёл непрерывно,
максимальное отставание **32,632404 с** потребовало бы около **2088473848 байт**
дополнительного complex64 IQ. Поэтому bounded RX state нельзя выдавать за готовую
ограниченную live-систему: при таких затратах потребуются потери/переполнение.
REAL-TIME CAPABLE = NO.
В длительном опыте встречено значительно больше фоновых кандидатов, чем в коротком:
147082 против 2374. Кэш дал 135057 повторных использований, но осталось 12239 новых
окон синхронизации. Следующее ограничение — стоимость этих фоновых событий.
Одного ускорения Viterbi недостаточно; подгонять отсев по TX запрещено.
## Регрессии и независимая проверка
38 целевых тестов: весь диапазон timing, ненулевые и нулевые ties, локальные
DC/края общего RRC, точная децимация при движении окна, Viterbi noisy/tie/invalid,
совпадение старого/нового блочного приёма, усечение, буферы и finally.
Verifier нашёл два случая округления FFT (timing и refine); оба исправлены,
воспроизведены повторно и закреплены тестами. Adversary отдельно проверил
malformed PHY (0 body/Viterbi), дубликаты и старые ID, лимиты объекта,
усечение и восстановление глобальных подмен.
Сохранённая Lab047-запись 20 ксимв/с повторно обработана штатным Lab048:
**36/36, FEC/CRC PASS, JPEG SHA MATCH**, 19 ошибок до FEC, 0 после.
Исходный путь и SHA JPEG сохранены в regression20k/result.json.
Старый режим работает прежним декодером; адаптер Lab051 принимает только 500 ксимв/с
и не подменяет аппаратную проверку этого режима.
Итог: quick_gate PASS — 688 тестов, 17 функциональных проверок, 13 штатных пропусков.
Полный pytest — 688 passed за 72,92 с, 0 failed; одно предупреждение доступа к
`.pytest_cache`. git diff --check PASS. Подробности — в записи 040 журнала.
Управление ELRS/CRSF, локальная защита и ядро protocol не менялись.
## Файлы и воспроизведение
- [Потоковый RX](../experiments/lab051_streaming_receiver.py)
- [Профилировщик](../experiments/lab051_receiver_profile.py)
- [Сценарии и событийная шкала](../experiments/lab051_receiver_performance.py)
- [Тесты](../tests/test_lab051_streaming_receiver.py)
- [Baseline](../data/processed/lab051/baseline_profile.json)
- [Три коротких замера и профиль](../data/processed/lab051/benchmark.json)
- [Основной поток и все 739 блоков](../data/processed/lab051/stream.json)
- [Длинный тест памяти](../data/processed/lab051/memory.json)
- [Наблюдение массивов](../data/processed/lab051/replay.json)
- [20 ксимв/с](../data/processed/lab051/regression20k/result.json)
```powershell
python experiments/lab051_receiver_profile.py
python experiments/lab051_receiver_performance.py --stage all
python -m pytest tests/test_lab051_streaming_receiver.py -q -p no:cacheprovider
python tools/quick_gate.py
python -m pytest tests/ -q
git diff --check
```
Старая запись:
```powershell
python experiments/lab048_receiver_performance.py --run-saved-capture --capture 'C:\Users\user\Documents\SDR_Rover_Lab047_raw\20260903_115747\rx_stream.cs8' --original-result 'C:\Users\user\Documents\SDR_Rover_Lab047_raw\20260903_115747\independent_result.json' --tx-image data/processed/lab047/tx_image.jpg --output-directory data/processed/lab051/regression20k
```
Локальные replay .npy — ограниченные окна, не полная IQ-запись; в Git не добавлены.
Новый полный CS8/IQ файл не создавался. Lab051 git add/commit и push не выполнялись.
Следующий рекомендуемый этап — отдельное программное исследование стоимости
уникальных фоновых кандидатов в полном режиме 5 кадров/с с тем же PHY/FEC/CRC.
RF, QPSK, новый FEC/кодек, 180 МГц и аппаратные 500 ксимв/с не выполнялись. STOP.