Skip to content

Latest commit

 

History

History
477 lines (346 loc) · 35.9 KB

File metadata and controls

477 lines (346 loc) · 35.9 KB

Image Convolution — Документация

Реализует свёртку (convolution) изображений с различными фильтрами, демонстрируя разные стратегии распараллеливания задачи.


Возможности

  • 24 фильтра четырёх размеров (3×3 … 9×9): каждый размер реализует одинаковый набор типов — Identity, Box Blur, Gaussian Blur, Sharpen, Edges, Emboss
  • 5 стратегий параллелизации: GPU (OpenCL with JOCL), по пикселям, по строкам, по столбцам, по сетке (2D grid с настраиваемой формой)
  • Цепочки фильтров: последовательное применение нескольких фильтров; опциональная математическая композиция в одно ядро
  • Кэш-friendly тайлинг: флаг --tile-size задаёт размер тайла в пикселях для подбора размера сетки под L1/L2 кэш
  • Пайплайн: потоковая пакетная обработка с ридером, N воркерами и врайтером
  • GUI-режим: интерактивный интерфейс на Compose Desktop
  • CLI apply-режим: применить фильтр(ы) и сохранить результат — быстро, один проход
  • CLI benchmark-режим: полный бенчмарк всех стратегий с прогревом JIT и подробной статистикой

Запуск

GUI (по умолчанию)

./gradlew run

Интерфейс разделён на два блока:

Сайдбар (левая панель) — все настройки:

  • Цепочка фильтров — список фильтров, применяемых по порядку. Кнопка «+ Добавить фильтр» добавляет ещё один шаг. Каждый шаг можно удалить кнопкой «×».
  • Объединить в одно ядро — чекбокс появляется при двух и более фильтрах. Показывает итоговый размер составного ядра (например, 5×5 для двух 3×3).
  • Алгоритм — выбор стратегии распараллеливания.
  • Потоки — слайдер 1 до числа ядер (виден для параллельных режимов).
  • Строк/Столбцов сетки — два слайдера 1–16 (видны только при «По сетке»).
  • Кнопка «Применить», время последнего запуска, сообщения об ошибках.

Область изображений (правая часть) — два панели рядом: оригинал и результат. Кнопки «Загрузить» и «Сохранить» находятся под соответствующей панелью.


CLI — apply (одно изображение)

Применяет фильтр(ы) одним проходом и опционально сохраняет результат на диск.

./gradlew run --args='<путь> <фильтр...> [--strategy <стратегия>] [--output <файл>] [--threads N] [--compose] [--tile-size N] [--grid-rows R] [--grid-cols C]'

Пример вывода:

Изображение : photo.png (1920×1080)
Стратегия   : По строкам, 8 потоков
Фильтры     : Gaussian Blur 3×3
Время       : 91 мс
Сохранено   : result.png

CLI — benchmark (одно изображение)

Прогревает JIT, затем делает 30 замеров каждой стратегии и выводит полную статистику.

./gradlew run --args='<путь> [фильтр...] --benchmark [--threads N] [--compose] [--tile-size N] [--grid-rows R] [--grid-cols C] [--kernel-size N[,N…]] [--csv <файл>]'

Пример вывода:

Изображение : photo.png (1920×1080)
Процессоров : 8  |  Потоков : 8

═══ Фильтр: Gaussian Blur 3×3
  прогрев: ▪▪▪▪▪▪▪
  Стратегия              среднее    ±std     p50      p95      p99    CV%   ускор
  ─────────────────────────────────────────────────────────────────────────────
  Последовательный          312 мс   ±8 мс   311 мс   324 мс   340 мс   2.5%
  По пикселям                89 мс   ±3 мс    88 мс    94 мс    99 мс   3.1%   ×3.51
  По строкам                 85 мс   ±2 мс    85 мс    89 мс    93 мс   2.8%   ×3.67
  По столбцам                91 мс   ±4 мс    90 мс    97 мс   103 мс   4.0%   ×3.43
  По сетке (3×3)             88 мс   ±3 мс    87 мс    93 мс    98 мс   3.2%   ×3.55

CLI — pipeline apply (пакетная обработка)

Обрабатывает все изображения в директории (или один файл) потоком через конвейер ридер → воркеры → врайтер.

./gradlew run --args='<путь|директория> <фильтр...> --pipeline [--workers N] [--worker-strategy <стратегия>] [--worker-threads N] [--input-buffer N] [--output-buffer N] [--output <директория>]'

Пример вывода:

Изображений  : 120
Воркеры      : 4  |  Режим: Последовательный
Буферы       : вход=8  выход=8
Фильтры      : Gaussian Blur 3×3
Вывод        : results/

обработано: 120 / 120
Всего        : 4312 мс  (27.8 изобр/с)
чтение     : 890 мс (суммарно)
свёртка    : 14200 мс (суммарно)
запись     : 620 мс (суммарно)

CLI — pipeline benchmark

Прогоняет синтетический батч одного изображения через пайплайн с разным количеством воркеров и показывает масштабирование throughput.

./gradlew run --args='<путь> [фильтр...] --pipeline --benchmark [--workers N] [--batch-size N] [--csv <файл>]'

Пример вывода:

Изображение  : photo.png (1920×1080)
Фильтры      : Gaussian Blur 3×3
Батч         : 16 изображений

  воркеры       изобр/с     всего мс   чтение мс  свёртка мс  запись мс   ускор
  ─────────────────────────────────────────────────────────────────────────────
        1           3.1         5161        420       14820         0    ×1.00
        2           5.9         2702        410        7610         0    ×1.90
        4          11.2         1428        390        3980         0    ×3.61
        8          18.7          855        405        2100         0    ×6.03

Флаги CLI

Общие (apply и benchmark, одно изображение)

Флаг Описание Пример
<путь> Путь к изображению (обязательный) photo.png
[фильтр ...] Один или несколько фильтров — образуют цепочку "Gaussian Blur 3×3" "Sharpen 3×3"
--threads N Максимальное число потоков для стратегий параллелизации --threads 4
--compose Скомпоновать фильтры цепочки в одно ядро перед применением --compose
--tile-size N Размер тайла в пикселях для BY_GRID (вместо --grid-rows/--grid-cols) --tile-size 64
--grid-rows R Явное число строк сетки --grid-rows 2
--grid-cols C Явное число столбцов сетки --grid-cols 8

Только apply (одно изображение)

Флаг Описание Пример
--strategy <s> Стратегия: seq, pixels, rows, cols, grid (по умолчанию: seq) --strategy rows
--output <файл> Путь для сохранения результата (PNG); без флага не сохраняется --output result.png

Только benchmark (одно изображение)

Флаг Описание
--benchmark Включает режим: прогрев JIT + 30 замеров всех стратегий
--kernel-size N[,N…] Ограничить фильтрами указанных размеров (3, 5, 7, 9)
--csv <файл> Сохранить результаты в CSV

Пайплайн (общие для apply и benchmark)

Флаг Описание Пример
--pipeline Включает режим пакетной обработки --pipeline
<путь> Директория с изображениями или путь к одному файлу /images/
--workers N Количество параллельных воркеров-свёртки (по умолчанию: число процессоров) --workers 4
--worker-strategy <s> Стратегия свёртки внутри каждого воркера: те же ключи что у --strategy, включая gpu --worker-strategy gpu
--worker-threads N Потоков внутри воркера при параллельном --worker-strategy --worker-threads 2
--worker-grid-rows R Строк сетки для --worker-strategy grid (0 = авто) --worker-grid-rows 2
--worker-grid-cols C Столбцов сетки для --worker-strategy grid (0 = авто) --worker-grid-cols 4
--gpu-workers K K воркеров из --workers используют GPU; остальные K−N работают по --worker-strategy. При недоступном GPU тихо переходят на CPU --gpu-workers 1
--input-buffer N Ёмкость канала ридер→воркеры; ридер приостанавливается при заполнении (по умолчанию: workers × 2) --input-buffer 8
--output-buffer N Ёмкость канала воркеры→врайтер; воркеры приостанавливаются при заполнении (по умолчанию: workers × 2) --output-buffer 8
--output <директория> Куда сохранять результаты (для pipeline apply) --output results/

Только pipeline benchmark

Флаг Описание Пример
--batch-size N Сколько раз повторить изображение в батче (по умолчанию: 16) --batch-size 32
--workers N Максимальное количество воркеров (бенчмарк проходит 1, 2, 4 … до этого значения) --workers 8
--csv <файл> Сохранить таблицу throughput в CSV --csv out.csv

Все числовые флаги принимают форму --flag N и --flag=N.


Примеры

Apply (одно изображение)

# Последовательно, сохранить результат
./gradlew run --args='photo.png "Gaussian Blur 3×3" --output result.png'

# Параллельно по строкам, 4 потока
./gradlew run --args='photo.png "Gaussian Blur 3×3" --strategy rows --threads 4 --output result.png'

# Цепочка фильтров
./gradlew run --args='photo.png "Gaussian Blur 3×3" "Sharpen 3×3" --strategy rows --output result.png'

# Цепочка скомпонована в одно ядро
./gradlew run --args='photo.png "Gaussian Blur 3×3" "Sharpen 3×3" --compose --strategy rows --output result.png'

# По сетке с тайлом под L1-кэш
./gradlew run --args='photo.png "Gaussian Blur 3×3" --strategy grid --tile-size 90 --output result.png'

# Конкретная форма сетки: 2 строки × 8 столбцов
./gradlew run --args='photo.png "Edges 3×3" --strategy grid --grid-rows 2 --grid-cols 8 --output result.png'

Benchmark (одно изображение)

# Все фильтры, все доступные потоки
./gradlew run --args='photo.png --benchmark'

# Только 3×3 фильтры
./gradlew run --args='photo.png --benchmark --kernel-size 3'

# Несколько размеров
./gradlew run --args='photo.png --benchmark --kernel-size 3,5'

# Один фильтр, не более 4 потоков, сохранить CSV
./gradlew run --args='photo.png "Gaussian Blur 3×3" --benchmark --threads 4 --csv bench.csv'

# Цепочка + сравнение с составным ядром
./gradlew run --args='photo.png "Gaussian Blur 3×3" "Sharpen 3×3" --benchmark --compose'

Apply (GPU)

# GPU-свёртка одного изображения
./gradlew run --args='photo.png "Gaussian Blur 3×3" --strategy gpu --output result.png'

Pipeline apply (пакетная обработка)

# Обработать все изображения из директории, 4 воркера
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 4 --output results/'

# Воркеры сами параллельные (параллельная свёртка внутри каждого воркера)
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 2 --worker-strategy rows --worker-threads 4 --output results/'

# GPU-воркеры: изображения раскидываются по воркерам, свёртка на GPU
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 1 --worker-strategy gpu --output results/'

# Гибрид (4b): 1 GPU-воркер + 3 CPU-воркера обрабатывают разные изображения параллельно
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 4 --gpu-workers 1 --output results/'

# Ограничить память: маленькие буферы при большом батче
./gradlew run --args='/images/ "Gaussian Blur 3×3" --pipeline --workers 4 --input-buffer 4 --output-buffer 4 --output results/'

Pipeline benchmark

# Сравнить throughput при 1, 2, 4, 8 воркерах
./gradlew run --args='photo.png "Gaussian Blur 3×3" --pipeline --benchmark --batch-size 32'

# До 4 воркеров, сохранить результат
./gradlew run --args='photo.png "Gaussian Blur 3×3" --pipeline --benchmark --workers 4 --batch-size 32 --csv pipeline_bench.csv'

Важно: все аргументы программы передаются как одна строка в --args='...'. Не разделяйте их — gradle примет остальное за свои собственные задачи.

Числовые флаги принимают оба формата: --threads 4 и --threads=4 — оба равнозначны.


Фильтры

Граничные пиксели обрабатываются с zero-padding (выход за границу считается нулём).

Каждый размер реализует одинаковый набор типов фильтров — удобно сравнивать визуальный эффект и производительность при росте ядра.

Тип фильтра 3×3 5×5 7×7 9×9 Примечание
Identity Не изменяет изображение; все варианты дают одинаковый результат (zero-padding invariance)
Box Blur Все веса = 1/N²; при большем ядре размытие заметно сильнее
Gaussian Blur σ = 0.85 / 1.0 / 1.5 / 2.0; нормированная сумма = 1
Sharpen Ромбовидная «корона» отрицательных весов; центр компенсирует, сумма = 1
Edges Лапласиан; центр = N²−1, остальные = −1, сумма = 0
Emboss Диагональный градиент; сумма = 1

Производительность по размеру

Ядро Операций/пиксель ×3×3
3×3 9
5×5 25 ~3×
7×7 49 ~5×
9×9 81 ~9×

Стратегии параллелизации (одно изображение)

Стратегия CLI-ключ Описание
Последовательный seq Один поток, строка за строкой
GPU (OpenCL) gpu Свёртка выполняется на GPU через OpenCL (JOCL). При отсутствии GPU автоматически падает на CPU-устройство OpenCL.
По пикселям pixels Все пиксели делятся на N чанков, каждый поток обрабатывает свой чанк
По строкам rows Строки изображения делятся между потоками
По столбцам cols Столбцы изображения делятся между потоками
По сетке grid 2D-разбиение на R × C ячеек. Без явных --grid-rows/--grid-cols форма авто: √T × ⌈T/√T⌉. В benchmark дополнительно тестируются полосы 1×T и T×1 для наглядного сравнения кэш-эффектов. Горизонтальные полосы дружелюбнее к row-major данным.

Почему «По столбцам» медленнее «По строкам»

GrayImage хранит пиксели в row-major порядке: пиксели одной строки лежат подряд в памяти, соседние строки — рядом. При обходе по строкам (BY_ROW) поток читает и пишет в непрерывные адреса — HW-prefetcher предсказывает следующий кэш-линии и подтягивает их заранее, промахи кэша минимальны.

При обходе по столбцам (BY_COLUMN) каждый следующий пиксель находится на width × 4 байт дальше в памяти (страйд ~7–28 KB для типичных изображений). Для каждого пикселя процессор обращается к новой кэш-линии, которой ещё нет в кэше, — prefetcher не справляется со скачкообразным паттерном доступа, и возникают постоянные cache miss, которые вынуждают ждать данные из RAM.

BY_ROW   (поток 0 обрабатывает строки 0..k):
  чтение:  [px0, px1, px2, px3, ...]  ← непрерывно, L1/L2 hit
  запись:  [px0, px1, px2, px3, ...]  ← непрерывно, L1/L2 hit

BY_COLUMN (поток 0 обрабатывает столбцы 0..k):
  чтение:  [px0, px_width, px_2*width, ...]  ← страйд width*4 байт, cache miss на каждом шаге
  запись:  [px0, px_width, px_2*width, ...]  ← то же самое

На практике BY_COLUMN проигрывает BY_ROW на ~5–15% при небольших изображениях и заметно больше при изображениях, не помещающихся в L3-кэш (>50–100 МБ), — именно потому, что стоимость каждого cache miss растёт при работе с DRAM.

BY_GRID с квадратными тайлами занимает промежуточное положение: каждый тайл имеет и строчный, и столбцовый обход, но тайл помещается в кэш целиком, поэтому внутри тайла промахов нет.


Пайплайн (потоковая пакетная обработка)

Архитектура

ридер ──[inputChannel]──► воркер 1 ──┐
                         ► воркер 2 ──┼──[outputChannel]──► врайтер
                         ► воркер N ──┘

Все три стадии работают одновременно как отдельные корутины:

  • Ридер (Dispatchers.IO) — читает файлы один за другим и отправляет в inputChannel. Приостанавливается, если воркеры не успевают (канал заполнен).
  • Воркеры (Dispatchers.Default) — берут задания из inputChannel, применяют свёртку, кладут результат в outputChannel. Несколько воркеров позволяют одному ридеру прокормить несколько параллельных свёрток.
  • Врайтер (Dispatchers.IO) — читает готовые результаты из outputChannel и сохраняет PNG. Если записывает медленно, outputChannel заполняется и воркеры тормозят.

Конфигурации воркеров

--workers --gpu-workers --worker-strategy Когда использовать
1 0 seq Ридер быстрее свёртки незначительно; минимальный overhead
N 0 seq Ридер быстрый, свёртка медленная — один ридер кормит N CPU-свёрток
1 0 rows / pixels Один образ, максимально параллельная CPU-свёртка
N 0 rows Ридер быстрый, каждая свёртка параллельная (максимальный CPU)
N 1 seq 1 GPU-воркер + N−1 CPU-воркеров обрабатывают разные изображения параллельно (4b)
N N Только GPU-воркеры; вызовы сериализуются на @Synchronized, N>1 не даёт ускорения — зато ридер и врайтер не простаивают

Архитектура проекта

Data flow

Файл на диске
    │
    ▼
BufferedImage  ──toGrayImage()──▶  GrayImage(width, height, FloatArray)
                                        │
                                        │  для каждого фильтра в pipeline:
                                        │  applyKernelAt(x, y, kernel) → Float
                                        │  (zero-padding на границах)
                                        ▼
                                   GrayImage (результат)
                                        │
                                        │  clamp [0, 255] → Int
                                        ▼
                                   BufferedImage  ──▶  PNG на диск / GUI

Два режима применения фильтров:

  • Pipeline — фильтры применяются последовательно: выход предыдущего становится входом следующего.
  • Compose (--compose) — фильтры математически сворачиваются в одно ядро (composeKernels), затем применяется единственный проход. Результат тот же, количество проходов по изображению — одно.

Структура файлов

src/main/kotlin/
├── Main.kt                       — точка входа: GUI если нет аргументов, иначе диспетчер CLI-команд
├── cli/
│   ├── Cli.kt                    — CliCommand, parseArgs(), parseStrategy(), resolveThreads(), tileToGrid()
│   ├── ApplyCommands.kt          — runApply, runPipelineApply
│   └── BenchmarkCommands.kt      — runBenchmark, runPipelineBenchmark
├── convolution/
│   ├── Convolution.kt            — алгоритмы свёртки (sequential + parallel)
│   ├── GpuConvolution.kt         — GPU-свёртка через OpenCL (JOCL): GpuContext singleton, convolveGpu()
│   ├── Kernels.kt                — реестр всех фильтров (Kernels.all)
│   └── kernels/
│       ├── Kernels3x3.kt
│       ├── Kernels5x5.kt
│       ├── Kernels7x7.kt
│       └── Kernels9x9.kt
├── pipeline/
│   └── ImagePipeline.kt          — runPipeline, PipelineConfig, PipelineStats, collectImagePaths
└── ui/
    ├── App.kt                    — Compose Desktop GUI (только вёрстка)
    └── AppViewModel.kt           — состояние UI и бизнес-логика (загрузка/сохранение, запуск свёртки)

src/test/kotlin/
├── convolution/
│   ├── ConvolutionTest.kt        — тесты задач 1 и 2
│   └── GpuConvolutionTest.kt     — тесты задачи 4a (пропускаются если OpenCL недоступен)
└── pipeline/
    └── PipelineTest.kt           — тесты задач 3 и 4b

Известные особенности

Гибридные CPU (Intel 12th gen+, некоторые ARM): на процессорах с P-ядрами (производительными) и E-ядрами (эффективными) планировщик ОС сам решает, на каких ядрах запускать потоки. При --threads N нет гарантии, что задействуются именно P-ядра — часть потоков может попасть на E-ядра и замедлить результат. Это объясняет повышенный CV% в отдельных запусках бенчмарка.

GPU на Intel Iris Xe (интегрированная графика, без дискретной GPU): замеры задачи 4a проводились на ноутбуке с Intel Core i5-12450H + Intel Iris Xe Graphics. Дискретной GPU нет — OpenCL-устройством выступает встроенная графика. На Linux ICD-лоадер может не найти нужный драйвер автоматически; в этом случае нужно явно указать путь к PoCL:

OCL_ICD_FILENAMES=/etc/OpenCL/vendors/pocl.icd ./gradlew run   # GUI

В CLI (--strategy gpu) OpenCL также работает, но переменная нужна только при запуске GUI через ./gradlew run. На системах с корректно установленным GPU-драйвером (NVIDIA, AMD, Intel Arc) эта переменная не нужна.


Тесты

./gradlew test

После запуска доступны два HTML-отчёта:

Отчёт Путь
Результаты тестов build/reports/tests/test/index.html
Покрытие (JaCoCo) build/reports/jacoco/test/html/index.html

Задача 1 — последовательная свёртка (15 тестов)

Корректность на простых фильтрах (identity, zero kernel), свойство композициональности, zero-expansion invariance, граничные размеры изображений (1×1, 1×N, N×1, изображение равного с ядром размера).

Задача 2 — параллельная свёртка (17 тестов)

Все 4 режима (BY_PIXEL, BY_ROW, BY_COLUMN, BY_GRID) совпадают с последовательным эталоном на 1/2/4/8 потоках, всех 24 предопределённых фильтрах и случайных ядрах 1×1…7×7. Отдельно проверяются крупные ядра (5×5, 7×7, 9×9): граничные размеры, zero-padding invariance, корректность размера и результата композиции.

Задача 3 — пайплайн (10 тестов)

Результат пайплайна совпадает с последовательным эталоном при 1/2/4 воркерах, разных размерах буферов (backpressure), цепочках фильтров и Parallel-режиме воркеров. Дополнительно: корректность статистики, колбэк прогресса, сканирование директории.

Задача 4b — гибридный пайплайн (3 теста)

Гибридный конфиг (gpuWorkerCount > 0) даёт тот же результат, что и sequential-эталон. Проверяется: gpuWorkerCount > workerCount молча обрезается, GPU недоступен — воркеры тихо переходят на CPU без ошибок.

Задача 4a — GPU-свёртка (14 тестов)

Тесты автоматически пропускаются (assumeTrue) если OpenCL недоступен на машине — CI не ломается. Проверяется: корректность на всех 24 предопределённых фильтрах, граничные размеры изображений (1×1, 1×N, N×1), ядра 3×3…9×9, случайные ядра, GPU-конвейер из нескольких фильтров, стабильность при многократных вызовах без утечек.


Требования

  • JVM 21