MapReduce в The Farmer Was Replaced

Обсуждение кода. Часть I.

27 лет назад появился Google. Одной из ключевых идей был PageRank - оценка «доверия» к веб-странице (статья). Чтобы считать это быстро, нужна большая вычислительная мощность. Можно было строить специализированный суперкомпьютер, но Google пошел другим путем: массивы обычного железа, объединенные в сеть. Программы для такой платформы нужно очень аккуратно оркестрировать, и в те времена распределенные вычисления были почти магией.

В 2004 году началась новая эра распределенных вычислений - эпоха MapReduce (см. статью Дина и Гемавата). Идея проста: делим данные на части, запускаем одну и ту же map-функцию параллельно на множестве машин, затем объединяем (reduce) частичные результаты в итог.

Этот пост посвящен адаптации этого подхода к задаче по сбору подсолнухов в игре The Farmer Was Replaced.

Внутриигровая задача состоит из трех этапов: подготовить почву, посадить подсолнухи, собрать урожай. Мы хотим использовать бонус 5×, поэтому собираем подсолнухи в порядке значения measure(): от большего к меньшему. Речь о поздней стадии игры, когда есть поле 32×32 и 32 дрона, то есть можно назначить по одному дрону на каждый столбец или строку.

Мне нужна была крошечная «структура», позволяющая назначать задачи дронам и запускать их параллельно. Поскольку мы проводим измерения, нам также необходимо собирать данные с разных дронов и объединять их. Ядром этого фреймворка является функция drone_map_reduce из скрипта.

Функция drone_map_reduce принимает три параметра:

objects — элементы для обработки
f_map - функция, обрабатывающая пакет объектов
f_red - функция-редьюсер, объединяющая результаты пакетов в общий результат

GitHub reference

API дронов, на который мы полагаемся:

spawn_drone(f) — запускает дрон с функцией f и возвращает его идентификатор
⌖ дрон «исчезает», когда завершается f
wait_for(drone_id) - ждет завершения работы дрона и возвращает результат

Сначала создаем слоты для дронов и распределяем между ними задания:

def drone_map_reduce(objects, f_map, f_red):
    drone_jobs = []

    for _ in range(max_drones()):
        drone_jobs.append([])

    ind = 0
    for q in objects:
        drone_jobs[ind % len(drone_jobs)].append(q)
        ind += 1

Теперь каждый элемент в drone_jobs — это пакет работ для одного дрона (или для основного потока).

Затем запускаем дронов и также обрабатываем некоторые пакеты в основном потоке:

    results = []
    drones = []

    while drone_jobs:
        while drone_jobs and num_drones() < max_drones():
            current = drone_jobs.pop()
            drones.append(spawn_drone(f_map(current)))
        if drone_jobs:
            current = drone_jobs.pop()
            results.append(f_map(current)())

После этого собираем все результаты с дронов:

    for current_drone in drones:
        results.append(wait_for(current_drone))

И, наконец, объединяем все в один результат, используя f_red:

    reduced = None

    for r in results:
        if reduced is None:
            reduced = r
        else:
            reduced = f_red(reduced, r)

    return reduced

Несколько важных деталей:

⌖ Чтобы создать корректную задачу для дрона, f_map ожидает список элементов того же типа, что и элементы в objects.
⌖ Мы создаем до max_drones() пакетов, поэтому во внутреннем цикле просто спавним дронов, пока num_drones() не достигнет max_drones().
⌖ Код работает и когда задач меньше, чем дронов (например, при set_world_size(3)): часть пакетов будет пустой.
⌖ Если функция дрона ничего не возвращает (None) и результаты вам не нужны, можно передать фиктивный редьюсер, например no_reduce, который игнорирует аргументы. Или просто None в качестве редьюсера, если он никогда не будет вызываться.

В следующий раз: как построить настоящую игровую логику на базе этого инструмента.