Создание артефактов и развертывание модели в кластере

В Части 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-4instances. Модели 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, предоставленные для успеха этого руководства.