У меня есть сценарий bash, который применяет различные преобразования/сопоставления к столбцам файла TSV. Я пытаюсь распараллелить преобразования, используя параллель GNU, однако мой код зависает.
Для простоты рассмотрим cat
, преобразователь идентификаторов (т. е. ввод -> вывод) и файл TSV из трех столбцов (генерируемый на лету с использованием paste
и seq
).
n=1000000
map=cat # identity: inp -> out
rm -f tmp.col{1,2}.fifo
mkfifo tmp.col{1,2}.fifo
paste <(seq $n) <(seq $n) <(seq $n) \
| tee >(cut -f1 | $map > tmp.col1.fifo) \
| tee >(cut -f2 | $map > tmp.col2.fifo) \
| cut -f3- \
| paste tmp.col{1,2}.fifo - \
| python -m tqdm > /dev/null
Приведенный выше код работает нормально.
ПРИМЕЧАНИЕ.
python -m tqdm > /dev/null
печатает скорость.
Далее мы можем распараллелить задачи сопоставления, используя аргументы --pipe --keep-order
GNU Parallel. Вот минимальный параллельный пример, который работает:
seq 100 | parallel --pipe -k -j4 -N10 'cat && sleep 1'
Теперь, сложив все это вместе, вот мой код, который параллельно отображает столбцы TSV:
n=1000000
map=cat # identity map: inp -> out
rm -f tmp.col{1,2}.fifo
mkfifo tmp.col{1,2}.fifo
paste <(seq $n) <(seq $n) <(seq $n) \
| tee >(cut -f1 | parallel --id jobA --pipe -k -j4 -N1000 "$map" > tmp.col1.fifo) \
| tee >(cut -f2 | parallel --id jobB --pipe -k -j4 -N1000 "$map" > tmp.col2.fifo) \
| cut -f3- \
| paste tmp.col{1,2}.fifo - \
| python -m tqdm > /dev/null
Этот код должен был работать, однако этот код зависает. Почему он замерзает и как его разморозить?
Среда: Linux 5.15.0-116-generic, Ubuntu 22.04.4 LTS на x86_64.
Спасибо за вопрос. У меня есть терабайт сжатых данных в формате TSV, которые я использую для обучения моделей машинного обучения. Я хочу выполнить очистку столбцов TSV без создания промежуточных временных файлов. Здесь я выбрал cat
для демонстрации, но мои задачи картографии выполняются немного медленно, и я хотел бы их распараллелить (напоминание: никаких временных файлов, так как у нас мало места, и я также предпочитаю избегать дискового ввода-вывода. У меня много ядер ЦП и оперативной памяти, поэтому они не являются узким местом.
Я думаю, что относительно простой скрипт Python с использованием multiprocessing
был бы намного проще, но смысл зависит от объема данных (в каждом поле и в целом) и типа обработки.
Согласен, и я уже пробовал многопроцессорность Python. Это сработало, но прирост производительности за счет распараллеливания с Python был близок к производительности FIFO/PIPE вообще без параллелизма. Многопроцессорность Python, похоже, тратит много времени на взаимодействие между процессами. фоновые процессы не могут читать/записывать в STDIN/STDOUT основного процесса (ожидается), мне нужно правильно настроить размеры буфера. Это мой план Б, если я не могу заставить gnu работать параллельно.
FIFO имеет ограничения по размеру, можете ли вы реорганизовать скрипт таким образом?
#!/bin/bash
n=${1-10}
map=cat
paste <(paste <(seq $n) <(seq $n) <(seq $n) | cut -f1 | parallel --id jobA --pipe -k -j4 -N1000 "$map")\
<(paste <(seq $n) <(seq $n) <(seq $n) | cut -f2 | parallel --id jobA --pipe -k -j4 -N1000 "$map")\
<(paste <(seq $n) <(seq $n) <(seq $n) | cut -f3 ) \
| python -m tqdm > /dev/null
Бежать с
bash test.sh 100
Это состояние гонки с Fifos, а не с GNU Parallel.
Предположим это:
| tee >(cut -f1 | $map1 > tmp.col1.fifo) \
| tee >(cut -f2 | $map2 > tmp.col2.fifo) \
| cut -f3- \
| paste tmp.col{1,2}.fifo - \
Предположим, что $map1
печатает очень мало, а $map2
печатает много.
paste
пытается прочитать строку из tmp.col1.fifo
, но читать нечего, поэтому он блокируется. $map2
печатает много в tmp.col2.fifo
и заполняет FIFO, поэтому тоже блокируется.
Вам просто повезло, что состояние гонки не поразило вас раньше.
Вы, конечно, можете использовать временные файлы для решения этой проблемы, но у меня такое ощущение, что вы пытаетесь этого избежать.
Возможно, вы сможете «увеличить» размер FIFO с помощью такого инструмента, как mbuffer
:
| tee >(cut -f1 | parallel --pipe -k -j4 -N1000 "$map" | mbuffer -q -m6M -b5 > tmp.col1.fifo) \
| tee >(cut -f2 | parallel --pipe -k -j4 -N1000 "$map" | mbuffer -q -m6M -b5 > tmp.col2.fifo) \
| cut -f3- | mbuffer -q -m6M -b5 \
| paste tmp.col{1,2}.fifo - \
| python -m tqdm > /dev/null
Но если вы не уверены, что природа ваших данных не изменится, тогда это хрупкое решение, которое просто отбрасывает банку еще дальше.
Как насчет этого вместо этого?
n=1000000
map=cat # identity map: inp -> out
rm -f tmp.col{1,2,3,4}.fifo
mkfifo tmp.col{1,2,3,4}.fifo
paste <(seq $n) <(seq $n) <(seq $n) | cut -f1 | parallel --pipe -k -j4 -N1000 "$map" > tmp.col1.fifo &
paste <(seq $n) <(seq $n) <(seq $n) | cut -f2 | parallel --pipe -k -j4 -N1000 "$map" > tmp.col2.fifo &
paste <(seq $n) <(seq $n) <(seq $n) | cut -f3 > tmp.col3.fifo &
paste <(seq $n) <(seq $n) <(seq $n) > tmp.col4.fifo &
paste tmp.col{1,2,3,4}.fifo | python -m tqdm > /dev/null
Вы запустите еще несколько paste
, но если процессор не является проблемой, то это не должно привести к гонкам.
(Также: --id
(он же --semaphore-name
) не используется с --pipe
, а только с --semaphore
. См. https://www.gnu.org/software/parallel/parallel_options_map.pdf)
(Также: если вам не нужно ровно 1000 записей (-N1000
), то --block
будет быстрее).
У ОП может быть ограничение сделать paste <(seq $n) <(seq $n) <(seq $n)
только один раз.
Из комментариев: I have a terabyte of compressed data in TSV format
так что можно просто распаковать файл несколько раз. Возможно, это пустая трата циклов ЦП, но если циклы ЦП не являются ограничивающим фактором, это может быть достойным решением.
Я не подозревал, что tee
— это те, у кого есть взаимоблокировки. Я добавил --id
к параллельному мышлению, параллели - это те, у которых есть состояние гонки (поэтому я назначил им идентификаторы различных заданий, надеясь, что это исправит). И @Philippe прав в том, что я предпочитаю читать данные один раз (если это возможно), поскольку мои наборы данных хранятся в сетевой файловой системе, что добавляет дополнительные накладные расходы при многократном чтении. Тем не менее, спасибо за содержательный ответ, @OleTange.
@ThammeGowda Можете ли вы увидеть мой удаленный ответ? Если первое paste
можно выполнить несколько раз, думаю, ФИФО вам не нужны.
Я вижу ответ. Это еще один обходной путь, но не очень красивый. С точки зрения дизайна я хотел отделить код устройства чтения от задач устройства отображения, чтобы мы могли изменить код устройства отображения (или, при необходимости, отключить его), не затрагивая код устройства чтения. paste <(seq $n) <(seq $n) <(seq $n)
— это просто заполнитель для сложной программы чтения (которая циклически обрабатывает кучу файлов, выполняет некоторые базовые проверки работоспособности, распаковывает и выполняет потоковую передачу на STDOUT).
Я только что узнал, что @OleTange — автор параллельной программы GNU! Спасибо за такой замечательный инструмент.
Какую фактическую обработку вы выполняете, когда это причудливое расположение имеет смысл?