Векторизация функции с использованием преимуществ параллелизма - PullRequest
0 голосов
/ 04 мая 2018

Для простой нейронной сети я хочу применить функцию ко всем значениям гонуса VecDense.

У Gonum есть метод Apply для плотных матриц, но не для векторов, поэтому я делаю это вручную:

func sigmoid(z float64) float64 {                                           
    return 1.0 / (1.0 + math.Exp(-z))
}

func vSigmoid(zs *mat.VecDense) {
    for i := 0; i < zs.Len(); i++ {
        zs.SetVec(i, sigmoid(zs.AtVec(i)))
    }
}

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

var wg sync.WaitGroup

func sigmoid(z float64) float64 {                                           
    wg.Done()
    return 1.0 / (1.0 + math.Exp(-z))
}

func vSigmoid(zs *mat.VecDense) {
    for i := 0; i < zs.Len(); i++ {
        wg.Add(1)
        go zs.SetVec(i, sigmoid(zs.AtVec(i)))
    }
    wg.Wait()
}

Это не сработает, возможно, не неожиданно, поскольку Sigmoid() не заканчивается на wg.Done(), так как после него идет оператор return (который выполняет всю работу).

У меня такой вопрос: как я могу использовать параллелизм для применения функции к каждому элементу вектора гонума?

1 Ответ

0 голосов
/ 04 мая 2018

Прежде всего обратите внимание, что эта попытка выполнить вычисления одновременно предполагает, что методы SetVec() и AtVec() безопасны для одновременного использования с различными индексами. Если это не так, то попытка решения по своей сути небезопасна и может привести к скачкам данных и неопределенному поведению.


wg.Done() должен быть вызван, чтобы сигнализировать, что рабочая программа завершила свою работу. Но только , когда рутин закончил свою работу.

В вашем случае это не только функция sigmoid(), которая выполняется в рабочей программе, а zs.SetVec(). Поэтому вы должны позвонить wg.Done(), когда zs.SetVec() вернется, но не раньше.

Одним из способов было бы добавить wg.Done() в конец метода SetVec() (это также может быть defer wg.Done() в его начале), но было бы невозможно ввести эту зависимость (SetVec() не должен знать о каких-либо группах ожидания и процедурах, это серьезно ограничило бы его удобство использования.

Самый простой и понятный способ в этом случае - запустить анонимную функцию (литерал функции) в качестве рабочей программы, в которой вы можете вызвать zs.SetVec(), и в которой вы можете вызвать wg.Defer(), как только упомянуто выше. функция вернулась.

Примерно так:

for i := 0; i < zs.Len(); i++ {
    wg.Add(1)
    go func() {
        zs.SetVec(i, sigmoid(zs.AtVec(i)))
        wg.Done()
    }()
}
wg.Wait()

Но только это не будет работать, так как литерал функции (закрытие) ссылается на переменную цикла, которая изменяется одновременно, поэтому литерал функции должен работать со своей собственной копией, например ::

for i := 0; i < zs.Len(); i++ {
    wg.Add(1)
    go func(i int) {
        zs.SetVec(i, sigmoid(zs.AtVec(i)))
        wg.Done()
    }(i)
}
wg.Wait()

Также обратите внимание, что у подпрограмм (хотя они могут быть легкими) есть накладные расходы. Если работа, которую они выполняют, «мала», накладные расходы могут перевесить выигрыш в производительности при использовании нескольких ядер / потоков, и в целом вы не сможете повысить производительность, выполняя такие небольшие задачи одновременно (черт, вы можете даже сделать хуже, чем без использования программ) , Измерить.

Также вы используете мини-программы для выполнения минимальной работы, вы можете улучшить производительность, не «выбрасывая» программы после того, как они выполнят свою «крошечную» работу, но вы можете их «использовать» повторно. См. Связанный вопрос: Это пул рабочих потоков идиоматического в Go?

...