Советы по производительности

В этом разделе кратко описаны некоторые общие рекомендации по достижению хорошей производительности при использовании Turing. Общие методы по обеспечению надлежащей производительности программ Julia приводятся в документации по Julia.

Используйте многомерные распределения

Как правило, рекомендуется использовать многомерные распределения, если это возможно.

Следующий пример:

@model function gmodel(x)
    m ~ Normal()
    for i = 1:length(x)
        x[i] ~ Normal(m, 0.2)
    end
end

можно выразить более эффективно с помощью простого преобразования:

using FillArrays

@model function gmodel(x)
    m ~ Normal()
    x ~ MvNormal(Fill(m, length(x)), 0.04 * I)
end

Выбирайте соответствующий бэкенд AD

В настоящее время Turing поддерживает два различных бэкенда автоматического дифференцирования (AD). Как правило, старайтесь использовать :forwarddiff для моделей с небольшим количеством параметров и :reversediff, :tracker или :zygote для моделей с большими векторами параметров или операциями линейной алгебры. Подробные сведения см. в статье об автоматическом дифференцировании.

Уделяйте особое внимание :tracker и :zygote

В случае с :tracker и :zygote необходимо пока избегать циклов. В основном это связано с обратным режимом работы AD в бэкендах Tracker и Zygote, которые неэффективны для таких ситуаций. ReverseDiff работает лучше, но векторизированные операции все равно будут выполняться эффективнее.

Избежать циклов можно с помощью filldist(dist, N) и arraydist(dists). filldist(dist, N) создает многомерное распределение, состоящее из N одинаковых и независимых копий одномерного распределения dist, если dist является одномерным, или создает матрично-вариативное распределение, состоящее из N одинаковых и независимых копий многомерного распределения dist, если dist является многомерным. filldist(dist, N, M) также можно использовать для создания матрично-вариативного распределения из одномерного распределения dist. arraydist(dists) аналогичен filldist, но в качестве входных данных принимает массив распределений dists. Написание пользовательского распределения с пользовательским сопряжением является еще одним вариантом отказа от циклов.

Обеспечьте возможность вывода типов в модели

Для эффективного вывода на основе градиента, например при использовании HMC, NUTS или ADVI, важно обеспечить возможность вывода типов в вашей модели.

Следующий пример с абстрактными типами

@model function tmodel(x, y)
    p,n = size(x)
    params = Vector{Real}(undef, n)
    for i = 1:n
        params[i] ~ truncated(Normal(), 0, Inf)
    end

    a = x * params
    y ~ MvNormal(a, I)
end

может быть преобразовано в следующее представление с конкретными типами:

@model function tmodel(x, y, ::Type{T} = Float64) where {T}
    p,n = size(x)
    params = Vector{T}(undef, n)
    for i = 1:n
        params[i] ~ truncated(Normal(), 0, Inf)
    end

    a = x * params
    y ~ MvNormal(a, I)
end

В качестве альтернативы в этом примере можно использовать filldist:

@model function tmodel(x, y)
    params ~ filldist(truncated(Normal(), 0, Inf), size(x, 2))
    a = x * params
    y ~ MvNormal(a, I)
end

Обратите внимание, что вы можете использовать @code_warntype для поиска типов в определении модели, которые компилятор не может вывести. В REPL Julia они помечены красным цветом.

Например, рассмотрим следующую простую программу:

@model function tmodel(x)
    p = Vector{Real}(undef, 1)
    p[1] ~ Normal()
    p = p .+ 1
    x ~ Normal(p[1])
end

Мы можем использовать

using Random

model = tmodel(1.0)

@code_warntype model.f(
    model,
    Turing.VarInfo(model),
    Turing.SamplingContext(
        Random.default_rng(), Turing.SampleFromPrior(), Turing.DefaultContext(),
    ),
    model.args...,
)

для проверки вывода типов в модели.

Повторно используйте вычисления в выборке Гиббса

Часто при выполнении выборки Гиббса можно сэкономить вычислительное время за счет кэширования вывода дорогих функций. Кэшированные значения можно повторно использовать в последующих субитерациях Гиббса, которые не изменяют входные значения для этой дорогой функции. Например, в следующей модели

@model function demo(x)
    a ~ Gamma()
    b ~ Normal()
    c = function1(a)
    d = function2(b)
    x .~ Normal(c, d)
end
alg = Gibbs(MH(:a), MH(:b))
sample(demo(zeros(10)), alg, 1000)

если в субитерации Гиббса обновлять только a, сохраняя b неизменным, то значение d не меняется. И если обновлять только b, значение c не меняется. Однако если function1 и function2 являются дорогостоящими и выполняются в каждой субитерации Гиббса, на вычисление значений, которые мы уже вычислили ранее, уйдет много времени. Такую проблему можно решить с помощью Memoization.jl. Мемоизация функции позволяет хранить и повторно использовать вывод функции для каждого входного значения, с которым она вызывается. Это связано с небольшими временными затратами, но для дорогих функций экономия будет намного больше.

Чтобы использовать Memoization.jl, просто определите мемоизированные версии function1 и function2 следующим образом:

using Memoization

@memoize memoized_function1(args...) = function1(args...)
@memoize memoized_function2(args...) = function2(args...)

Затем определите модель Turing, используя новые функции, следующим образом:

@model function demo(x)
    a ~ Gamma()
    b ~ Normal()
    c = memoized_function1(a)
    d = memoized_function2(b)
    x .~ Normal(c, d)
end