Сообщение Re[11]: Эльбрус мёртв, да здравствует Эльбрус-Б! от 21.05.2025 19:26
Изменено 22.05.2025 5:41 netch80
Re[11]: Эльбрус мёртв, да здравствует Эльбрус-Б!
Здравствуйте, Sinclair, Вы писали:
V>>На обычных многопоточных, даже независимых по данным.
S>На "обычных многопоточных" данные и так не пересекаются, безо всяких усилий по разметке. Например, при перемножении матриц каждый из потоков работает со своим сегментом матрицы.
S>При сортировке слиянием каждый из потоков сортирует свой сегмент. И так далее.
S>Чтобы когерентность начала как-то сильно мешать, надо специально писать многопоточный код так, чтобы потоки наступали друг другу на пятки.
Ты не понял, разделение задач по памяти тут не поможет. Ключевые слова — uniform memory access (UMA) и синхронизация кэшей, где работает (в общем смысле) MESI протокол.
Пусть у тебя 1024 ядра (я выбрал цифру пострашнее
), каждое со своим кэшом. Ты очень аккуратно разделил память, пересечений нет. Каждое ядро начало читать свои данные. И тут на сцену выходит MESI. Обнаружив отсутствие строки с таким адресом в кэше, ядро посылает запрос "хочу читать", который отрабатывается согласно MESI:
— Если строки ни у кого нет, возвращается "ничей бычок, товарищ генерал! можете курить!" и ядро идёт читать напрямую из оперативки. (Заодно лочится у себя доступ к ней, пока нет ответа.)
— Если строка у кого-то в Exclusive (не изменённая), она переходит в Shared и посылается ответ — содержимое строки (все 64 байта) с "тоже поставь себе Shared".
— Если строка у кого-то в Modified, отдаётся содержимое строки с "ты себе тоже поставь Modified, а я уберу из своего кэша".
— Если строка в состоянии "я с ней что-то делаю", запрос тормозится до разрешения ситуации.
Возможны расширения (всякие MOESI, MEFSI и т.п.), на самом деле там авторы процессоров держат десятки состояний и сотни видов сообщений по этой шине с учётом разных тонкостей, так что я описал только самые базовые тупые случаи... но это всё неважно. Важно то, что на каждое такое чтение из памяти вопрос "а что с этой строкой?" с "вот что я собралось с ней делать" бежит через _все_ ядра. Повторяю, все 1023 ядра на каждый такой доступ к памяти должны обработать сообщение, ответить на него и передать дальше (обычно делается кольцевая шина). И вот это — узкое место.
Разумеется, его пытаются лечить. Например, inclusive cache решает, частично, эту проблему. Если есть сводный L3 кэш на камне... пусть эти 1024 ядра были 16 камней по 64 ядра. На запрос от других камней отвечает только L3 кэш, который всё знает, а внутренние уже бегут по своим ядрам. Это сокращает... не плотность потока, но время: количество инстанций, которые должен пройти каждый элемент. Но следующий уровень — non-uniform memory access (NUMA), при котором уже есть у каждой группы ядер своя память и есть чужая, и точно так же менеджером кэша для чужих областей выступает граничный мост. Но это уже требует понимания от ОС, какой нити давать какую память и на каких ядрах её размещать. Если нить будет использовать память не той группы ядер, где исполняется — будет медленнее и дороже в разы, чем на памяти своей группы. Где-то может быть и несколько уровней иерархии доменов NUMA (я не видел, но заранее поверю).
Или вот интересный комментарий про SystemZ. Если верить его автору (я не могу проверить), то там может быть вариант, что такого межъядерного обмена нет вообще! Любая команда синхронизации полностью сбрасывает кэши ядра. Влезть впараллель над одним куском памяти — результаты непредсказуемы, нет никаких гарантий, что в память попадут твои изменения. Но при этом пока ядро работает над своими данными — ему никто не мешает. Всей этой дорогой и прожорливой по трафику трахомудии просто нет, запросы идут напрямую в контроллер памяти. Я честно ХЗ, но даже если этого нет — идея красивая. Только вот x86, ARM и прочие не могут себе такого позволить, они уже заложились на гарантию синхронизации (когерентности) кэшей... а она, как описано выше, дорогая и не масштабируемая.
(Вот если бы в Эльбрусе повторили этот "подвиг" — был бы прикол
)
V>>Размечать блоки данных, которые локальные для некоего потока, т.е. чтобы была возможность разметить несколько таких блоков в лейауте объекта.
V>>Дальнейшая автоматизация в компиляторе, потом на реальном железе на уровне загрузчика ОС со стандартной коррекцией адресов в процессе загрузки.
S>Было бы интересно посмотреть на пример кода, в котором нужно делать рукопашный лейаут, с бенчмарком "тупого" случая по сравнению с "умным".
S>Ну, и идеи по поводу того, как могла бы выглядеть такая разметка.
Мне тоже сложно себе представить такое. Тут большая проблема... А вот сделать на уровне кода, что вся синхронизация действительно идёт через выделенные операции, грубо говоря, CAS — это уже реально делается много лет.
V>>На обычных многопоточных, даже независимых по данным.
S>На "обычных многопоточных" данные и так не пересекаются, безо всяких усилий по разметке. Например, при перемножении матриц каждый из потоков работает со своим сегментом матрицы.
S>При сортировке слиянием каждый из потоков сортирует свой сегмент. И так далее.
S>Чтобы когерентность начала как-то сильно мешать, надо специально писать многопоточный код так, чтобы потоки наступали друг другу на пятки.
Ты не понял, разделение задач по памяти тут не поможет. Ключевые слова — uniform memory access (UMA) и синхронизация кэшей, где работает (в общем смысле) MESI протокол.
Пусть у тебя 1024 ядра (я выбрал цифру пострашнее
— Если строки ни у кого нет, возвращается "ничей бычок, товарищ генерал! можете курить!" и ядро идёт читать напрямую из оперативки. (Заодно лочится у себя доступ к ней, пока нет ответа.)
— Если строка у кого-то в Exclusive (не изменённая), она переходит в Shared и посылается ответ — содержимое строки (все 64 байта) с "тоже поставь себе Shared".
— Если строка у кого-то в Modified, отдаётся содержимое строки с "ты себе тоже поставь Modified, а я уберу из своего кэша".
— Если строка в состоянии "я с ней что-то делаю", запрос тормозится до разрешения ситуации.
Возможны расширения (всякие MOESI, MEFSI и т.п.), на самом деле там авторы процессоров держат десятки состояний и сотни видов сообщений по этой шине с учётом разных тонкостей, так что я описал только самые базовые тупые случаи... но это всё неважно. Важно то, что на каждое такое чтение из памяти вопрос "а что с этой строкой?" с "вот что я собралось с ней делать" бежит через _все_ ядра. Повторяю, все 1023 ядра на каждый такой доступ к памяти должны обработать сообщение, ответить на него и передать дальше (обычно делается кольцевая шина). И вот это — узкое место.
Разумеется, его пытаются лечить. Например, inclusive cache решает, частично, эту проблему. Если есть сводный L3 кэш на камне... пусть эти 1024 ядра были 16 камней по 64 ядра. На запрос от других камней отвечает только L3 кэш, который всё знает, а внутренние уже бегут по своим ядрам. Это сокращает... не плотность потока, но время: количество инстанций, которые должен пройти каждый элемент. Но следующий уровень — non-uniform memory access (NUMA), при котором уже есть у каждой группы ядер своя память и есть чужая, и точно так же менеджером кэша для чужих областей выступает граничный мост. Но это уже требует понимания от ОС, какой нити давать какую память и на каких ядрах её размещать. Если нить будет использовать память не той группы ядер, где исполняется — будет медленнее и дороже в разы, чем на памяти своей группы. Где-то может быть и несколько уровней иерархии доменов NUMA (я не видел, но заранее поверю).
Или вот интересный комментарий про SystemZ. Если верить его автору (я не могу проверить), то там может быть вариант, что такого межъядерного обмена нет вообще! Любая команда синхронизации полностью сбрасывает кэши ядра. Влезть впараллель над одним куском памяти — результаты непредсказуемы, нет никаких гарантий, что в память попадут твои изменения. Но при этом пока ядро работает над своими данными — ему никто не мешает. Всей этой дорогой и прожорливой по трафику трахомудии просто нет, запросы идут напрямую в контроллер памяти. Я честно ХЗ, но даже если этого нет — идея красивая. Только вот x86, ARM и прочие не могут себе такого позволить, они уже заложились на гарантию синхронизации (когерентности) кэшей... а она, как описано выше, дорогая и не масштабируемая.
(Вот если бы в Эльбрусе повторили этот "подвиг" — был бы прикол
V>>Размечать блоки данных, которые локальные для некоего потока, т.е. чтобы была возможность разметить несколько таких блоков в лейауте объекта.
V>>Дальнейшая автоматизация в компиляторе, потом на реальном железе на уровне загрузчика ОС со стандартной коррекцией адресов в процессе загрузки.
S>Было бы интересно посмотреть на пример кода, в котором нужно делать рукопашный лейаут, с бенчмарком "тупого" случая по сравнению с "умным".
S>Ну, и идеи по поводу того, как могла бы выглядеть такая разметка.
Мне тоже сложно себе представить такое. Тут большая проблема... А вот сделать на уровне кода, что вся синхронизация действительно идёт через выделенные операции, грубо говоря, CAS — это уже реально делается много лет.
Re[11]: Эльбрус мёртв, да здравствует Эльбрус-Б!
Здравствуйте, Sinclair, Вы писали:
V>>На обычных многопоточных, даже независимых по данным.
S>На "обычных многопоточных" данные и так не пересекаются, безо всяких усилий по разметке. Например, при перемножении матриц каждый из потоков работает со своим сегментом матрицы.
S>При сортировке слиянием каждый из потоков сортирует свой сегмент. И так далее.
S>Чтобы когерентность начала как-то сильно мешать, надо специально писать многопоточный код так, чтобы потоки наступали друг другу на пятки.
Ты не понял, разделение задач по памяти тут не поможет. Ключевые слова — uniform memory access (UMA) и синхронизация кэшей, где работает (в общем смысле) MESI протокол.
Пусть у тебя 1024 ядра (я выбрал цифру пострашнее
), каждое со своим кэшом. Ты очень аккуратно разделил память, пересечений нет. Каждое ядро начало читать свои данные. И тут на сцену выходит MESI. Обнаружив отсутствие строки с таким адресом в кэше, ядро посылает запрос "хочу читать" эту строку в слой когерентности, который отрабатывается согласно MESI и на который должны прореагировать все соседние ядра:
— Если строки ни у кого нет, возвращается "ничей бычок, товарищ генерал! можете курить!" и ядро идёт читать напрямую из оперативки. (Заодно лочится у себя доступ к ней, пока нет ответа.)
— Если строка у кого-то в Exclusive (не изменённая), она переходит в Shared и посылается ответ — содержимое строки (все 64 байта) с "тоже поставь себе Shared".
— Если строка у кого-то в Modified, отдаётся содержимое строки с пометкой "ты себе тоже поставь Modified, а я уберу из своего кэша".
— Если строка в состоянии "я с ней что-то делаю", запрос тормозится до разрешения ситуации.
Это самая общая схема. Возможны расширения (всякие MOESI, MEFSI и т.п.), на самом деле там авторы процессоров держат десятки состояний и сотни видов сообщений по этой шине с учётом разных тонкостей, так что я описал только самые базовые тупые случаи... но это всё неважно. Важно то, что на каждое такое чтение из памяти вопрос "а что с этой строкой?" с "вот что я собралось с ней делать" бежит через _все_ ядра. Повторяю, все 1023 ядра на каждый такой доступ к памяти должны обработать сообщение, ответить на него и передать дальше (обычно делается кольцевая шина). И вот это — узкое место.
Разумеется, его пытаются лечить сокращением нагрузки. Например, inclusive cache решает, частично, эту проблему. Если есть сводный L3 кэш на камне... пусть эти 1024 ядра были 16 камней по 64 ядра. На запрос от других камней отвечает только L3 кэш, который всё знает, а внутренние запросы уже бегут по своим ядрам. Это сокращает... в основном не плотность потока, но время: количество инстанций, которые должен пройти каждый элемент. (Иногда везёт и соседнее ядро отвечает, так что запрос не выходит за пределы камня; но мы же начали с варианта, когда у каждого своя часть оперативной памяти?) Но следующий уровень — non-uniform memory access (NUMA), при котором уже есть у каждой группы ядер своя память и есть чужая, и точно так же менеджером кэша для чужих областей выступает граничный мост между доменами. Но это уже требует понимания от ОС, какой нити давать какую память и на каких ядрах её размещать. Если нить будет использовать память не той группы ядер, где исполняется — будет медленнее и дороже в разы, чем на памяти своей группы. Где-то может быть и несколько уровней иерархии доменов NUMA (я не видел, но заранее поверю).
Или вот интересный комментарий про SystemZ. Если верить его автору (я не могу проверить), то там может быть вариант, что такого межъядерного обмена — протокола когерентности — нет вообще! Любая команда синхронизации полностью сбрасывает кэши ядра. Влезть впараллель над одним куском памяти — результаты непредсказуемы, нет никаких гарантий, что в память попадут твои изменения. Но при этом пока ядро работает над своими данными — ему никто не мешает. Всей этой дорогой и прожорливой по трафику трахомудии просто нет, запросы идут напрямую в контроллер памяти. Я честно ХЗ, но даже если этого нет — идея красивая. Только вот x86, ARM и прочие не могут себе такого позволить, они уже заложились на гарантию синхронизации (когерентности) кэшей... а она, как описано выше, дорогая и не масштабируемая.
(Вот если бы в Эльбрусе повторили этот "подвиг" — был бы прикол
)
V>>Размечать блоки данных, которые локальные для некоего потока, т.е. чтобы была возможность разметить несколько таких блоков в лейауте объекта.
V>>Дальнейшая автоматизация в компиляторе, потом на реальном железе на уровне загрузчика ОС со стандартной коррекцией адресов в процессе загрузки.
S>Было бы интересно посмотреть на пример кода, в котором нужно делать рукопашный лейаут, с бенчмарком "тупого" случая по сравнению с "умным".
S>Ну, и идеи по поводу того, как могла бы выглядеть такая разметка.
Мне тоже сложно себе представить такое. Тут большая проблема... А вот сделать на уровне кода, что вся синхронизация действительно идёт через выделенные операции, грубо говоря, CAS — это уже реально делается много лет.
V>>На обычных многопоточных, даже независимых по данным.
S>На "обычных многопоточных" данные и так не пересекаются, безо всяких усилий по разметке. Например, при перемножении матриц каждый из потоков работает со своим сегментом матрицы.
S>При сортировке слиянием каждый из потоков сортирует свой сегмент. И так далее.
S>Чтобы когерентность начала как-то сильно мешать, надо специально писать многопоточный код так, чтобы потоки наступали друг другу на пятки.
Ты не понял, разделение задач по памяти тут не поможет. Ключевые слова — uniform memory access (UMA) и синхронизация кэшей, где работает (в общем смысле) MESI протокол.
Пусть у тебя 1024 ядра (я выбрал цифру пострашнее
— Если строки ни у кого нет, возвращается "ничей бычок, товарищ генерал! можете курить!" и ядро идёт читать напрямую из оперативки. (Заодно лочится у себя доступ к ней, пока нет ответа.)
— Если строка у кого-то в Exclusive (не изменённая), она переходит в Shared и посылается ответ — содержимое строки (все 64 байта) с "тоже поставь себе Shared".
— Если строка у кого-то в Modified, отдаётся содержимое строки с пометкой "ты себе тоже поставь Modified, а я уберу из своего кэша".
— Если строка в состоянии "я с ней что-то делаю", запрос тормозится до разрешения ситуации.
Это самая общая схема. Возможны расширения (всякие MOESI, MEFSI и т.п.), на самом деле там авторы процессоров держат десятки состояний и сотни видов сообщений по этой шине с учётом разных тонкостей, так что я описал только самые базовые тупые случаи... но это всё неважно. Важно то, что на каждое такое чтение из памяти вопрос "а что с этой строкой?" с "вот что я собралось с ней делать" бежит через _все_ ядра. Повторяю, все 1023 ядра на каждый такой доступ к памяти должны обработать сообщение, ответить на него и передать дальше (обычно делается кольцевая шина). И вот это — узкое место.
Разумеется, его пытаются лечить сокращением нагрузки. Например, inclusive cache решает, частично, эту проблему. Если есть сводный L3 кэш на камне... пусть эти 1024 ядра были 16 камней по 64 ядра. На запрос от других камней отвечает только L3 кэш, который всё знает, а внутренние запросы уже бегут по своим ядрам. Это сокращает... в основном не плотность потока, но время: количество инстанций, которые должен пройти каждый элемент. (Иногда везёт и соседнее ядро отвечает, так что запрос не выходит за пределы камня; но мы же начали с варианта, когда у каждого своя часть оперативной памяти?) Но следующий уровень — non-uniform memory access (NUMA), при котором уже есть у каждой группы ядер своя память и есть чужая, и точно так же менеджером кэша для чужих областей выступает граничный мост между доменами. Но это уже требует понимания от ОС, какой нити давать какую память и на каких ядрах её размещать. Если нить будет использовать память не той группы ядер, где исполняется — будет медленнее и дороже в разы, чем на памяти своей группы. Где-то может быть и несколько уровней иерархии доменов NUMA (я не видел, но заранее поверю).
Или вот интересный комментарий про SystemZ. Если верить его автору (я не могу проверить), то там может быть вариант, что такого межъядерного обмена — протокола когерентности — нет вообще! Любая команда синхронизации полностью сбрасывает кэши ядра. Влезть впараллель над одним куском памяти — результаты непредсказуемы, нет никаких гарантий, что в память попадут твои изменения. Но при этом пока ядро работает над своими данными — ему никто не мешает. Всей этой дорогой и прожорливой по трафику трахомудии просто нет, запросы идут напрямую в контроллер памяти. Я честно ХЗ, но даже если этого нет — идея красивая. Только вот x86, ARM и прочие не могут себе такого позволить, они уже заложились на гарантию синхронизации (когерентности) кэшей... а она, как описано выше, дорогая и не масштабируемая.
(Вот если бы в Эльбрусе повторили этот "подвиг" — был бы прикол
V>>Размечать блоки данных, которые локальные для некоего потока, т.е. чтобы была возможность разметить несколько таких блоков в лейауте объекта.
V>>Дальнейшая автоматизация в компиляторе, потом на реальном железе на уровне загрузчика ОС со стандартной коррекцией адресов в процессе загрузки.
S>Было бы интересно посмотреть на пример кода, в котором нужно делать рукопашный лейаут, с бенчмарком "тупого" случая по сравнению с "умным".
S>Ну, и идеи по поводу того, как могла бы выглядеть такая разметка.
Мне тоже сложно себе представить такое. Тут большая проблема... А вот сделать на уровне кода, что вся синхронизация действительно идёт через выделенные операции, грубо говоря, CAS — это уже реально делается много лет.