Обновить

Реальный time series агрегатор: как обрабатывать 10 событий/сек на графе из 300k узлов

Время на прочтение7 мин
Охват и читатели8.8K
Всего голосов 2: ↑1 и ↓1+1
Комментарии8

Комментарии 8

Не нашел ни в одном примере, как задаются связи между элементами слоя и между слоями

Связи жёстко зашиты в DefaultPhi.FromPrevLayer: каждый узел зависит от [i, i-1, i+1, i+2] предыдущего слоя (кольцевое замыкание). Это не конфигурируется через API, но можно реализовать свой IPhi с любой логикой. Однако если вы меняете радиус зависимости, интервальный пропагатор может дать неоптимальный affected set (он зашит на расширение 2 влево, 1 вправо). Для произвольных графов PhiFlow, скорее всего, не подойдёт — это библиотека для слоистых stencil-пайплайнов.

А объясните тогда, пожалуйста, пример 2 из области GameDev, где провинции и налоги, как это вообще работает? Доход провинции 1 зависит от налога провинций 1, 2, 3, 4? Как именно зависит, с какими коэффициентами?

В текущей реализации DefaultPhi зависимости детерминированные и не имеют настраиваемых коэффициентов. В примере с провинциями коэффициенты были бы единичными (все влияния равны).

Как это работает в коде

Напомню, что DefaultPhi.FromPrevLayer вычисляет значение узла так:

int p0 = prev[i];                      // налог текущей провинции
int p1 = prev[Mod(i - 1, width)];      // налог левой провинции
int p2 = prev[Mod(i + 1, width)];      // налог правой провинции
int p3 = prev[Mod(i + 2, width)];      // налог следующей провинции
uint x = (uint)p0;
x ^= BitOperations.RotateLeft((uint)p1, 13);
x ^= BitOperations.RotateLeft((uint)p2, 29);
x ^= (uint)p3 * 0x9E3779B9u;
for (int k = 0; k < kWork; k++)
    x = Mix32(x + (uint)p0 + ((uint)p1 ^ (uint)p2) + (uint)p3 * (uint)(k + 1));
return ReduceToDomain(x, domainSize);

Что это значит для примера «провинции и налоги»

Пусть у нас 5 провинций, расположенных кольцом (провинция 0 соседствует с 4 и 1).

Провинции: [0] [1] [2] [3] [4]

Входной слой (layer 0) — налог в каждой провинции. Значения от 0 до domainSize-1 (например, 0..1000).

Выходной слой (layer 1) — доход провинции. Доход провинции i вычисляется из налогов провинций i, i-1, i+1, i+2.

То есть:

Доход(провинция 2) = F( налог(2), налог(1), налог(3), налог(4) )

Функция F — не взвешенная сумма, а хеширующая функция со следующими свойствами:

  1. Детерминированная — одинаковый вход всегда даёт одинаковый выход.

  2. Чувствительная к изменениям — меняется любой налог → меняется доход.

  3. Принимает значения только в диапазоне [0..domainSize-1].

  4. Имеет параметр kWork — количество дополнительных раундов перемешивания (чем больше, тем «сложнее» зависимость).

Почему не обычная формула вроде налог 0.7 + налог_соседа 0.3?

Потому что PhiFlow — не калькулятор линейных уравнений. Она решает другую задачу:

  • Нужно детерминированное отображение с предсказуемым распределением выходов.

  • Коэффициенты не нужны, потому что значения всё равно дискретны и домен ограничен.

  • Хеширующая функция даёт хорошее рассеивание — даже маленькое изменение на входе может дать любое значение на выходе. Это полезно для симуляций и тестирования, когда не нужна точная экономическая модель, а нужна быстрая цепочка вычислений.

Как сделать свой пример с коэффициентами?

Реализовать кастомный IPhi:

public class EconomicPhi : IPhi
{
    private readonly float _selfWeight = 0.7f;
    private readonly float _neighborWeight = 0.3f;
    public int FromPrevLayer(ReadOnlySpan<int> prev, int i, int width, int domainSize, int kWork)
    {
        int self = prev[i];
        int left = prev[Mod(i - 1, width)];
        int right = prev[Mod(i + 1, width)];
        // Взвешенная сумма
        float weighted = _selfWeight * self + _neighborWeight * (left + right) / 2f;
        
        // Приведение к домену
        int result = (int)Math.Round(weighted);
        return Math.Clamp(result, 0, domainSize - 1);
    }
    public int MutateInputInDomain(int oldValue, int domainSize)
    {
        // Простая мутация: случайный сдвиг
        var newValue = oldValue + Random.Shared.Next(-10, 11);
        return Math.Clamp(newValue, 0, domainSize - 1);
    }
    private static int Mod(int a, int m)
    {
        int r = a % m;
        return r < 0 ? r + m : r;
    }
}

Использование:

var phi = new EconomicPhi();
var runtime = new PhiFlowRuntime(width: 5000, layers: 6, domainSize: 1000, phi);
runtime.SetInput(taxValues);  // налоги
runtime.BuildAll(kWork: 0);   // kWork не используется в кастомной логике
// Доходы в layer 1 теперь считаются по вашей формуле
var incomes = runtime.GetLayerSpan(1);

Что происходит с интервальным пропагатором?

Если вы изменили радиус зависимости (например, убрали зависимость от i+2), интервальный пропагатор IntervalPropagator всё равно будет предполагать расширение 2 влево и 1 вправо, потому что он зашит на топологию DefaultPhi. Это значит:

  • ApplyInputUpdates может пересчитывать больше узлов, чем реально нужно (субоптимально, но корректно).

  • Если вы хотите точного соответствия, нужно либо использовать полный пересчёт (RecomputeAll), либо переопределять пропагатор.

Пример с налогами в статье иллюстрирует класс задач, где PhiFlow применима:

  • Есть много узлов (провинций).

  • Есть много слоёв (доход → счастье → производительность → сила армии).

  • Приходит одно изменение (налог подняли).

  • Нужно быстро пересчитать всё и ответить на запросы (тотальная сила армии, топ-3 самых сильных провинций).

Конкретные коэффициенты и формула — на разработчике игры. PhiFlow даёт инструмент для быстрого инкрементального пересчёта и готовые индексы для запросов. Саму экономическую модель можно написать в кастомном IPhi.

Позовите пожалуйста оператора - я хочу пообщаться с живым человеком, а не с ллмкой.

Доход(провинция 2) = F( налог(2), налог(1), налог(3), налог(4) )

В каком мире доход провинции может зависеть от налогов ее соседей??? С одинаковыми коэффициентами между всеми элементами в кольце и на всех слоях? Это абсурд, который нейросеть видимо сначала нагаллюцинировала, а теперь продолжает на голубом глазу расписывать длинными предложениями с примерами. Соответственно, остальные примеры также могут быть натягиванием совы на глобус.

да, увидел в коде stencil [i, i-1, i+1, i+2] и притянул к нему первую попавшуюся игровую метафору, пример был неудачным

P.S. Спасибо что обратили внимание, статью отредактировал, добавил корректный пример - 6 слоёв: заражение в городе → риск для соседних городов → плотность заболевших → нагрузка на больницы → дефицит ресурсов → уровень паники.

А в чем преимущество перед родным дотнетовским TPL Dataflow?

TPL Dataflow — для асинхронных конвейеров и произвольных графов. PhiFlow — для слоистых stencil-пайплайнов, где нужно после небольшого изменения получить мгновенные агрегаты (CountGreater, TopK, Sum) по всем слоям. Ускорение достигается за счёт интервального конуса влияния (не пересчитываем весь слой, только affected interval) и точных индексов (Fenwick, гистограммы). TPL Dataflow не умеет ни того, ни другого из коробки. Если вам нужно и то, и другое — поставьте PhiFlow внутрь блока Dataflow.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации