Skip to content

[datasets] fix zero-dimension synthetic image for blank glyphs (#2016) - #2123

Draft
ousamabenyounes wants to merge 1 commit into
mindee:mainfrom
ousamabenyounes:fix/issue-2016
Draft

[datasets] fix zero-dimension synthetic image for blank glyphs (#2016)#2123
ousamabenyounes wants to merge 1 commit into
mindee:mainfrom
ousamabenyounes:fix/issue-2016

Conversation

@ousamabenyounes

@ousamabenyounes ousamabenyounes commented Aug 15, 2026

Copy link
Copy Markdown

Summary

Closes #2016

Recognition training with the synthetic WordGenerator intermittently dies in a DataLoader worker with:

RuntimeError: Input and output sizes should be greater than 0, but got input (H: 0, W: 29) output (H: 1, W: 128)

Root cause. synthesize_text_img sizes the canvas from font.getbbox(text). A font may map a codepoint to an empty outline: the glyph carries a horizontal advance but draws no ink, so getbbox returns a zero-height box, h becomes 0, and the resulting 0-dimension image is rejected by F.resize.

This is reachable from shipped components alone. _BASE_VOCABS["currency"] = "£€¥¢฿" flows into VOCABS["english"] and from there into 73 of the 215 shipped vocabs, including polish — and several Liberation faces draw ฿ (U+0E3F) blank:

f = ImageFont.truetype("LiberationSerif-Regular.ttf", 32)
f.getbbox("฿")            # (0, 29, 25, 29)  -> 25px advance, 0px height
f.getmask("฿").getbbox()  # None             -> nothing drawn

Fix. Characters the given fonts cannot draw fall back to the system fonts; only a character no font at all can render raises. Detection compares each glyph to the font's own .notdef box, obtained by rendering Unicode non-characters, so a tofu box counts as unrenderable too — a font merely lacking the codepoint is caught just as well as one mapping it to an empty outline.

Words are drawn with a single font, so the font is picked first and the word is built from the characters that font covers, weighted by its coverage. Fonts and per-character results are cached rather than reloaded per glyph. synthesize_text_img now rejects empty text and text drawn without ink.

Validation

RED on unmodified main:

synthesize_text_img('')   -> image (0, 0)   (no exception)
synthesize_text_img('  ') -> image (22, 0)  (no exception)

GREEN with this commit, and no baseline regression:

tests/common/    519 passed (main)  ->  527 passed (+8 new tests)
tests/pytorch/test_datasets_pt.py generators   3 passed
ruff format --check / ruff check / mypy doctr/  clean

Files changed

File Change
doctr/utils/fonts.py get_font_candidates() — the recommended families actually installed
doctr/datasets/generator/base.py per-character font resolution with system fallback, .notdef detection, caching, per-font word synthesis
tests/common/test_utils_fonts.py candidate listing skips missing families
tests/common/test_datasets.py fallback, .notdef rejection, unrenderable error, empty/inkless text
tests/pytorch/test_datasets_pt.py a word only uses characters its font can draw

@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.99%. Comparing base (5332574) to head (abb0afe).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2123      +/-   ##
==========================================
- Coverage   97.00%   96.99%   -0.01%     
==========================================
  Files         169      169              
  Lines        9611     9613       +2     
==========================================
+ Hits         9323     9324       +1     
- Misses        288      289       +1     
Flag Coverage Δ
unittests 96.99% <100.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@felixdittrich92 felixdittrich92 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ousamabenyounes 👋,

Thanks for the PR! However, this is a false positive.

  1. Whitespace characters should not be included in the vocabulary
  2. Instead of silently overriding the character when there is no font that can render it, we should explicitly raise an error. In this case, it’s a user error that should be fixed by providing font(s) capable of rendering all characters

Maybe a pre-check would be useful to ensure that all characters in the vocabulary can be rendered correctly by the provided font(s)?

@felixdittrich92
felixdittrich92 marked this pull request as draft August 18, 2026 10:40
@ousamabenyounes

Copy link
Copy Markdown
Author

Thanks for the review — agreed on the clamp, I've dropped it. Turning a degenerate canvas into a 1px strip trades a crash for silent label noise, which is worse.

Digging into the mechanism though, I don't think the "user error" framing covers the whole case. ฿ (U+0E3F) ships in VOCABS["english"], and half the Liberation faces on a stock Ubuntu draw it blank.

_BASE_VOCABS["currency"] = "£€¥¢฿" (vocabs.py:18) flows into VOCABS["english"] (:242) and from there into 73 of the 215 shipped vocabs — including polish (:308), the one used in #2016.

from PIL import ImageFont
f = ImageFont.truetype("LiberationSerif-Regular.ttf", 32)
f.getbbox("฿")            # (0, 29, 25, 29)  -> 25px advance, 0px height
f.getmask("฿").getbbox()  # None             -> nothing drawn

from doctr.datasets.generator.base import synthesize_text_img
synthesize_text_img("฿฿", font_family="LiberationSerif-Regular.ttf").size
# (55, 0)  -> the zero-dimension image behind #2016

6 of the 12 Liberation faces installed here are affected — all four LiberationSerif-*, plus LiberationSans-Italic and -BoldItalic. LiberationSans-Regular, doctr's own third default candidate, is fine. Checked on Ubuntu 24.04.3, Pillow 12.2.0, main @ 2d8e244.

So --vocab english --font LiberationSerif-Regular.ttf reaches the crash with nothing misconfigured on the user's side.

This is probably not the reporter's exact trigger: their W: 29 means text_w = 26px of total advance, so at least two blank glyphs narrower than ฿ (25px each here) — some other character in their custom font list. Same mechanism, and it's reachable from shipped components alone.

On the mechanism itself: it isn't about the font lacking the glyph. Most families draw a tofu box for .notdef, which has ink and a non-zero bbox height, so a missing character usually can't produce this — though .notdef is a normal glyph and some families ship it empty. Either way it's the ink that decides, not cmap coverage, which is what makes getmask the right predicate.

What I pushed

I've implemented your pre-check suggestion in a separate commit (a0c6e41) rather than squashing, so you can check out just that commit and test it against the clamp if you want. I'll squash before marking the PR ready for review.

  • find_unrenderable_chars() in doctr/utils/fonts.py returns every (character, font) pair the font draws without ink, keyed on get_font(family, size).getmask(char).getbbox() is None. getbbox(char) can't be the predicate — it returns the advance box, which stays non-zero for a blank glyph.
  • Both _CharacterGenerator.__init__ and _WordGenerator.__init__ run it, so an unrenderable vocabulary fails before the first batch and reports every offending pair at once. Raising from synthesize_text_img alone would fire inside a DataLoader worker and reach the user through data.reraise() — the same unreadable traceback as today, possibly several epochs in.
  • synthesize_text_img still raises on a zero-dimension canvas, as a last resort for anything the pre-check exempts.
  • Cost: one getmask per (character, font) at init.

Two decisions I'd like from you

1. Whitespace. I exempted it from the pre-check, because VOCABS["latex"] contains a space — vocabs.py:13 builds it as "".join(sorted(set("...| "))), so " " is its first character, and _BASE_VOCABS is copied into VOCABS wholesale at :235. Rejecting inkless characters unconditionally would make WordGenerator(vocab=VOCABS["latex"], ...) raise for every font. If you'd rather have the strict rule you described, the space should come out of VOCABS["latex"] first — happy to do that instead.

2. Existing setups. This starts raising for configurations that currently "work": --vocab english with a Liberation Serif font has been silently producing black ฿ samples. Hard error, or error with an opt-out?

One detail that shaped the placement: only _WordGenerator can actually crash. For len(text) == 1, synthesize_text_img takes img_size = (max(h, w), max(h, w)), so a single blank glyph yields a square (28x28 for ฿) — no exception, just a black image labelled ฿. _CharacterGenerator gets the check for label quality, not for the crash.

Validation

RED on unmodified main @ 2d8e244 with only the new tests applied:

--- tests/common/test_datasets.py::test_synthesize_text_img_rejects_inkless_text
>       with pytest.raises(ValueError):
E       Failed: DID NOT RAISE ValueError
2 failed in 0.08s

--- tests/pytorch/test_datasets_pt.py::test_generator_rejects_unrenderable_vocab
E       AttributeError: module 'doctr.datasets.generator.base' has no attribute 'find_unrenderable_chars'
2 failed in 0.10s

--- tests/common/test_utils_fonts.py
E       ImportError: cannot import name 'find_unrenderable_chars' from 'doctr.utils.fonts'

GREEN with the commit, and no baseline regression:

tests/common/          414 passed  (branch HEAD, before)  ->  416 passed  (after, +2 new tests)
tests/pytorch/test_datasets_pt.py  charactergenerator / wordgenerator / new  ->  4 passed
ruff format --check    153 files already formatted
ruff check .           All checks passed!
mypy doctr/            Success: no issues found in 171 source files

Happy to reshape any of it.

@felixdittrich92 felixdittrich92 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ousamabenyounes 👋,

First the latex VOCABS entry can be ignored atm it's not used and more like an experimential addition for further work (formula recognition)

About the ฿ char - it should work out of the box with a fallback to system fonts besides the given ones.

I would suggest the following:

fonts.py:

# Copyright (C) 2021-2026, Mindee.

# This program is licensed under the Apache License 2.0.
# See LICENSE or go to <https://opensource.org/licenses/Apache-2.0> for full license details.

import logging
import platform
from functools import lru_cache

from PIL import ImageFont

__all__ = ["get_font", "get_font_candidates"]

_FONT_CANDIDATES: dict[str, tuple[str, ...]] = {
    "Linux": (
        "DejaVuSans.ttf",
        "NotoSans-Regular.ttf",
        "LiberationSans-Regular.ttf",
        "FreeSans.ttf",
        "FreeMono.ttf",  # legacy default
    ),
    "Darwin": (
        "Arial Unicode.ttf",
        "Helvetica.ttc",
        "Arial.ttf",  # legacy default
    ),
    "Windows": (
        "arial.ttf",  # legacy default
        "segoeui.ttf",
        "tahoma.ttf",
    ),
}


def get_font_candidates() -> tuple[str, ...]:
    """Lists the recommended fonts which are installed on this system

    Returns:
        the available font families, by order of preference
    """
    candidates = _FONT_CANDIDATES.get(platform.system(), _FONT_CANDIDATES["Linux"])
    return tuple(family for family in candidates if _is_available(family))


def _is_available(font_family: str) -> bool:
    try:
        _ = ImageFont.truetype(font_family, 10)
    except OSError:
        return False
    return True


@lru_cache(maxsize=1)
def _resolve_default_font_family() -> str | None:
    """Find the first available candidate font for this platform."""
    candidates = get_font_candidates()
    return candidates[0] if candidates else None


def get_font(font_family: str | None = None, font_size: int = 13) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    """Resolves a compatible ImageFont for the system

    Args:
        font_family: the font family (or path to a font file) to use. If None,
            the best available system font is picked automatically.
        font_size: the size of the font upon rendering

    Returns:
        the Pillow font
    """
    if font_family is not None:
        return ImageFont.truetype(font_family, font_size)

    default_family = _resolve_default_font_family()
    if default_family is not None:
        return ImageFont.truetype(default_family, font_size)

    # Last resort: Pillow's built-in font.
    try:
        return ImageFont.load_default(size=font_size)
    except TypeError:  # pragma: no cover
        logging.warning(
            "Unable to load any recommended font family. Loading default PIL font, "
            "font size issues may be expected. "
            "To prevent this, it is recommended to specify the value of 'font_family'."
        )
        return ImageFont.load_default()

base.py:

# Copyright (C) 2021-2026, Mindee.

# This program is licensed under the Apache License 2.0.
# See LICENSE or go to <https://opensource.org/licenses/Apache-2.0> for full license details.

import random
from collections.abc import Callable, Sequence
from functools import lru_cache
from typing import Any

from PIL import Image, ImageDraw, ImageFont

from doctr.io.image import tensor_from_pil
from doctr.utils import Sample
from doctr.utils.fonts import get_font, get_font_candidates

from ..datasets import AbstractDataset


@lru_cache(maxsize=32)
def _load_font(font_family: str | None, font_size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
    return get_font(font_family, font_size)


def _glyph_ink(font: ImageFont.FreeTypeFont | ImageFont.ImageFont, char: str) -> tuple[tuple[int, int], bytes] | None:
    """Rasterized glyph of a character, None if it is drawn without ink."""
    mask = font.getmask(char, mode="L")
    return (mask.size, bytes(mask)) if mask.getbbox() is not None else None


@lru_cache(maxsize=32)
def _notdef_ink(font_family: str | None, font_size: int) -> frozenset[tuple[tuple[int, int], bytes]]:
    """Glyph a font falls back to for characters it does not cover (the ".notdef" box)."""
    font = _load_font(font_family, font_size)
    # Unicode non-characters are never mapped, so they always yield ".notdef"
    return frozenset(ink for char in ("\ufffe", "\U0010ffff") if (ink := _glyph_ink(font, char)) is not None)


@lru_cache(maxsize=8192)
def _renders_char(char: str, font_family: str | None, font_size: int) -> bool:
    """Whether a font draws the actual glyph of a character, rather than nothing or a box."""
    ink = _glyph_ink(_load_font(font_family, font_size), char)
    return ink is not None and ink not in _notdef_ink(font_family, font_size)


def _resolve_fonts(font_family: str | list[str] | None) -> list[str | None]:
    font_families: list[str | None] = [*font_family] if isinstance(font_family, list) else [font_family]
    for font in font_families:
        try:
            _ = _load_font(font, 32)
        except OSError:
            raise ValueError(f"unable to locate font: {font}")
    return font_families


def _fonts_per_char(
    vocab: str, font_families: Sequence[str | None], font_size: int = 32
) -> dict[str, tuple[str | None, ...]]:
    """Maps each character of the vocab to the fonts able to render it.

    Characters none of the given fonts can render fall back to the system fonts.

    Args:
        vocab: the characters to render
        font_families: the font families to pick from
        font_size: the size the characters are rendered at

    Returns:
        the fonts able to render each character

    Raises:
        ValueError: if a character is rendered by none of the fonts
    """
    mapping = {
        char: tuple(font for font in font_families if _renders_char(char, font, font_size))
        for char in dict.fromkeys(vocab)
    }
    missing = [char for char, fonts in mapping.items() if not fonts]
    if missing:
        fallbacks = [font for font in get_font_candidates() if font not in font_families]
        mapping.update(
            {char: tuple(font for font in fallbacks if _renders_char(char, font, font_size)) for char in missing}
        )
        unrenderable = [char for char in missing if not mapping[char]]
        if unrenderable:
            raise ValueError(
                f"the following characters cannot be rendered, neither by the given fonts {list(font_families)} "
                f"nor by the system fonts {fallbacks}: "
                f"{', '.join(f'{char!r} (U+{ord(char):04X})' for char in unrenderable)}. "
                "They are drawn blank or as a '.notdef' box, please provide font(s) covering the whole vocab."
            )
    return mapping


def synthesize_text_img(
    text: str,
    font_size: int = 32,
    font_family: str | None = None,
    background_color: tuple[int, int, int] | None = None,
    text_color: tuple[int, int, int] | None = None,
) -> Image.Image:
    """Generate a synthetic text image

    Args:
        text: the text to render as an image
        font_size: the size of the font
        font_family: the font family (has to be installed on your system)
        background_color: background color of the final image
        text_color: text color on the final image

    Returns:
        PIL image of the text

    Raises:
        ValueError: if the text is empty or rendered without ink
    """
    if not text:
        raise ValueError("unable to synthesize an image from an empty text")

    background_color = (0, 0, 0) if background_color is None else background_color
    text_color = (255, 255, 255) if text_color is None else text_color

    font = _load_font(font_family, font_size)
    left, top, right, bottom = font.getbbox(text)
    text_w, text_h = right - left, bottom - top
    if text_w <= 0 or text_h <= 0:
        raise ValueError(f"font {font_family!r} draws no ink for {text!r}, resulting in a zero-dimension image")

    h, w = int(round(1.3 * text_h)), int(round(1.1 * text_w))
    # If single letter, make the image square, otherwise expand to meet the text size
    img_size = (h, w) if len(text) > 1 else (max(h, w), max(h, w))

    img = Image.new("RGB", img_size[::-1], color=background_color)
    d = ImageDraw.Draw(img)
    # Offset so that the text is centered
    text_pos = (int(round((img_size[1] - text_w) / 2)), int(round((img_size[0] - text_h) / 2)))
    # Draw the text
    d.text(text_pos, text, font=font, fill=text_color)
    return img


class _CharacterGenerator(AbstractDataset):
    def __init__(
        self,
        vocab: str,
        num_samples: int,
        cache_samples: bool = False,
        font_family: str | list[str] | None = None,
        img_transforms: Callable[[Any], Any] | None = None,
        sample_transforms: Callable[[Sample], Sample] | None = None,
    ) -> None:
        self.vocab = vocab
        self._num_samples = num_samples
        self.font_family = _resolve_fonts(font_family)
        self._fonts_per_char = _fonts_per_char(self.vocab, self.font_family)
        self.img_transforms = img_transforms
        self.sample_transforms = sample_transforms

        self._data: list[Image.Image] = []
        if cache_samples:
            self._data = [
                (synthesize_text_img(char, font_family=font), idx)  # type: ignore[misc]
                for idx, char in enumerate(self.vocab)
                for font in self._fonts_per_char[char]
            ]

    def __len__(self) -> int:
        return self._num_samples

    def _read_sample(self, index: int) -> tuple[Any, int]:
        # Samples are already cached
        if len(self._data) > 0:
            idx = index % len(self._data)
            pil_img, target = self._data[idx]  # type: ignore[misc]
        else:
            target = index % len(self.vocab)
            char = self.vocab[target]
            pil_img = synthesize_text_img(char, font_family=random.choice(self._fonts_per_char[char]))
        img = tensor_from_pil(pil_img)

        return img, target


class _WordGenerator(AbstractDataset):
    def __init__(
        self,
        vocab: str,
        min_chars: int,
        max_chars: int,
        num_samples: int,
        cache_samples: bool = False,
        font_family: str | list[str] | None = None,
        img_transforms: Callable[[Any], Any] | None = None,
        sample_transforms: Callable[[Sample], Sample] | None = None,
    ) -> None:
        self.vocab = vocab
        self.wordlen_range = (min_chars, max_chars)
        self._num_samples = num_samples
        self.font_family = _resolve_fonts(font_family)
        fonts_per_char = _fonts_per_char(self.vocab, self.font_family)
        # A word is drawn with a single font, so the font is picked first and the word is
        # built from the characters this font can render, weighted by its coverage
        self._vocab_per_font = {
            font: "".join(char for char in self.vocab if font in fonts_per_char[char])
            for font in dict.fromkeys(font for fonts in fonts_per_char.values() for font in fonts)
        }
        self._fonts = list(self._vocab_per_font)
        self._font_weights = [len(vocab_) for vocab_ in self._vocab_per_font.values()]
        self.img_transforms = img_transforms
        self.sample_transforms = sample_transforms

        self._data: list[Image.Image] = []
        if cache_samples:
            self._data = [self._synthesize_sample() for _ in range(num_samples)]  # type: ignore[misc]

    def _generate_string(self, min_chars: int, max_chars: int, vocab: str | None = None) -> str:
        num_chars = random.randint(min_chars, max_chars)
        return "".join(random.choice(self.vocab if vocab is None else vocab) for _ in range(num_chars))

    def _synthesize_sample(self) -> tuple[Image.Image, str]:
        font = random.choices(self._fonts, weights=self._font_weights)[0]
        text = self._generate_string(*self.wordlen_range, vocab=self._vocab_per_font[font])
        return synthesize_text_img(text, font_family=font), text

    def __len__(self) -> int:
        return self._num_samples

    def _read_sample(self, index: int) -> tuple[Any, str]:
        # Samples are already cached
        if len(self._data) > 0:
            pil_img, target = self._data[index]  # type: ignore[misc]
        else:
            pil_img, target = self._synthesize_sample()
        img = tensor_from_pil(pil_img)

        return img, target

It adds the prev check that all chars can be rendered additional it caches the fonts instead of reloading at each char

mindee#2016)

A font may map a codepoint to an empty outline: the glyph carries a horizontal
advance but no ink, so a word made only of such characters yields a
zero-dimension image and an opaque resize failure inside a DataLoader worker.

Rather than refusing the vocabulary, characters the given fonts cannot draw now
fall back to the system fonts, and only a character no font at all can render
raises. Detection compares each glyph to the font's own ".notdef" box, obtained
by rendering Unicode non-characters, so a tofu box counts as unrenderable too.
Whitespace is exempt: it is inkless in every font by design, and judging it by
ink would reject any vocabulary containing a space.

Words are drawn with a single font, so the font is picked first and the word is
built from the characters that font covers, weighted by its coverage. Fonts and
per-character results are cached instead of being reloaded for every glyph.

synthesize_text_img now rejects empty text and text drawn without ink.
@ousamabenyounes

ousamabenyounes commented Aug 20, 2026

Copy link
Copy Markdown
Author

Applied your version, rebased onto current main as one commit — f47c4f5.

One thing to flag before this is reviewable: as written, the check rejects any vocabulary containing a space.

_fonts_per_char("hello world", [None])
# ValueError: the following characters cannot be rendered ... ' ' (U+0020)

A space is inkless in every font by design, so no font can be said to "cover" it. I exempted whitespace — that also un-breaks VOCABS["latex"], independently of whether that vocab is used. An all-whitespace word still raises, but from synthesize_text_img with a clear message rather than as a resize failure in a worker.

The fallback itself does the job: with font_family=["LiberationSerif-Regular.ttf"], ฿ resolves to a system font while a stays on Liberation. And comparing each glyph to the font's own .notdef catches the tofu case I thought was undetectable.

I kept the capability tests independent of installed fonts, though. Asserting that Liberation Serif draws ฿ blank, or that DejaVu boxes U+13000, is true here but is not a stable contract across runners or font versions. The fallback, .notdef rejection and error paths are asserted deterministically, and a single real-font smoke test keeps the Pillow/FreeType path covered.

tests/common 519 → 529, ruff and mypy clean.

@felixdittrich92 felixdittrich92 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two small things left 👍

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove all the comments under the functions here - the test names are clear enough

# produced blank glyphs and 0-dimension images (#2016).
from doctr.datasets.generator import base

monkeypatch.setattr(base, "get_font_candidates", lambda: ())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code can be moved in the existing test_wordgenerator function with a small comment and the import should be moved on top of the file where the other imports are

def test_wordgenerator(monkeypatch):
    ... existing code

   # each word must be drawn with a single font that can render all its chars (#2016)
    monkeypatch.setattr(base, "get_font_candidates", lambda: ())
    monkeypatch.setattr(base, "_load_font", lambda family, size: None)
    # "b" is drawn by the second font only, "a" by both
    monkeypatch.setattr(base, "_renders_char", lambda char, font, size: char == "a" or font == "second.ttf")
    monkeypatch.setattr(base, "synthesize_text_img", lambda text, font_family=None: Image.new("RGB", (8, 8)))

    ds = datasets.WordGenerator(
        vocab="ab",
        min_chars=4,
        max_chars=4,
        num_samples=16,
        cache_samples=True,
        font_family=["first.ttf", "second.ttf"],
    )
    assert ds._vocab_per_font == {"first.ttf": "a", "second.ttf": "ab"}
    for _, target in ds._data:
        assert len(target) == 4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fatal error while training with Word Generator on multi GPU

2 participants