Как распараллелить выполнение формулы настраиваемой функции, сохранив при этом доступ к Google Sheet без разрешения? - PullRequest
3 голосов
/ 18 июня 2020

У меня есть таблица Google с формулой настраиваемой функции, которая: принимает матрицу и два вектора из электронной таблицы, выполняет некоторые длительные вычисления матрицы-вектора (> 30 se c, то есть выше квоты), прежде чем выводить результат в виде связки строк. Он однопоточный, так как это то, что изначально является сценарием Google Apps Script (GAS), но я хочу распараллелить вычисления, используя обходной путь многопоточности, поэтому он значительно ускоряет его.

Требования (1-3 ):

  1. UX : он должен запускать вычисления автоматически и реактивно как формулу настраиваемой функции, что означает, что пользователю не нужно запускать его вручную нажав кнопку запуска или аналогичный. Как и моя однопоточная версия.

  2. Parallelizable : в идеале он должен порождать ~ 30 потоков / процессов, так что вместо того, чтобы занимать> 30 секунд, как сейчас делает (что делает его тайм-аут из-за ограничения квоты Google ), это должно занять ~ 1 секунду. (Я знаю, что GAS однопоточный, но есть обходные пути, о которых говорится ниже).

  3. Возможность совместного использования : в идеале я должен иметь возможность поделиться Листом с другими людьми , чтобы они могли «сделать копию» его, и сценарий по-прежнему будет выполнять для них вычисления:

    • 3,1 без разрешений : без необходимости вручную передавать отдельные разрешения пользователям (без разрешения). Например, всякий раз, когда кто-то «делает копию» и «запускает приложение как пользователь, имеющий доступ к веб-приложению» . Мое элементарное тестирование предполагает, что это возможно.
    • 3,2 Ненавязчивый : без необходимости предоставления пользователям электронной таблицы навязчивых разрешений типа «Дайте эту таблицу / скрипт / доступ приложения ко всей учетной записи Google Диска или Gmail? ". Пользователи, которым необходимо дать ненавязчивую авторизацию скрипту / веб-приложению, могут быть приемлемы, пока сохраняется требование 3.1.
    • 3.3 UX : без принуждения пользователей к просмотру HTML боковая панель в электронной таблице.

Я уже прочитал этот отличный связанный ответ от @TheMaster, который описывает некоторые потенциальные способы решения распараллеливания в скрипте Google Apps В основном. Обходной путь №3 google.script.run и обходной путь №4 UrlFetchApp.fetchAll (оба с использованием веб-приложения Google) выглядят наиболее многообещающими. Но некоторые детали мне неизвестны, например, могут ли они соответствовать требованиям 1 и 3 с его дополнительными требованиями.

Я могу придумать другой потенциальный наивный обходной путь, который заключался бы в разделении функции на несколько формулы пользовательских функций и распараллеливание (с помощью некоторого вида Map / Reduce) внутри самой электронной таблицы (сохранение промежуточных результатов обратно в электронную таблицу и использование формул пользовательских функций в качестве редукторов). Но в моем случае это нежелательно и, вероятно, невыполнимо.

Я очень уверен, что мою функцию можно распараллелить, используя какой-то процесс Map / Reduce. В настоящее время функция оптимизирована путем выполнения всех вычислений в памяти, не касаясь электронной таблицы между этапами, прежде чем окончательно вывести результат в электронную таблицу. Подробности этого довольно сложные и более 100 строк, поэтому я не хочу перегружать вас дополнительной (и потенциально запутанной) информацией, которая на самом деле не влияет на общую применимость этого случая. В контексте этого вопроса вы можете предположить, что моя функция является распараллеливаемой (и map-reduce'able), или рассмотреть любую функцию, которую вы уже знаете, которая будет. Интересно то, чего обычно можно достичь с помощью распараллеливания в скрипте Google Apps, сохраняя при этом высочайший уровень совместимости и UX. При необходимости я дополню этот вопрос более подробной информацией.

Обновление 2020-06-19:

Чтобы быть более ясным, я не исключаю полностью обходные пути Google Web App, поскольку у меня нет опыта с их практическими ограничениями, чтобы точно знать, могут ли они решить проблему в рамках требований. Я обновил дополнительные требования 3.1 и 3.2, чтобы отразить это. Я также добавил подпункт 3.3, чтобы прояснить намерения. Я также удалил req 4, так как он в значительной степени перекрывался с req 1.

Я также отредактировал вопрос и удалил связанные подвопросы, так что он больше сосредоточен на единственном основном вопросе HOWTO в заголовке. Требования в моем вопросе должны обеспечивать четкий объективный стандарт, для которого ответы будут считаться лучшими.

Я понимаю, что вопрос может быть поиском Святого Грааля обходных путей многопоточности Google Sheet, как @TheMaster указал в частном порядке . В идеале Google должен предоставить одну или несколько функций для поддержки многопоточности, сокращения карты или более широкого доступа без разрешения. Но до тех пор я действительно хотел бы знать, каков оптимальный обходной путь в рамках текущих ограничений, которые у нас есть. Я надеюсь, что этот вопрос актуален и для других, даже с учетом жестких требований.

Ответы [ 2 ]

2 голосов
/ 20 июня 2020

Если вы публикуете sh веб-приложение с «любым, даже анонимным», выполняете как «Я», тогда пользовательская функция может использовать UrlFetchApp.fetchAll Авторизация не требуется для публикации в этом веб-приложении. Это будет работать параллельно proof . Это решает все три требования.

Предостережение: если лист используют несколько человек, и пользовательская функция должна будет отправить сообщение в «то же» веб-приложение (которое вы опубликовали для выполнения как вы) для обработки Google ограничит одновременное выполнение лимит квоты: 30 .

Чтобы решить эту проблему, вы можете попросить людей, использующих ваш лист, опубликовать sh свои собственные веб-приложения. Им придется сделать это один раз вначале, и авторизация не требуется.

Если нет, вам потребуется разместить собственный сервер для нагрузки или что-то вроде может помочь

1 голос
/ 01 июля 2020

Я закончил тем, что использовал наивный обходной путь, который я упомянул в своем сообщении:

Я могу придумать другой потенциальный наивный обходной путь, который заключался бы в том, чтобы разделить функцию на несколько формул пользовательских функций и выполнить распараллеливание (с помощью некоторого вида Map / Reduce) внутри самой электронной таблицы (сохранение промежуточных результатов обратно в электронную таблицу и использование пользовательских формул функций в качестве редукторов). Но в моем случае это нежелательно и, вероятно, невыполнимо.

Я сначала проигнорировал это, потому что это связано с наличием дополнительной вкладки листа с вычислениями, что было не идеально. Но когда я задумался над этим после исследования альтернативных решений, он фактически решает все заявленные требования самым ненавязчивым образом. Поскольку для этого не требуется ничего лишнего от пользователей, к таблице открыт доступ. Он также остается `` внутри '' Google Таблиц, насколько это возможно (не требуется полу- или полностью внешнее веб-приложение), выполняя распараллеливание, полагаясь на собственное распараллеливание одновременно выполняющихся ячеек электронной таблицы, где результаты могут быть объединены в цепочку и видны пользователю. например, с использованием обычных формул (без дополнительных пунктов меню или кнопок запуска этого сценария).

Итак, я реализовал MapReduce в Google Таблицах, используя пользовательские функции, каждая из которых работает с частью интервала, который я хотел вычислить. Причина, по которой я смог это сделать в моем случае, заключалась в том, что входные данные для моих вычислений были разделены на интервалы, каждый из которых можно было вычислить отдельно , а затем присоединить позже. **

Каждая параллельная настраиваемая функция затем берет один интервал , вычисляет его и выводит результаты обратно на лист (я рекомендую выводить в виде строк, а не столбцов, поскольку столбцы ограничены максимумом 18 278 столбцов. См. Это отличное сообщение об ограничениях Google Spreadsheet .) Я столкнулся с ограничением only 40,000 new rows at a time, но смог выполнить некоторое сокращение на каждом интервале, так что они выводят в электронную таблицу только очень ограниченное количество строк. Это было распараллеливание; Часть Map в MapReduce. Затем у меня была отдельная настраиваемая функция, которая выполняла часть «Уменьшение», а именно: динамически нацеливать *** на область вывода электронной таблицы отдельно рассчитанных настраиваемых функций и принимать их результаты, когда они доступны, и объединять их вместе, дополнительно уменьшая их (до найти наиболее эффективные результаты), чтобы получить окончательный результат.

Интересная часть заключалась в том, что я думал, что достигну only 30 simultaneous execution предела квоты Google Таблиц . Но я смог распараллелить до 64 независимо и, казалось бы, одновременно выполняя пользовательские функции. Может случиться так, что Google помещает их в очередь, если они превышают 30 одновременных выполнений, и фактически обрабатывают только 30 из них в любой момент времени (прокомментируйте, если вы знаете). Но в любом случае выгода / ускорение распараллеливания были огромными и, казалось, почти бесконечно масштабируемыми. Но с некоторыми оговорками:

  1. Вы должны определить количество параллельных пользовательских функций заранее вручную. Таким образом, распараллеливание не может бесконечно автоматически масштабироваться в соответствии с требованиями ****. Это важно из-за противоречащего интуиции результата, что в некоторых случаях с меньшим распараллеливанием на самом деле выполняется быстрее . В моем случае набор результатов из очень небольшого интервала может быть чрезвычайно большим, в то время как если бы интервал был больше, то многие результаты были бы исключены в алгоритме в этой распараллеленной пользовательской функции (т.е. некоторое сокращение).

  2. В редких случаях (с огромными входными данными) функция Reducer будет выводить результат до того, как будут выполнены все параллельные функции (Map) (поскольку некоторые из них, по-видимому, принимают слишком долго). Таким образом, у вас, по-видимому, есть полный набор результатов, но через несколько секунд он обновится, когда последняя параллельная функция вернет свой результат. Это не идеально, поэтому, чтобы получить уведомление об этом, я реализовал функцию, которая сообщает мне, был ли результат верным. Я поместил его в ячейку над функцией «Уменьшить» (и покрасил текст в красный цвет). B6 - это количество интервалов (здесь 4), а другая ячейка ссылается на go на ячейку с пользовательской функцией для каждого интервала: =didAnyExecutedIntervalFail($B$6,S13,AB13,AK13,AT13)

    function didAnyExecutedIntervalFail(intervalsExecuted, ...intervalOutputs) {
      const errorValues = new Set(["#NULL!", "#DIV/0!", "#VALUE!", "#REF!", "#NAME?", "#NUM!", "#N/A","#ERROR!", "#"]);
      // We go through only the outputs for intervals which were included in the parallel execution.
      for(let i=0; i < intervalsExecuted; i++) {
        if (errorValues.has(intervalOutputs[i]))
          return "Result below is not valid (due to errors in one or more of the intervals), even though it looks like a proper result!";
      }
    }

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

Вывод всего этого ограничен ограничениями Google Sheet . А именно максимум 5M ячеек в электронной таблице. Таким образом, вам может потребоваться некоторое уменьшение размера результатов, вычисляемых в каждой параллельной пользовательской функции, прежде чем возвращать результат в электронную таблицу. Чтобы каждая из них была меньше 40 000 строк, иначе вы получите ужасную ошибку «Результаты слишком велики»). Кроме того, в зависимости от размера результата каждой параллельной настраиваемой функции, это также ограничит количество настраиваемых функций, которые вы можете использовать одновременно, поскольку они и их ячейки результатов занимают место в электронной таблице. Но если каждая из них займет всего, скажем, 50 ячеек (включая очень небольшой вывод), то вы все равно сможете распараллелить довольно много (5M / 50 = 100 000 параллельных функций) на одном листе. Но вам также нужно место для того, что вы хотите сделать с этими результатами. И ограничение 5M ячеек предназначено для всей электронной таблицы в целом, а не только для одной из ее вкладок , по-видимому.

** Для тех, кому интересно: я в основном хотел вычислить все комбинации последовательности битов (методом перебора), поэтому функция была 2^n, где n - количество битов. Первоначальный диапазон комбинаций был от 1 to 2^n, поэтому его можно было разделить на интервалы комбинаций, например, при разделении на два интервала это будет один из 1 to X, а затем один из X+1 to 2^n.

*** Для интересующихся: я использовал формулу отдельного листа для динамического определения диапазона вывода одного из интервалов на основе наличия строк с содержимым. Он находился в отдельной ячейке для каждого интервала. Для первого интервала он находился в ячейке S11, и формула выглядела следующим образом: =ADDRESS(ROW(S13),COLUMN(S13),4)&":"&ADDRESS(COUNTA(S13:S)+ROWS(S1:S12),COLUMN(Z13),4), и он выводил S13:Z15, который является динамически вычисляемым диапазоном вывода, который учитывает только те строки с содержимым (с использованием COUNTA(S13:S)), таким образом избегая статического определения диапазона. Поскольку при нормальном диапазоне stati c размер вывода должен быть известен заранее, а это не так, иначе он, возможно, не будет включать весь вывод или много пустых строк (а вы не не хочу, чтобы Reducer перебирал множество по существу пустых структур данных). Затем я бы ввел этот диапазон в функцию Reduce, используя INDIRECT(S$11). Вот как вы получаете результаты из одного из интервалов, обработанных параллельной пользовательской функцией, в основную функцию Reducer.

**** Хотя вы могли бы сделать его автоматическим масштабированием до некоторого заранее определенного количество параллельных пользовательских функций. Вы можете использовать некоторые предварительно сконфигурированные пороги и в некоторых случаях разделить, скажем, на 16 интервалов, но в других случаях автоматически разделить на 64 интервала (предварительно сконфигурированных, исходя из опыта). Затем вы просто останавливаете / закорачиваете пользовательские функции, которые не должны участвовать, в зависимости от того, превышает ли количество этой параллельной пользовательской функции количество интервалов, на которые вы хотите разделить и обработать. В первой строке распараллеленной пользовательской функции: if (calcIntervalNr > intervals) return;. Хотя вам придется заранее настроить все параллельные пользовательские функции, что может быть утомительным (помните, что вы должны учитывать область вывода каждой, и ограничены максимальным пределом ячеек в 5 миллионов ячеек в Google Таблицы).

...