Skip to main content

Taskflow update #3: task syncing

In this update I want to describe some base concepts of synchronizing tasks.

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

In some cases we would want to hold tasks from moving forward before certain conditions are met, maybe even merge those tasks, or at least merge their attributes.

В ряде случаев мы хотели бы ожидать определённого условия прежде, чем пропустить таски к следующей ноде, возможно даже объединив таски, или объединив их атрибуты.

For example if we want to make a video file out of separate rendered frames: each rendered frame corresponds to one render task, so we need to be able to wait for all of renders for the same sequence in order to proceed to making a video out of them.

Например, если мы хотим сделать видео превью из отрендеренных кадров: каждый отрендеренный кадр соответствует одному рендер таску, значит мы должны уметь ждать все кадры одной секвенции прежде, чем дать ход таску по созданию видео из этих кадров.

Unlike in TOPs, here we cannot wait for ALL tasks - we never know how many of them can be since everything is dynamic.

В отличии от ТОПов, здесь мы не можем ждать ВСЕ таски - таски создаются динамически, а значит нода не может понять, когда больше тасков в неё не придет без сложного анализа всего дерева нод, чего нода делать и не должна.

1. Иерархия / Hierarchy

This concept should be familiar and pretty intuitive - tasks make a hierarchy: if a task creates new tasks - those new tasks become it's children, and original task becomes a parent to them.

Думаю не стоит углубляться в объяснение этой концепции. Таски создают иерархию: если таск создаёт новые таски - эти новые таски становятся его дочерними тасками, а он сам для них становится родительским таском.

So first synchronization method is for a parent to wait for all it's children. A special node has two inputs: first one for parents, second one for children to sync. Tasks will not proceed further until a parent from first input has all it's children arrived from the second input. As soon as it happens - parent will be sent out from the first output, children will be sent out from the second output. Optionally, attributes from children might be inherited by parents according to rules set in the node's interface.

Так вот один из методов синхронизации -  ждать всех дочерних тасков одного родителя. Специальная нода имеет два входа: первый - для родителей, второй - для их дочерних тасков, которые надо синхронизовать. Таски будут ожидать в этой ноде, пока для родителя из первого входа на второй вход не поступят все его доченрие таски. Как только это произойдет - эти таски могут быть пропущены дальше, опять же по разным выходам. По желанию атрибуты с дочерних тасков могут быть унаследованы родительским таском, это задаётся в интерфейсе ноды

Now coming back to that render sequence example: one task for ifd/ass creation spawns 10 mantra/kick tasks. After that that ifd/ass task will go to parent_children_waiter node and wait for all it's 10 children to arrive. After that it will take output filename attribute from all children, combine it into a list and proceed to ffmpeg node which will create a movie from that image path list.

Возвращаясь к примеру создания видео из кадров: если некий исходный таск по созданию ifd/ass создал по 10 mantra/kick тасков. После этого ifd/ass таск перейдёт в ноду parent_childran_waiter и будет там одижать своих 10 детей. Он заберёт у дочерних тасков атрибут с путём к отрендерённой картинке и объединит их в массив и проследует в ffmpeg ноду, которая использует этот массив картинок для создания видео. 

 

2. Разделения / Splits

Second type of task spawning is task splitting. A task does not create children, but, instead, creates a number of copies of itself and create a new "split" group for these. Not counting that split group thing - it's like spawning new children for parent's task.

Помимо создания иерархии один таск может разделиться на несколько, порождая при этом новую группу разделения. по сути вместо создания дочерних тасков для себя, он создаёт новые дочерние таски для своего родителя, и, вдобавок, объединяется с ними в некоторую группу.

Such a "horizontal" split makes more sense if new tasks are more logical to be equal tasks to the original, not children. For example: splitting one task's frame range into smaller ones, or creating wedges of the task's attributes. creating children and having one of those copies being a parent does not feels logical.

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

Though instead of this splitting, the original task could create all new copies of itself as children, and itself stop being processed. But having a bunch of dead parent tasks did not sound too cozy. Plus this task splitting happens implicitly if single output is connected to more than one input, which would be weird to do with creating children tasks.

В принципе, можно было бы решить примеры выше и через создание новых дочерних тасков, но мне не нравилось идея плодить детей для родительских тасков, которые скорее всего не будут использоваться дальше. Плюс разденелие тасков создаётся неявно в случае, если один выход подключён к более чем одному входу.

So the second synchronization method is just waiting for all tasks from the same split group. A special slice waiter/gatherer node will wait for all tasks of a slice, letting forward only the main task, the original one that initiated the slice in the first place. other slice tasks are now dead and won't go anywhere. Same as with parent-children awaiter node, attributes from slice tasks might be inherited by the main task moving forward.

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

Пример совмещения / Combined example

For a more real life example let's do wedges. We start with one task that only has required framerange (100 frames) and hipfile to render in it's attributes.
В качестве более реалистичного примера будем делать веджи. Мы начинаем с одного единственного таска, с атрибутами фреймренджа (100 кадров), и именем хип файла.

1. Task gets splitted by a wedge node that wedges 2 parameters into 2 variants each, total of 4 tasks (1 original + 3 copies)
Таск разделяется ведж нодой на 4 таска (1 начальный таск и 3 его копии) с двумя вариациями двух параметров

2. Those 4 tasks go into node that splits frameranges, and it will split once again each of those 4 tasks, each having frame range attribute of 100 frames, into 20 tasks of 5 frames, making total of 80 tasks.
Эти 3 таска переходят в ноду, разделяющую фреймренджи, и эта нода вновь разделит каждый из 4х тасков, у каждого из которых, помним, фреймрендж 100 кадров, в 20 тасков, с фреймренджем по 5 кадров каждый. итого - 80 тасков

3. These 80 tasks go to a rop renderer node that caches some procedural geometry (noised pig, wedges affect the noise). During each task's invocation 5 new child tasks will be created, one for each cached frame.
Эти 80 тасков попадают в ноду, запускающую рендер кеша некоторой сложной процедурной геометрии, варианты которой нам нужны (свинья с нойзом). Во время выполнения этих нагрузок из каждого таска будет создано 5 дочерних тасков, по одному на кадр

4. Those frame tasks go out of the second output, to ifd creation node, that task creates a single child mantra task.
Эти таски пойдут во второй выход ноды, в ноду по созданию ifd, в ходе генерации ifd будет создан один единственный мантра таск для каждого ifd таска.


5. Then ifd task goes to the first input of parent-children waiter node, and mantra tasks after the mantra node go to the second input of that parent-children waiter node. Here each ifd task will wait for it's child mantra task, inherit picture file path from it and proceed further. the mantra child task will just stay there, as node's second output is not connected.
Теперь ifd таск уйдёт в первый вход ноды, ожидающей всех детей родителя, а мантра таски, после выполнения, пойдут во второй вход. Каждый ifd таск дождётся своего единственного дочернего мантра таска, заберёт с него путь к итоговой картинке, и продолжит своё путешествие. Тогда как мантра таск останется в ноде, так как второй выход ноды никуда не подключен

6. So that ifd task with output picture attribute will go to the second input of yet another parent-children waiter node, where tasks we got at stage 2, that cached geometry at stage 3, are already waiting for them
Теперь наш ifd таск с путём до картинки попадёт во второй вход другой ноды, ожидающей детей родителя, где её уже будет ожидать таск из второго пункта, после того, как он откешировал геометрию в третьем пункте.

7. Tose ifd tasks will stay in that node as, again, it's second output is not connected. While the parent tasks once it has waited for all of it's ifd children - will go further, after inheriting picture frame paths from ifd tasks, combining them into a list of 5 image paths.
Эти ifd таски так там и останутся, так как опять второй выход ноды никуда не подсоединён. А родительские таски, дождавшись всех ifd детей, пойдут дальше, унаследовав у детей пути к картинкам, объединив их в список из пяти путей каждый.

8. And further are 2 slice gatherer nodes. First one await all of those 20 tasks which the original framerange was split into for each wedge. From the 20 tasks only the first one will be allowed through, the one wedge task that was originally splitted into 20 by framerange. It will inherit picture frame path lists, combining them into one single list of all 100 image paths for that wedge.
Далее идут две ноды ожидания частей разделений подряд. Первая из этих нод будет ожидать 20 тасков, на которые был разделён каждый ведж. Из 20 тасков только первый из них будет пропущен далее, тот самый, что изначально и был разделён на 20. Он унаследует списки путей картинок со всех своих разделений, составляя из них один список из всех 100 картинок для этого веджа.

9. And we are back to 4 tasks, which are now waiting for each other in another slice gatherer node. Only one original task will be let through, and all frame sequences are gathered into it's attribute, as a sequence of sequences of images, and this task goes to ffmpeg node.
И вот мы вновь имеем всего 4 таска, которые собираются в очередной ноде ожидания частей разделений. Только наш изначальный таск будет пропушен далее, унаследовав с остальных все секвенции, объединив их в секвенцию из 4х секвенций по 100 кадров каждая. Этот таск перейдёт в ноду ффмпег

10. ffmpeg node will see that sequence of sequence attribute and understand that it needs to make a mosaic out of them. So that it will do and voila!
Нода ффмпег увидит секвенцию из секвенций и поймёт, что от неё ждут мозайки, что она и сделает, вот и всё.


 

And the final result:

И финальный результат: