Создание артефактов и развертывание модели в кластере
В Части 1 мы узнали, как использовать terraform
для удобной настройки и управления нашей инфраструктурой. В этой части мы продолжим наше путешествие по развертыванию работающей модели Stable Diffusion в подготовленном кластере.
Примечание. Вы можете следовать этому руководству от начала до конца, даже если вы являетесь бесплатным пользователем (при условии, что у вас осталось немного кредитов уровня бесплатного пользования).
Все изображения, если не указано иное, принадлежат автору
Гитхаб: https://github.com/thushv89/tf-serving-gke
Давайте посмотрим, каким будет конечный результат.
Подготовка артефактов модели
Что такое стабильная диффузия?
Модель стабильной диффузии состоит из пяти основных компонентов:
- Tokenizer — токенизирует заданную строку в список токенов (числовых идентификаторов).
- Кодировщик текста — принимает токенизированный текст и создает встраивание текста.
- Диффузионная модель — принимает встраивание текста и скрытое изображение (первоначально шум) в качестве входных данных и постепенно уточняет скрытое изображение, чтобы кодировать все больше и больше полезной информации (визуально приятной).
- Декодер — принимает окончательное скрытое изображение и создает фактическое изображение.
- Кодировщик изображения (используется для функции рисования — в этом упражнении мы его проигнорируем)
Основная новаторская идея стабильной диффузии (диффузионных моделей) заключается в следующем.
Если вы будете добавлять немного шума к изображению постепенно в течение многих шагов, вы получите изображение, содержащее шум. Изменив порядок процесса на обратный, вы можете получить вход (шум) и цель (исходное изображение). Затем модель обучается предсказывать исходное изображение по шуму.
Все вышеперечисленные компоненты работают слаженно для достижения этой идеи.
Хранение модели стабильной диффузии
Код: https://github.com/thushv89/tf-serving-gke/blob/master/notebooks/savedmodel_stable_diffusion.ipynb
Чтобы построить Модель стабильной диффузии, мы будем использовать библиотеку keras_cv
, которая включает в себя набор популярных моделей глубокого обучения для классификации изображений, сегментации, генеративного ИИ и т. д. Вы можете найти учебник здесь, который объясняет, как использовать StableDiffusion
в keras_cv
. Вы можете открыть блокнот и поиграть с моделью, чтобы ознакомиться.
Наша цель — сохранить модель StableDiffusion
в формате SavedModel
; стандарт для сериализации моделей TensorFlow. Одно из важнейших требований для этого — убедиться, что все используемые операции совместимы с графом TensorFlow. К сожалению, это не случай.
- Текущая версия модели использует несовместимый с графом TensorFlow токенизатор, поэтому его необходимо вывести из упакованной модели и использовать на отдельном шаге.
- Текущая версия использует
predict_on_batch
для создания изображения, что не поддерживается построением графа TensorFlow.
Фиксация модели
Чтобы исправить модель режима ожидания StableDiffusion
, мы создадим новую модель с именем StableDiffusionNoTokenizer
. В этой новой модели мы заменим все вызовы predict_on_batch()
вызовами __call__()
, совместимыми с графиком. Мы также отделим процесс токенизации от модели, как следует из названия. Кроме того, в функции generate_image()
мы заменим,
timesteps = tf.range(1, 1000, 1000 // num_steps) alphas, alphas_prev = self._get_initial_alphas(timesteps) progbar = keras.utils.Progbar(len(timesteps)) iteration = 0 for index, timestep in list(enumerate(timesteps))[::-1]: latent_prev = latent # Set aside the previous latent vector t_emb = self._get_timestep_embedding(timestep, batch_size) unconditional_latent = self.diffusion_model.predict_on_batch( [latent, t_emb, unconditional_context] ) latent = self.diffusion_model.predict_on_batch( [latent, t_emb, context] ) latent = unconditional_latent + unconditional_guidance_scale * ( latent - unconditional_latent ) a_t, a_prev = alphas[index], alphas_prev[index] pred_x0 = (latent_prev - math.sqrt(1 - a_t) * latent) / math.sqrt( a_t ) latent = ( latent * math.sqrt(1.0 - a_prev) + math.sqrt(a_prev) * pred_x0 ) iteration += 1 progbar.update(iteration)
с,
latent = self.diffusion_reverse_loop( latent, context=context, unconditional_context=unconditional_context, batch_size=batch_size, unconditional_guidance_scale=unconditional_guidance_scale, num_steps=num_steps, )
где,
@tf.function def diffusion_reverse_loop(self, latent, context, unconditional_context, batch_size, unconditional_guidance_scale, num_steps): index = num_steps -1 cond = tf.math.greater(index, -1) timesteps = tf.range(1, 1000, 1000 // num_steps) alphas, alphas_prev = self._get_initial_alphas(timesteps) iter_partial_fn = functools.partial( self._diffusion_reverse_iter, timesteps=timesteps, alphas=alphas, alphas_prev=alphas_prev, context=context, unconditional_context=unconditional_context, batch_size=batch_size, unconditional_guidance_scale=unconditional_guidance_scale, num_steps=num_steps ) latent, index = tf.while_loop(cond=lambda _, i: tf.math.greater(i, -1), body=iter_partial_fn, loop_vars=[latent, index]) return latent @tf.function def _diffusion_reverse_iter(self, latent_prev, index, timesteps, alphas, alphas_prev, context, unconditional_context, batch_size, unconditional_guidance_scale, num_steps): t_emb = self._get_timestep_embedding(timesteps[index], batch_size) combined_latent = self.diffusion_model( [ tf.concat([latent_prev, latent_prev],axis=0), tf.concat([t_emb, t_emb], axis=0), tf.concat([context, unconditional_context], axis=0) ], training=False ) latent, unconditional_latent = tf.split(combined_latent, 2, axis=0) latent = unconditional_latent + unconditional_guidance_scale * ( latent - unconditional_latent ) a_t, a_prev = alphas[index], alphas_prev[index] pred_x0 = (latent_prev - tf.math.sqrt(1 - a_t) * latent) / tf.math.sqrt(a_t) latent = latent * tf.math.sqrt(1.0 - a_prev) + tf.math.sqrt(a_prev) * pred_x0 index -= 1 return latent, index
Два основных изменения, которые я сделал:
- Вместо цикла Python
for
я использовал циклtf.while_loop
, который более эффективен в TensorFlow. - Объединил два отдельных вызова
diffusion_model
в один вызов, а затем разделил выходные данные.
Есть и другие изменения, такие как замена различных операций на эквивалент TensorFlow (например, np.clip()
-> tf.clip_by_value()
), вы можете сравнивать и сопоставлять исходную модель с этой версией, чтобы сравнивать и сопоставлять.
При работе с TensorFlow в режиме выполнения графа вы можете использовать операторы
tf.print()
, чтобы обеспечить валидность кода во время выполнения. Пожалуйста, обратитесь к Приложению для получения дополнительной информации оtf.print()
.
После того, как базовая модель будет исправлена, мы можем создать следующую модель, которую можно без проблем выполнить в графическом режиме.
class StableDiffusionTFModel(tf.keras.models.Model): def __init__(self): super().__init__() self.image_width = self.image_height = 384 self.model = StableDiffusionNoTokenizer(img_width=self.image_width, img_height=self.image_height, encoded_text_length=None, jit_compile=True) # This forces the model download its components # self.image_encoder is only required for in-painting - we will ignore this functionality in this excercise self.text_encoder = self.model.text_encoder self.diffusion_model = self.model.diffusion_model self.decoder = self.model.decoder self.default_num_steps = tf.constant(40) self.default_batch_size = tf.constant(2) # These negative prompt tokens are borrowed from the original stable diffusion model self.default_negative_prompt_tokens = tf.constant( [ 49406, 8159, 267, 83, 3299, 267, 21101, 8893, 3500, 267, 21101, 8893, 4804, 267, 21101, 8893, 1710, 267, 620, 539, 6481, 267, 38626, 267, 12598, 943, 267, 4231, 34886, 267, 4231, 7072, 267, 4231, 5706, 267, 1518, 15630, 267, 561, 6528, 267, 3417, 268, 3272, 267, 1774, 620, 539, 6481, 267, 21977, 267, 2103, 794, 267, 2103, 15376, 267, 38013, 267, 4160, 267, 2505, 2110, 267, 782, 23257, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407, 49407 ], dtype=tf.int32 ) def call(self, inputs): encoded_text = self.text_encoder([inputs["tokens"], self.model._get_pos_ids()], training=False) images = self.model.generate_image( encoded_text, negative_prompt_tokens=inputs.get("negative_prompt_tokens", self.default_negative_prompt_tokens), num_steps=inputs.get("num_steps", self.default_num_steps), batch_size=inputs.get("batch_size", self.default_batch_size) ) return images model = StableDiffusionTFModel()
Эта модель принимает следующие входные данные:
input_tokens
: Токенизированное представление входной строкиnegative_prompt_tokens
: Токенизированное представление отрицательной подсказки (подробнее об отрицательной подсказке: здесь)num_steps
: Количество шагов для запуска процесса распространения дляbatch_size
: Количество изображений для создания каждого изображения
Вот пример использования этой модели:
# Tokenizing the prompts tokenizer = SimpleTokenizer() def generate_tokens(tokenizer, prompt, MAX_PROMPT_LENGTH): inputs = tokenizer.encode(prompt) if len(inputs) > MAX_PROMPT_LENGTH: raise ValueError( f"Prompt is too long (should be <= {MAX_PROMPT_LENGTH} tokens)" ) phrase = tf.concat([inputs, ([49407] * (MAX_PROMPT_LENGTH - len(inputs)))], axis=0) return phrase tokens = generate_tokens(tokenizer, "a ferrari car with wings", MAX_PROMPT_LENGTH) # Invoking the model all_images = [] num_steps = 30 tokens = generate_tokens(tokenizer, "a castle in Norway overlooking a glacier, landscape, surrounded by fairies fighting trolls, sunset, high quality", MAX_PROMPT_LENGTH) neg_tokens = generate_tokens(tokenizer, "ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, mutation, mutated, extra limbs, extra legs, extra arms, disfigured, deformed, cross-eye, body out of frame, blurry, bad art, bad anatomy, blurred, text, watermark, grainy", MAX_PROMPT_LENGTH) images = model({ "tokens": tokens, "negative_prompt_tokens": neg_tokens, "num_steps": tf.constant(num_steps), "batch_size": tf.constant(1) })
Помните, что я (т.е. уровень бесплатного пользователя) сильно ограничен квотами в этом проекте.
- Нет квоты GPU вообще
- Макс. 8 ЦП N2 (если вы выберете ЦП N1, вы можете увеличить до 12)
Поэтому я не могу использовать ни одного экземпляра GPU или более 2 экземпляров n2-standard-4
instances. Модели Stable Diffusion довольно медленные, поэтому у нас возникнут проблемы с задержкой при использовании экземпляров ЦП.
Вот некоторые подробности о том, сколько времени это занимает при различных параметрах. Тесты проводились на n2-standard-8
машине на Vertex AI workbench.
- Размер изображения (
num_steps = 40
)
— изображение 512x512: 474 с
— изображение 384x384: 233 с batch_size
иnum_steps
—batch size = 1
: 21,6 с (num_steps=5
), 67,7 с (num_steps=20
) и 99,5 с (num_steps=30
)
—batch size = 2
, 55,6 с (num_steps=5
), 121,1 с (num_steps=20
) и 180,2 с (num_steps=30
)
—batch size=4
, 21,6 с (num_steps=5
), 67,7 с (num_steps=20
) и 99,5 с (num_steps=30
)
Как видите, увеличение image_size
, batch_size
, num_steps
приводит к увеличению затрат времени. Поэтому, уравновешивая вычислительные затраты качеством изображения, мы выбрали следующие параметры для нашей развернутой модели.
image_size
:384x384
num_steps
:30
batch_size
:1
После создания модели загрузите ее в созданную корзину GCS.
!gsutil -m cp -r ./stable_diffusion_model gs://<project>-bucket/
Это будет источник данных, который мы будем использовать для развертывания нашей модели в качестве службы прогнозирования.
Прежде чем перейти к следующему разделу, давайте еще раз оценим некоторые изображения, созданные моделью.
Развертывание и обслуживание модели
Код: https://github.com/thushv89/tf-serving-gke/tree/master/infrastrcture
Чтобы развернуть нашу модель и настроить службу прогнозирования, нам нужны 3 конфигурации:
configmap.yaml
— определяет различные переменные, необходимые во время развертывания. Например, это будет включать расположение SavedModel в GCS (т. е. доступное через переменную средыMODEL_PATH
).deployment.yaml
— Развертывание определяет характеристики пода (например, ЦП) и контейнеры, которые он должен запускать. В этом случае мы будем запускать один контейнер сtensorflow-serving
, обслуживающий модель, расположенную по адресуMODEL_PATH
.service.yaml
— Сервис — это механизм, с помощью которого мы раскрываем нашеtensorflow-serving
приложение, работающее в наших модулях. Например, мы можем сказать, чтобы наши модули отображались через балансировщик нагрузки.
Развертывание
Давайте сначала посмотрим на спецификацию deployment
:
spec: replicas: 1 selector: matchLabels: app: stable-diffusion template: metadata: labels: app: stable-diffusion spec: containers: - name: tf-serving image: "tensorflow/serving:2.11.0" args: - "--model_name=$(MODEL_NAME)" - "--model_base_path=$(MODEL_PATH)" - "--rest_api_timeout_in_ms=720000" envFrom: - configMapRef: name: tfserving-configs imagePullPolicy: IfNotPresent readinessProbe: httpGet: path: "/v1/models/stable-diffusion" port: 8501 scheme: HTTP initialDelaySeconds: 30 periodSeconds: 15 failureThreshold: 10 ports: - name: http containerPort: 8501 protocol: TCP - name: grpc containerPort: 8500 protocol: TCP resources: requests: cpu: "3" memory: "12Gi"
Мы можем сделать несколько интересных наблюдений:
- Мы объявляем только одну реплику в скрипте, масштабирование будет настроено отдельно и будет контролироваться через политику автомасштабирования.
- Мы предоставляем
selector
, который сервис будет искать в развертывании, чтобы убедиться, что он работает в правильном развертывании. - Выставляем два порта; 8501 (HTTP-трафик) и 8500 (GRPC-трафик)
- Мы будем запрашивать 3 «процессорного времени» и 12Gi на контейнер.
Примечание 1. На узле обычно выполняются другие модули, необходимые для Kubernetes (например, DNS, мониторинг и т. д.). Поэтому такие факторы необходимо учитывать при выделении вычислительных ресурсов для модуля. Вы можете видеть, что хотя у нас есть 4 ЦП в узле, мы запрашиваем только 3 (вы также можете запрашивать дробные ресурсы ЦП — например, 3,5). Вы можете увидеть выделяемый ЦП/память каждого узла в GCP (Консоль GCP → Кластеры → Узлы → Щелкните узел) или с помощью
kubectl describe nodes
.
Если ваш узел не может использовать вычислительные ресурсы, указанные вами, Kubernetes не будет может запускать модули и выдавать ошибку (например, PodUnschedulable).
Примечание 2. Один из самых важных аргументов, с которым вам нужно быть осторожным, — это
--rest_api_timeout_in_ms=720000
. Обработка одного запроса занимает около 250 секунд, поэтому здесь мы устанавливаем время ожидания примерно в три раза больше, чтобы учесть любые запросы в очереди, когда мы отправляем параллельные запросы. Если вы установите слишком маленькое значение, время ожидания ваших запросов истечет до того, как они будут выполнены.
Определение услуги
Здесь мы определяем службу типа LoadBalancer
, где мы будем предоставлять приложение stable-diffusion
через балансировщик нагрузки GCP. При таком подходе вам будет предоставлен IP-адрес балансировщика нагрузки, по которому балансировщик нагрузки будет направлять трафик на поступающие к нему модули. Пользователи будут делать запросы к IP-адресу балансировщика нагрузки.
metadata: name: stable-diffusion namespace: default labels: app: stable-diffusion spec: type: LoadBalancer ports: - port: 8500 protocol: TCP name: tf-serving-grpc - port: 8501 protocol: TCP name: tf-serving-http selector: app: stable-diffusion
Автомасштабирование
Есть важная тема, которую мы откладывали; масштабирование нашего сервиса. В реальном мире вам может понадобиться обслуживать тысячи, миллионы или даже миллиарды клиентов. Для этого ваша служба должна иметь возможность увеличивать/уменьшать количество узлов/модулей в кластере в зависимости от спроса. К счастью, GCP предоставляет множество вариантов, от полностью управляемого автоматического масштабирования до полу/полностью управляемого пользователем автоматического масштабирования. Подробнее о них можно узнать в этом видео.
Здесь мы будем использовать горизонтальный автоскейлер pod (HPA). Горизонтальное автомасштабирование pod увеличивает количество pod в зависимости от заданного вами порога (например, использования ЦП или памяти). Вот пример.
kubectl autoscale deployment stable-diffusion --cpu-percent=60 --min=1 --max=2
Здесь мы даем HPA минимум 1, максимум 2 модуля и просим его добавить больше модулей, если средняя загрузка ЦП в текущем наборе модулей превысит 60%.
Применение изменений
Теперь у нас есть все строительные блоки, готовые к запуску нашего сервиса. Просто выполните следующие команды.
gcloud container clusters get-credentials sd-cluster --zone us-central1-c && \ kubectl apply -f tf-serving/configmap.yaml && \ kubectl apply -f tf-serving/deployment.yaml && \ kubectl autoscale deployment stable-diffusion --cpu-percent=60 --min=1 --max=2 && \ kubectl apply -f tf-serving/service.yaml
Прогнозирование по обслуживаемой модели
Чтобы предсказать, вам просто нужно сделать POST-запрос к правильному URL-адресу с полезной нагрузкой, содержащей входные данные для модели.
Последовательные предсказания
В качестве первого примера мы покажем, как можно сделать серию запросов один за другим.
def predict_rest(json_data, url): json_response = requests.post(url, data=json_data) response = json.loads(json_response.text) if "predictions" not in response: print(response) rest_outputs = np.array(response["predictions"]) return rest_outputs url = f"http://{stable_diffusion_service_ip}:8501/v1/models/stable-diffusion:predict" tokens_list = [ generate_tokens(tokenizer, "A wine glass made from lego bricks, rainbow colored liquid being poured into it, hyper realistic, high detail", MAX_PROMPT_LENGTH).numpy().tolist(), generate_tokens(tokenizer, "A staircase made from color pencils, hyper realistic, high detail", MAX_PROMPT_LENGTH).numpy().tolist(), generate_tokens(tokenizer, "A ferrari car in the space astronaut driving it, futuristic, hyper realistic, high detail", MAX_PROMPT_LENGTH).numpy().tolist(), generate_tokens(tokenizer, "a dragon covered with weapons fighting an army, fire, explosions, hyper realistic, high detail", MAX_PROMPT_LENGTH).numpy().tolist(), generate_tokens(tokenizer, "A sawing girl in a boat, hyper realistic, high detail", MAX_PROMPT_LENGTH).numpy().tolist(), ] negative_tokens = generate_tokens(tokenizer, "ugly, tiling, poorly drawn hands, poorly drawn feet, poorly drawn face, out of frame, mutation, mutated, extra limbs, extra legs, extra arms, disfigured, deformed, cross-eye, body out of frame, blurry, bad art, bad anatomy, blurred, text, watermark, grainy", MAX_PROMPT_LENGTH).numpy().tolist() all_images = [] all_data = [] for tokens, negative_tokens in zip(tokens_list, [negative_tokens for _ in range(5)]): all_data.append(generate_json_data(tokens, negative_tokens)) all_images = [predict_rest(data, url) for data in all_data]
Это заняло более 1600 секунд, когда я провел эксперимент. Как вы можете себе представить, эта установка довольно неэффективна и не может использовать возможности кластера по масштабированию.
Параллельные предсказания
Вы можете использовать многопроцессорную библиотеку Python для выполнения параллельных запросов, что больше похоже на запросы реальных пользователей.
def predict_rest(input_data, url): json_data, sleep_time = input_data["data"], input_data["sleep_time"] # We add a delay to simulate real world user requests time.sleep(sleep_time) print("Making a request") t1 = time.perf_counter() json_response = requests.post(url, data=json_data) response = json.loads(json_response.text) result = np.array([]) try: result = np.array(response["predictions"]) except KeyError: print(f"Couldn't complete the request {response}") finally: t2 = time.perf_counter() print(f"It took {t2-t1}s to complete a single request") return result t1 = time.perf_counter() with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: all_images_gen = executor.map( functools.partial(predict_rest, url=url), [{"data": data, "sleep_time": min(i*20, 60)} for i, data in enumerate(all_data)] ) all_images = [img for img in all_images_gen] t2 = time.perf_counter() print(f"It took {t2-t1}s to complete {n_requests} requests")
Это работало в 900-х годах. Таким образом, у нас есть ускорение примерно на 180% за счет увеличения кластера максимум до 2 модулей.
Примечание об установке времени ожидания
Будьте осторожны при настройке параллельных запросов. Если вы отправляете все параллельные запросы одновременно (поскольку их всего 6), вероятно, время их ожидания истечет. Это связано с тем, что для создания нового узла и инициализации нового модуля требуется время. Таким образом, если все запросы выполняются мгновенно, балансировщик нагрузки может даже не успеть увидеть второй узел и в конечном итоге попытаться обслужить все запросы к одному узлу.
Время ожидания, указанное выше, отсчитывается с момента получения запроса (т. е. входа в очередьtensorflow-serving
), а не с момента начала обслуживания запроса. Поэтому, если запрос слишком долго ожидает в очереди, это также считается тайм-аутом.
Вы можете отслеживать показатели вычислений, такие как использование ЦП и потребление памяти, в GCP (GCP → Kubernetes Engine → Службы и вход → Выберите службу).
Заключение
В этом уроке из 2 частей мы,
- Настройте инфраструктуру с помощью terraform (инструмент IaaS), которая в основном состоит из кластера и пула узлов (часть 1)
- Развернули модель и создали службу прогнозирования для обслуживания запросов пользователей с использованием модели стабильной диффузии (часть 2).
Мы настроили это руководство таким образом, чтобы его мог запустить даже пользователь бесплатного уровня. Мы настроили кластер с двумя узлами и создали по 1 поду на каждый узел. Затем мы сделали как последовательные, так и параллельные прогнозы и увидели, что параллельные прогнозы приводят к приросту пропускной способности примерно на 180%.
Следующие шаги:
- Прогрев модели —
tensorflow-serving
предлагает простой способ прогрева модели. Вы можете проанализировать примеры запросов, и они будут загружены и отправлены в модель до обслуживания реальных пользовательских запросов. Это уменьшит задержку для первоначальных пользовательских запросов. - Динамическая группировка запросов — вы можете выбрать динамическую группировку входящих запросов. Это позволит модели делать прогнозы по пакету входных данных, а не по каждому входу. При достаточном объеме памяти это, вероятно, обеспечит прирост пропускной способности, что позволит вам обслуживать множество запросов в разумные сроки.
Приложение
Отладка внутри модулей
Когда я пытался запустить это, я столкнулся с кропотливой проблемой, которая заключалась в том, чтобы столкнуться со следующей кирпичной стеной.
и когда я захожу в один из pod’ов в деплойменте, я получаю более осмысленную (пока незаметную) ошибку. Но по-прежнему было недостаточно указать, что именно было не так.
Поэтому мне пришлось найти способ микроскопически исследовать первопричину. Для этого я сначала вошел в контейнер рассматриваемого модуля,
kubectl exec --stdin --tty <container name> -- /bin/bash
Как только я войду, все, что мне нужно сделать, это извлечь выгоду из парадигмы все есть файл, на которой процветает Linux. Другими словами, вы можете просто нажать на файл, чтобы увидеть вывод/поток ошибок процесса. Например, в моем случае процесс tensorflow-serving
имел PID 7, поэтому /proc/7/fd/2
дает поток ошибок этого процесса.
tail -n 10 /proc/7/fd/2
Здесь я смог точно увидеть, почему это не было толчком. Это произошло потому, что у контейнера не было необходимого разрешения на доступ к корзине GCS, указанной в MODEL_PATH
.
Использование tf.print
для отладки
Как вы знаете, TensorFlow предлагает два стиля исполнения; императивные и декларативные. Поскольку мы используем __call__()
для вызова модели (т. е. self.model(<inputs>)
), эти вызовы выполняются как операции с графом. Возможно, вы уже знаете, что выполнение графа, как известно, сложно отлаживать из-за неясностей, вызванных внутренним графом. Одно из решений, предлагаемых TensorFlow, — это использование из tf.print
заявлений.
Вы можете поместить операторы tf.print
в свои вызовы модели, и эти операторы печати будут добавлены как операции к графу, чтобы вы могли видеть значения выполненных тензоров и т. д., что позволяет вам отлаживать код, а не бросать дротики и надеяться на лучшее.
Убедитесь, что ваш оператор tf.print
печатает ввод, который появляется непосредственно перед тем временем, когда вы хотите, чтобы он был напечатан. Если вы добавите независимые/фиктивные операторы tf.print
, они не будут встроены в график в правильную позицию. Это может дать вам обманчивое ощущение, что некоторые вычисления происходят очень быстро из-за неправильного размещения графика.
Примечание о типах машин
Есть два основных типа машин, которые вы можете использовать для этого упражнения; n1
и n2
. Инстансы N2 используют процессоры Xeon 3-го поколения, оснащенные специальными наборами инструкций (AVX-512
) для ускорения таких операций, как умножение матриц. Таким образом, код TensorFlow, привязанный к процессору, работает быстрее на машинах с n2, чем на n1.
Подтверждение
Я хотел бы отметить Программы для разработчиков ML и команду за кредиты GCP, предоставленные для успеха этого руководства.