DocTer: Documentation Guided Fuzzing for Testing Deep Learning API Functions

Danning Xie, Yitong Li, Mijung Kim, Hung Viet Pham, Lin Tan, Xiangyu Zhang, Michael W. Godfrey

Introduction

Input constraints are useful for various software development tasks (Wong et al., 2015; Zhai et al., 2020; Blasi et al., 2018; Godefroid et al., 2008; Majumda and Xu, 2007). For example, input constraints of a function enable the generation of valid inputs, i.e., inputs that follow these constraints, to test the function deeper. API functions of DL libraries expect their input arguments to follow constraints, many of which are DL-specific. For example, one parameter input of the PyTorch API function torch.as_strided has to be a tensor. A tensor is represented using an n-dimensional array, where nn is a non-negative integer. Any input that cannot be interpreted as a tensor (e.g., a Python list) is invalid. Many such DL-specific input constraints are described informally in free-form API documentation. The availability of such DL API documentation presents a great opportunity to automatically extract DL-specific constraints for better testing and other software development tasks.

Specifically, DL libraries’ API functions require two types of constraints for their input arguments: (1) data structures and (2) properties of these data structures. First, DL libraries often require their input arguments to be specific data structures such as lists, tuples, and tensors to perform numerical computations. For example, input of the PyTorch API function torch.as_strided has to be a tensor as dictated by its API document. Any input that cannot be interpreted as a tensor (e.g., a Python list) is rejected by the function’s input validity check. Such invalid inputs exercise only the input validity checking code, failing to test the core functionality of the API function. To test as_strided’s core functionality, a testing technique needs to generate a tensor object for the input parameter.

Second, API functions of DL libraries require their arguments to satisfy specific properties of data structures. Generating a correct data structure with incorrect properties often fails the input validity checking of the DL API functions. They often require two common properties of a data structure—dtype and shape. Property dtype specifies the data type of the data structure (e.g., int32, float64, and String). In Fig. 1a, the dtype of the parameter padding should be String. Property shape specifies the length of each dimension of the data structure. For example, a shape of 3×43\times 4 matrix is a 2-dimensional tensor with the first dimension of 3 elements and the second dimension of 4 elements. As another example, Fig. 1a shows the document for TensorFlow API tf.nn.max_pool3d, which indicates that the parameter input should be a tensor of 5 dimensions, with the size of each dimension unspecified. Similarly, any inputs that violate these dtype or shape requirements are rejected, failing to test the core functionality of the API function.

While existing techniques can extract constraints from code or software text (e.g., comments and documents), they are insufficient for extracting DL-specific constraints. Specifically, while Pytype (pyt, 2016) infers data types from Python code, it cannot precisely infer types for DL libraries because it cannot analyze across Python and C++ code. In addition, it cannot extract numerical constraints such as shape and range. Existing techniques that derive constraints from software text extract different types of constraints that are not DL-specific, such as exceptions (Blasi et al., 2018; Zhai et al., 2020), command-line options and file formats (Wong et al., 2015), locking (Tan et al., 2007), call-relations (Tan et al., 2007; Lv et al., 2020), interrupts (Tan et al., 2011), nullness (Tan et al., 2012; Zhou et al., 2018) and inheritance relations (Zhou et al., 2018). Although some (Blasi et al., 2018; Zhou et al., 2018; Zhai et al., 2020; Lv et al., 2020) can extract constraints related to valid ranges, those are only a small portion of DL-specific constraints (Section 4.1). Techniques such as C2S (Zhai et al., 2020) require pairs of Javadoc comments and formal JML (jml, 1999) constraints as input. For DL API functions, such formal constraints are unavailable.

To fill this gap, we design and implement a new technique—DocTer—to analyze API documentation to extract DL-specific input constraints. DocTer features a novel method to automatically derive constraint extraction rules from a small set of manually annotated API documents with precise constraint information. These rules can predict API parameter constraints based on syntactic patterns in the form of dependency parse tree in documents. They are then applied to the full sets of API documents of popular DL libraries to extract constraints.

To demonstrate the effectiveness of these extracted constraints, DocTer uses them to guide and improve an important task — generating test cases automatically to test DL API functions. Testing API functions of DL libraries (e.g., TensorFlow (Abadi et al., 2016) and PyTorch (Paszke et al., 2019)) is crucial because these libraries are widely used and contain software bugs (Zhang et al., 2020; Islam et al., 2019; Zhang et al., 2018; Humbatova et al., 2020; Pham et al., 2019), which hurt not only the development but also the accuracy and speed of the DL models.

Yet, generating test cases for DL libraries’ API functions is challenging. If a test-generation tool is (1) unaware of DL-specific constraints or (2) incapable of using these constraints to generate diverse inputs, it is practically impossible to generate valid inputs to reach deeper states and test the core functionality of DL API functions. Existing test-generation tools (afl, 2013; Swiecki, 2015; lib, 2015; Pacheco and Ernst, 2007; Fraser and Arcuri, 2013; Fuz, 2019) such as AFL (afl, 2013) and libFuzzer (lib, 2015) have no knowledge of such input constraints, thus are very limited in testing DL API functions. DocTer addresses these challenges by using the following techniques:

(1) DL-specific constraint extraction: Since API documents are written informally in a natural language, manually extracting constraints from a large number of API documents (e.g., TensorFlow v2.1.0 has 2,334 pages of API documents and 854,900 words) is inefficient and tedious. In addition, since these documents are constantly evolving, it is undesirable and error-prone to manually analyze them each time the documents are updated which can be as frequent as every commit. To address these challenges, we develop a novel method that can automatically derive a set of rules that predict parameter constraints from parse tree patterns of API description. Given a small set of API function descriptions and the corresponding constraint annotations, DocTer identifies a set of rules as an optimal mapping that can minimize prediction errors and achieve the maximum coverage of constraints. By applying these constructed rules to a much larger set of real-world documents, DocTer can automatically extract DL-specific constraints for API functions of the most widely used libraries.

(2) DL-specific input generation: After extracting DL-specific input constraints (e.g., Fig. 1b), DocTer uses these constraints to guide test generation to produce valid inputs (e.g, Fig. 1c), invalid inputs, and boundary inputs (such as -MaxInt, 0, and MaxInt for the constraint of dtype of int). DocTer evaluates valid inputs by checking if the API runs successfully without failures, e.g., crashes. If a failure occurs with a valid input, the generated test has manifested a bug in the implementation of the API’s core functionality.

Fig. 1 shows a previously unknown bug detected by DocTer in TensorFlow along with its patch that the TensorFlow developers committed after we reported the bug. The API document in Fig. 1a indicates that the shape of input is 5-D, and ksize is an integer or a list of 1, 3, or 5 integers. DocTer automatically extracts the constraints in Fig. 1b and generates the bug-triggering input in Fig. 1c. Detailed constraint formats are explained in Section 2.2. DocTer generates a valid input. Specifically, parameter input is a five-dimensional (5-D) tensor as a constant (tf.constant), where the five pairs of square brackets denote a five-dimensional tensor. Parameter ksize is a list of length 1, whose element is a zero (i.e., ), parameter strides is , and parameter padding is "VALID".

This bug is only triggered when the parameter ksize has a zero value. This zero value causes a division-by-zero fault, resulting in a floating point exception. To trigger this bug, the parameter padding must be either "VALID" or "SAME". Otherwise the function’s input validity checking would reject the input with an InvalidArgumentError. Therefore, it is practically impossible for techniques that randomly generate inputs to trigger this bug. After we reported this bug, the TensorFlow developers added the non_negative range validation for the parameter ksize (Fig. 1d).

In addition, DocTer generates invalid inputs that violate the constraints to detect crashes. Despite invalid inputs, DL API functions should not crash. Instead, they are expected to report an invalid input (e.g., by throwing an exception or printing an error message). This point is well confirmed by an API developer after we reported a crash bug detected by DocTer “A segmentation fault is never OK and we should fix it with high priority”. Such invalid-input generation is impossible without the constraints.

(3) Documentation-bug detection: Since incorrect API documentation provides false information about APIs, which often misleads API users to introduce bugs in code (Tan et al., 2007), it is important to detect bugs in API documents as well. Different from prior work (Tan et al., 2007, 2012) that detects inconsistencies between documents/comments and code, DocTer detects inconsistencies within documents. For example, in the document of tf.keras.backend.moving_average_update, the description for the parameter value is “ …with the same shape as `variable`,…”, but the parameter variable is not documented. This documentation bug of erroneous parameter dependencies has been fixed after we report it.

2. Contributions

In this paper, we make the following contributions:

A novel rule construction technique that formulates the challenge as an optimization problem aiming to find the smallest set of rules that can make the largest number of correct extractions of parameter constraints. We also develop an approximate solution to the problem based on sample space conditional probability computation.

A document-analysis technique that extracts 16,035 constraints automatically from API documentation with the focus on four categories of input properties in DL APIs: structure, dtype, shape, and valid values for 2,415 API functions across the three widely-used DL libraries, TensorFlow (Abadi et al., 2016), PyTorch (Paszke et al., 2019), and MXNet (Chen et al., 2015). The constraint extraction precision is 85.4%.

An application of our extracted constraints to guide the generation of DL-specific inputs.

A tool DocTer that combines the techniques above, and detects 94 bugs in 174 APIs from the three libraries, while a baseline that generates inputs without the knowledge of constraints detects 59 bugs only. Among the 94 bugs, 63 are previously unknown bugs, 54 of which have been fixed (49) or confirmed (5) by the developers after we report them. Notably, one of the previously unknown bugs was added to the CVE vulnerability database for TensorFlow after we reported it. In addition, DocTer detects 43 documentation bugs, 39 of which have been fixed (35) or confirmed (4) after we report them.

While our rule construction and constraint extraction techniques are general, the constructed rules and extracted constraints are domain-specific. We focus on testing DL libraries due to their importance and the lack of available constraint-extraction techniques for them. We leave the extension to other domains, e.g., classic machine learning libraries such as scikit-learn (Pedregosa et al., 2011), as future work.

Availability: We share the tool DocTer, bug list, and data in (sup, 2022).

Approach

Fig. 2 shows the overview of DocTer using an example of the TensorFlow API tf.nn.atrous_conv2d. DocTer consists of three phases. The rule construction phase (i.e., the green box in Fig. 2) takes a small portion of API documents with annotations to construct a set of rules that can extract concrete constraints from API documents. The rules are constructed by an optimization-based method. They are mappings from document dependency parse trees to the corresponding abstract parameter constraints in the form of assertions (e.g., on dtype and shape), which are called Abstract Constraints (ACs). In the constraint extraction phase (i.e, the orange box), the rules are applied to concrete API documents to derive concrete parameter constraints. To demonstrate the effectiveness of these extracted constraints, in the testing phase (the purple box), DocTer generates test inputs either conforming or violating the constraints (by the input generator), and executes the inputs to detect bugs (by the test case evaluator), in an iterative fashion.

A major challenge of constraint extraction is analyzing free-form API documentation written in the natural language (Wong et al., 2015; Tan et al., 2012; Blasi et al., 2018). We observe that developers have limited ways to express input constraints in natural language. However, these expressions are instantiated differently for different APIs and composed together in various ways, leading to complex overall syntactic structures that are difficult to translate to parameter constraints. We hence devise a novel method that works as follows. It first preprocesses/normalizes the documents to dependency parse trees and then breaks the trees into subtrees. With a small set of API documents and the corresponding manually annotated ACs, an algorithm is developed to identify the optimal mappings between subtrees and parameter constraints that can maximize the matching of the mapped constraints and the ground-truth annotations. These mappings are essentially our constraint extraction rules. DocTer applies these rules to extract constraints from API documents automatically. Apart from a fixed cost of annotating a small portion (e.g., 30%) of API parameters, our process is automatic and can be reapplied to future versions or another relevant library with little manual work.

Preprocessing: During both the rule construction and constraint extraction phases, DocTer performs two preprocessing steps: normalization and dependency tree parsing to convert free-form API descriptions to dependency parse trees (parse trees for short). (Fig. 2). The normalizer replaces keywords with abstractions, e.g., replacing data type keywords (e.g., int32, float64) with D_TYPE, structure type keywords (e.g., Tensor, list) with D_STRUCTURE, and integer constants with CONSTANT_NUM. This normalization improves the performance of the rule construction algorithm by suppressing instance differences. We use dependency tree parser (Manning et al., 2014) to convert the normalized sentences to parse trees.

An example: Our rule construction component identifies a rule 1 (row one of the Rules table in Fig. 2) that maps a frequently occurring subtree pattern “of type D_TYPE” (e.g., appearing in both sentences 1 and 2 in Fig. 2) to an abstract type assertion (an AC) pT∈{p_{T}\in\{D_TYPE}\}, which means that the valid dtype of parameter pp should be one from the set {D_TYPE}, where D_TYPE is an abstraction of one or more dtypes, which, in this example, are float16, float32, and float64. In the annotated dataset, the conditional probability of the type assertion pT∈{p_{T}\in\{D_TYPE}\}, given the subtree pattern is 1.0 and the pattern is the smallest with such predictive power. Thus, the rule constructor is able to create rule 1.

The constraint extractor applies constructed rules to all the preprocessed API documents to automatically extract a set of constraints for each input parameter. For example, in the Tensorflow document for API tf.nn.atrous_conv2d, one of the parse trees parsed from the description for parameter value (e.g., Fig. 2) contains two frequent subtrees “a CONSTANT_NUM d D_STRUCTURE” and “of type D_TYPE”. These structures correspond to rules 1, 2, and 3. DocTer applies these rules and obtains the extracted ACs for the parameter in Fig. 2 (the middle of the orange box), e.g., p=value, AC(p)=pT∈{D_TYPE}∧pS∈{D_STRUCTURE}∧pD∈{CONSTANT_NUM}p=value,~{}AC(p)=p_{T}\in\{\texttt{\small D\_TYPE}{}\}\wedge p_{S}\in\{\texttt{\small D\_STRUCTURE}{}\}\wedge p_{D}\in\{\texttt{\small CONSTANT\_NUM}{}\} , where pTp_{T}, pSp_{S} and pDp_{D} represent the data type, data structure and number of dimensions of parameter pp, respectively. DocTer further instantiates the abstract symbols (D_TYPE, D_STRUCTURE, and CONSTANT_NUM) with the corresponding value and types (i.e., float, Tensor, and 4) from the original sentence to convert the ACs to concrete constraints. We now discuss each individual step.

2. Preprocessing

The first step of DocTer is to collect the natural language API documents. They are at high volume. For example, there are 2,334 pages of API documents and 854,900 words in TensorFlow v2.1.0. It is hence a daunting and tedious task for developers to manually examine such a large set of API documents to identify constraints.

API document collection and tokenization: After collecting the API documents (in the form of HTML pages from DL libraries’ websites), DocTer parses these files to obtain API signatures and parameter descriptions with an HTML parsing tool (bea, 2004). Since sentence is a natural unit of organizing constraints, DocTer further splits the description into sentences with a sentence segmentator (Bird et al., 2009).

Normalization: The tokenized sentences are normalized. While developers may have a small number of patterns expressing parameter constraints, these patterns have diverse instantiations according to the concrete data types and parameters involved. Normalization abstracts away these instantiation differences.

Specifically, DocTer normalizes keywords such as (1) data types (e.g., int32) and (2) data structures (e.g., tensor) as D_TYPE and D_STRUCTURE, respectively. To get the list of keywords for data types, we collect a list of supported data types from each library (tf_, 2020; pt_, 2019; mx_, 2019). We then expand such a list with informal variations (e.g., “integer”, and “ints”) and missing common types (e.g., String) to match the format of API documents. In total, we use 84, 74, and 53 type keywords for TensorFlow, PyTorch, and MXNet, respectively. The full list of keywords can be found in (sup, 2022).

DocTer also normalizes constants such as (3) integer, (4) float, (5) boolean values as CONSTANT_NUM, CONSTANT_FLOAT, and CONSTANT_BOOL. It also replaces (6) relational expressions (e.g., “≥1\geq 1”) with REXPR and replaces (7) parameter names with PARAM.

The content that is (8) quoted often refers to enumerate values, so DocTer replaces such content with ENUM. For example, “‘NWC’ and ‘NCW’ are supported.” is normalized to “ENUM are supported”. The shape of a parameter is often put within (9) a pair of square brackets or parentheses, DocTer replaces such content with SHAPE. For example, “A Tensor of shape [num_classes,dim]” is normalized to “A D_STRUCTURE of shape SHAPE”. Finally, consecutive abstract annotations of the same type are replaced with just one. For example, the three type keywords in “Must be of type ‘float16‘, ‘float32‘, or ‘float64‘.” (Fig. 2) are replaced by a single D_TYPE, resulting in a normalized sentence “must be of type D_TYPE”.

Dependency tree parsing: Once the sentences are normalized, they are fed to the dependency tree parser (Manning et al., 2014), which conducts POS-Tagging and builds tree structure relationships (i.e., dependency parse trees) between words of a sentence based on the grammatical structure. For example, in the sentence “a D_STRUCTURE of type D_TYPE” from Fig. 2, the words “D_STRUCTURE”, “type”, and “D_TYPE” are first tagged as NN (noun). Then the parser conducts dependency parsing and generates the dependency parse tree as shown in the figure where D_STRUCTURE is the root, and D_TYPE is the nominal modifier (dep, 2014) of the root.

Annotating a subset of API descriptions with ACs: To support rule construction, we randomly pick a small set of the parameters (30%) and manually annotate them with their ACs. To minimize possible biases, the process involves three co-authors. Two authors independently annotate with 98.2% agreement. All disagreements are resolved with a third author to reach a consensus.

Abstract Constraints (AC): ACs are abstract constraints/assertions. These assertions are not on concrete dtype or shape but rather abstract ones. An AC for a parameter pp is denoted in the form of pt∈{T1,T2,...}p_{t}\in\{T_{1},T_{2},...\} where tt is the category of AC, and T1T_{1} and T2T_{2} are the possible abstract values. For example, pT∈{D_TYPE}p_{T}\in\{\texttt{\small D\_TYPE}\} means that pp is of D_TYPE. Specifically, the annotations of parameter pp are designed as follows:

pTp_{T} denotes the data type of an abstract constraint (AC) of pp.

pSp_{S} denotes the data structure of an AC of pp.

pSP∈NDp_{SP}\in\mathbf{N}^{D} where D∈ND\in\mathbf{N} denotes the shape AC of pp, where DD represents the number of dimensions of pp.

pD∈Np_{D}\in\mathbf{N} denotes the number of dimensions of an AC of pp. Therefore, pD=pSP.lengthp_{D}=p_{SP}.length if pp is a tensor or pD=0p_{D}=0 if pp is a scalar.

pip_{i} denotes an element in parameter pp if pp is a tensor, where i=1,2,...,Prod(pSP)i=1,2,...,Prod(p_{SP}). When pp is a scalar, its value is p0p_{0}.

An AC can be instantiated to different concrete constraints. Table 1 provides examples of ACs (first column as part of the rules) and their instantiations (last column) for several APIs.

AC annotation categories: We focus on annotating four categories of ACs (i.e., structure, dtype, shape, and valid value) because they represent the most common (93.6%) properties of input parameters of API functions in major DL libraries. The four categories are:

structure: the type of data structure that stores a collection of values for the input parameter, such as list, tuple, and n-dimensional array (i.e., tensor).

dtype: the data type, such as int, float, boolean, and String, of the parameter or the elements of structure.

shape: the shape or number of dimensions of the parameter. For example, in row 2 of Table 1, weights is of shape [num_classes, dim] (i.e., it is a 2-D array where the sizes of its first and second dimensions are num_classes and dim, respectively).

valid value: a set of valid values (e.g., parameter padding can only be either "VALID" or "SAME") or the valid range of a numerical parameter (e.g., a float between 0 and 1).

We make reasonable assumptions when annotating API descriptions. For example, a parameter is assumed to be a 0-dimensional non-negative integer if the document states it is a “number of …”. The assumptions are in the supplementary material (sup, 2022).

3. Rule Construction

Although API descriptions are in a natural language, these descriptions often share a small number of syntactic patterns. For example, a constraint of dtype assertion is mostly described by two syntactic patterns: “must be one of the following types …” and “a tensor of type …” in TensorFlow. Our idea is hence to identify such patterns in API descriptions and project them to the corresponding parameter constraints. We call such projections the constraint extraction rules.

Automatically deriving such rules is challenging. The first challenge is that a syntactic pattern may have different instantiations in various API descriptions, depending on the variables and types. For example, the aforementioned pattern “A ‘Tensor’ of type …” is instantiated to “A ‘Tensor’ of type ‘string”’ and “A ‘Tensor’ of type ‘int32”’ in two respective parameters contents and crop_window in API tf.io.decode_and_crop_jpeg. Our normalization step substantially mitigates this problem. The second challenge is that such patterns are often convoluted in the overall syntactic structure of an API description. For example, consider the description of parameter value as shown in Fig. 2. The normalized sentence “a CONSTANT_NUM d D_STRUCTURE of type D_TYPE.” is composed of two syntactic patterns “CONSTANT_NUM d D_STRUCTURE” and “of type D_TYPE”. Third, these patterns may have arbitrary sizes.

An optimization problem: We propose a novel method to automatically derive the extraction rules from a small set of APIs with their ACs manually annotated. We formulate it as an optimization problem. Specifically, given an API ff, its normalized natural language description is denoted as DfD_{f}, its ACs are denoted as AfA_{f}. We use trees(Df)\mathit{trees}(D_{f}) to denote all the subtrees of the parse tree of DfD_{f}. For example, Fig. 2 gives a 3-layer parse tree of the normalized sentence “a D_STRUCTURE of type D_TYPE”, which has subtrees “of type D_TYPE” and “a D_STRUCTURE type”. Such subtrees consider both parent-child (direct) connections and ancestor-descendant (indirect) connections. We use Tree\mathit{Tree} and AC\mathit{AC} to denote the domains of subtrees and ACs, respectively. Our goal is hence to derive a mapping R:Tree→ACR:\mathit{Tree}\rightarrow\mathit{AC}. The mapping should satisfy the following optimization objectives. First, the tree patterns can be used to precisely predict the corresponding ACs. If we consider the tree patterns and the ACs form a distribution, the conditional probability of an AC given the condition of its tree pattern shall be high. Second, all ACs can be predicted by these patterns, i.e., our mappings should be comprehensive. Third, the number of the mappings from a tree pattern to an AC in RR is minimum. The objective is needed otherwise a simple solution would be to include all tree patterns in descriptions. Fourth, the size of each tree pattern is minimum. It is very likely that a tree pattern and its sub-patterns both can predict a constraint. In such cases, we prefer the smallest one, which provides the maximum generalization.

Formally, the process to find the set of rules, that is, the optimal mappings RR, is the following.

Here, N\mathcal{N} denotes the distribution of API description and the corresponding ground-truth ACs. The above formula means that we are looking for an RR that can minimize the expected objective function value for all samples (Df,Af)∼N(D_{f},A_{f})\sim\mathcal{N}. The objective function is the sum of four terms. The first one is the average conditional probabilities for all the ACs in AfA_{f}. Intuitively, it means that for each AC aa in an API, our mapping RR should associate a tree pattern tt with aa such that the conditional probability P(a∣t)P(a|t) is maximum. The second term means that the number of ACs in AfA_{f} for which RR does not have a mapping is minimum. This is to maximize the coverage of our rules. The third term is to minimize the size of RR. The last term is to minimize the size of each tree pattern in RR.

Approximate solution: Solving the above optimization problem is difficult because it is discrete. Its complexity is NP. This is not a typical learning problem as it does not aim to learn a distribution but rather to construct a minimum and yet complete set of rules. In addition, the amount of data available for training is relatively small compared to other domains that have successful applications of deep learning models. We hence devise an approximate solution. The first term can be approximated by computing the sample space conditional probabilities and then including the top associations in RR. The sample space conditional probabilities are:

Intuitively, it is the number of co-occurrences of a tree pattern and an AC divided by the number of occurrences of the tree pattern. We further observe that if a tree pattern tt is rare, it is usually not related to parameters. As such, we can focus on the frequent subtrees. We use frequent subtree mining (Zaki, 2005) to efficiently discover the most frequent subtrees. We filter out the rare tree patterns with threshold min_support, i.e., any tree patterns that occur less than or equal to min_support times are discarded. The second and third terms are approximated by selecting only the associations (of aa and tt) with a large sample space conditional probability. Specifically, DocTer includes in RR associations (of aa and tt) with P‾(a∣t)\overline{P}(a|t) greater than or equal to min_confidence (the selection of and min_support and min_confidence is a trade-off between precision and recall and will be discussed in Section 3). An alternative to approximate the second term is to use a greedy algorithm to include additionally needed tree patterns to achieve (full) coverage of ACs. However, this often contradicts the third term. Empirically (see Section 4.1), we find that including the top associations provides a good balance. To approximate the fourth term, which minimizes the tree patterns, we keep only the smallest tree pattern when multiple patterns can be used to predict an AC.

Algorithm 1 formalizes the process of finding the approximated solution. For each sentence in the annotated data (Sample), the dependency tree parser parses the sentence and generates the parse tree, which is added to the set DfD_{f} (lines 5–7). Then, we select the set of frequent tree patterns FreqDfFreqD_{f} whose frequencies are at least min_support using frequent subtree mining (getFreqSubtree) (line 9). This process keeps only tree patterns whose size is smaller than or equal to MAX_SIZE. For each frequent tree pattern tt, selectAC selects a set of ACs (aa) with probabilities P‾(a∣t)\overline{P}(a|t) greater than or equal to min_confidence. Then, each association of aa and tt is added to the set RR (lines 10–14).

Table 1 shows examples of the automatically discovered rules by DocTer (col. “Extraction rules”) and examples of matched sentences (col. “API sentences”). For example, rule 3 in Table 1 is used to extract the enumerated value (e.g., valid value) of parameter pp, which is associated with the AC p0∈ENUMp_{0}\in{ENUM}. In rule 4, the pattern “number of” implies parameter p should be a 0-dimensional non-negative integer.

4. Constraint Extraction

Given an API description, the constraint extractor matches the tree patterns in the rules with the parse tree of the description. Matches are then projected to the corresponding ACs, which are further instantiated in the context of the description to derive the concrete constraints. For example, in Fig. 2, the constraint extractor finds rules 1, 2, and 3 match the two subtrees “a CONSTANT_NUM d D_STRUCTURE” and “of type D_TYPE” in the parse tree of the value description. DocTer then assigns the three relevant ACs to the parameter value. DocTer then instantiates the ACs with the concrete data types, structure types, and constants to generate the final constraints. In row 2 of Table. 1, the annotation SHAPE is instantiated based on the original text, i.e., “[num_classes, dim]”. Row 3 in Table. 1 shows an example rule of valid value constraints. DocTer detects the pattern “either ENUM” and uses it to extract the valid value constraint in the last column.

Constraint dependencies graphs: The description of one parameter often refers to the dtype or value of another parameter from the same API function. In such cases, DocTer extracts constraints that involve dependencies among input parameters. These dependencies are useful not only for generating valid inputs but also for determining the parameters’ generation order. The automatically constructed rules can detect dtype dependencies. For example, row 1 in the Table. 1) shows the pattern “must have the same type as …” which indicates a type dependency. In this example, the operator ‘&’ is to acquire the dtype of a parameter. An example of shape dependency is shown in row 2 of Table 1. Specifically, parameter weights should have shape [num_classes,dim] where the size of the first dimension is the value of another parameter num_classes while dim is a non-negative integer.

These dependencies are denoted in a graph with each edge representing a constraint dependency. During input generation, the graph is traversed in a topological order to ensure dependencies are properly considered. The graph construction is straightforward and hence elided.

5. Testing Phase

To demonstrate the effectiveness of the extracted constraints, for each API function, DocTer iteratively generates an input i.e., values of the API function’s parameters, and evaluates that input to detect crashes. By either following or violating the extracted constraints, the input generator generates conforming inputs (CIs) or violating inputs (VIs), respectively. The conforming inputs are designed to test the core functionality of the API function while the violating inputs aim to test the API function’s input validity checking code. In both cases, DocTer reports bug-triggering inputs that cause serious crashes (e.g., segmentation fault). DocTer tests each API function with maxIter number of inputs, and the ratio of inputs allocated to each mode (CI or VI) is determined by the ratio conform_ratio.

Input generator: In each iteration, DocTer generates values for all required parameters and some optional parameters (for testing more diverse code). The probability for generating each optional parameter is optional_ratio.

The input generator generates one input for each iteration. Given the extracted constraints, DocTer generates a value for each parameter following the order determined by constraint dependencies (Section 2.4). For a conforming input, all generated arguments satisfy the extracted constraints for structure, dtype, shape, and valid value. If concrete values are specified (e.g., enumerated values) in the constraints, the input generator chooses from those values. Otherwise, it chooses a dtype from the list of dtypes specified in the constraints and creates a shape following the constraints. If the constraints do not specify valid dtypes, DocTer selects one from a default list of dtype described in Section 2.2. While the input generator is choosing dtype and shape for a parameter, it ensures they are generated according to the parameter dependencies, if any. For example, parameters often have matching dimension(s), so the input generator needs to ensure such shape consistency.

Once the dtype and shape are determined, the input generator generates an n-dimensional array with values satisfying the given dtype, shape, and the range as specified in the constraints, if any. Finally, the structure constraints are checked and satisfied. For example, if the generated value is 1-dimensional and the constraints explicitly specify the structure (e.g., tuple or list) for the parameter, the input generator converts the generated value accordingly.

To generate an invalid input, the input generator randomly selects one parameter and generates a value that violates one or multiple relevant constraints of that parameter. For all other parameters, DocTer generates their values in the same way as conforming inputs (i.e., conforming to all constraints).

Constraint-guided boundary-input generation: Boundary input values (e.g., 0 and None) tend to cause bugs due to off-by-one errors etc. (Guo et al., 2020; Dutta et al., 2018). Thanks to the extracted constraints, DocTer generates boundary values that follow the constraints and boundary values that violate the constraints. For each API, DocTer picks one parameter with the probability of mutation_p to be mutated to one of the boundary cases. We consider six types of boundary mutators: one constraint-specific (boundary values of constraints) and five generics (None, zero, zero dimension, empty list, and empty string). As an example, the mutator “zero dimension” sets the size of one of the dimensions of the parameter’s shape to 0 (e.g., it mutates a 3-D tensor of shape to ).

Test case evaluator: The test case evaluator invokes the target function with the generated input. If a severe failure occurs, DocTer reports the input as a bug-triggering input. Specifically, DocTer returns those inputs causing a segmentation fault, floating-point exception, abort, and bus error in the C++ backend. We exclude aborts caused by assertion failures in MXNet since MXNet uses those for exceptions. Crashes from the C++ backend (which handles computationally-intensive DL tasks) indicate severe problems.

Experimental Setup

Data collection: We choose three popular DL libraries (TensorFlow 2.1.0, PyTorch 1.5.0, and MXNet 1.6.0) as test subjects. There are 144,541–854,900 words in the collected API documents. Among them, we consider 1008, 529, and 1021 relevant APIs for the three respective libraries. An API is irrelevant if it (1) is deprecated, (2) has no input argument, (3) cannot be parsed due to HTML syntactic errors and typos, (4) is a non-layer class constructor, or (5) has an API document without a “Parameter” description section. In total, 2,666 APIs are filtered out due to the five reasons above, 53.5% of which are due to reason (1). We list the break down in (sup, 2022).

Rule construction and constraints extraction: DocTer applies three thresholds MAX_SIZE, min_support, and min_confidence to construct extraction rules (Section 2.3). We set MAX_SIZE to 7 to all three libraries. To select the best value for min_support and min_confidence, we conducted 5-fold cross-validation on the 30% annotated data and measure the quality of the extracted constraints. By selecting the best F1 score, we set min_support to 10, 10, 20, and min_confidence to 0.9, 0.7, 0.9 for TensorFlow, PyTorch, and MXNet, respectively.

Input generation and testing: We use Docker with Ubuntu 18.04, TensorFlow 2.1.0, PyTorch 1.5.0, and MXNet 1.6.0. For multi-dimensional arrays, DocTer generates shapes of 0–5 dimensions or as specified by the constraints. By trying different values of optional_ratio and mutation_p on 10% randomly sampled APIs, we choose optional_ratio=0.2 and mutation_p=0.4.

Manual and execution time: The AC annotation (Section 2.2) takes 36 manual hours. DocTer takes 34 minutes to perform rule construction and constraints extraction for all libraries. On average, it takes DocTer 0.14 seconds to generate and test each input.

Evaluation and Results

We answer four research questions (RQs): RQ1: How effective is DocTer in extracting constraints from DL API documentation? (Section 4.1) RQ2: How is DocTer compared to existing constraint-extraction approaches? (Section 4.2) RQ3: Can the extracted constraints enable DocTer to detect more bugs? (Section 4.3) and RQ4: How effective is DocTer in generating valid inputs? (Section 4.4)

Approach: We apply DocTer to extract constraints in our subjects and study the number and quality of constraints. We randomly sample an extra of 5% (603) of input parameters (excluding the 30% AC annotated data for rule construction) to form an evaluation set. We manually annotate these parameters with concrete constraints to build the ground truth. The constraints extracted by DocTer are then compared against the evaluation set.

For each parameter, we consider all valid options for one category as one constraint. And the constraint for this category is correct iff all valid options are correctly extracted. For example, the parameter size of tf.slice can be either int32 or int64. The extracted dtype constraint pT∈{int32,int64}p_{T}\in\texttt{\small\{int32,int64\}} is deemed correct, while pT∈{int32}p_{T}\in\texttt{\small\{int32\}} is considered incorrect. If a parameter’s document contains no constraints of the four categories, it is excluded from the precision and recall computation. While it is reasonable to include such no-constraint parameters in our calculation because DocTer can trivially extract nothing, the accuracy may be inflated if there is a large portion of such parameters. Among the sampled parameters, the numbers of no-constraint parameters are 29 (15.3%), 10 (10.8%), and 91 (28.4%) for TensorFlow, PyTorch, and MXNet, respectively (details in Extraction result section below).

We use the standard metrics precision, recall, and F1 score of the extracted constraints of the sampled parameters for each constraint category. Precision is the percentage of the correctly extracted constraints (i.e., extracted constraints that match the ground-truth) over the number of all extracted constraints. Recall is the percentage of correctly extracted constraints over the total number of all ground truth constraints. F1 is the harmonic mean of precision and recall.

Extraction results: Table 2 shows the quality of extracted constraints. In total, DocTer extracts 16,035 constraints automatically from the three libraries (row #constr. extracted). Specifically, TreeMiner (Zaki, 2005) collects 873, 426, and 321 frequent parse subtrees from the three libraries respectively with the corresponding min_support. Then DocTer constructs rules with 665, 398, and 275 subtrees. The remaining subtrees do not constitute any rules because no AC is associated with the subtree with large enough conditional probability. Using these rules, DocTer extracts on average 6.6 constraints per API for all three libraries (row #constr. per API: Avg (Min-Max) Table 2). Overall, DocTer can extract constraints from 90.4%, 94.1%, and 98.5% of relevant APIs (details in Section 3) for TensorFlow, PyTorch, and MXNet, respectively.

For each library, Table 2 shows the number of parameters in the evaluation set (row #evaluated param.), the number of the parameters in the evaluation set with at least one constraint (row #evaluated param. with constr.), the number of constraints manually labeled in the evaluation set (row #evaluated constr.). The Total/Avg column shows the total number of parameters and constraints in the evaluation set, and the average precision, recall, and F1 score.

Overall, DocTer achieves a high precision (85.4%) and recall (78.2%) of constraint extraction across all three subjects. DocTer is quite effective in extracting constraints for dtype and structure with F1 score over 80%. It is less effective when extracting constraints for valid value. The reason is that sentences that describe constraints for valid value are not as common in the annotated data compared with other categories, e.g., structure, and thus DocTer misses some patterns given the min_support and min_confidence thresholds. For example, when analyzing the sentence “Only ‘zeros’ is supported for quantized convolution at the moment”, DocTer misses the valid value constraint p0∈{‘zeros’}p_{0}\in\{\texttt{\small`zeros'}\} because the pattern “Only … is/are supported” is not frequent enough in the annotated dataset and did not pass the set thresholds. In Fig. 1, DocTer misses the constraint that parameter ksize can also be a single integer due to the same reason. With the extracted incomplete constraints (i.e., ksize is a list of integers), DocTer still detects the bug. This confirms that to detect real-world bugs effectively, one does not need to have complete constraints (Wong et al., 2015). We choose the thresholds as a trade-off between precision and recall, and one can choose lower thresholds for a better recall.

To show the impact of our rule extraction design (Section 2.3), we conduct an ablation study, where we do not use the conditional probabilities (Eq. 2) when constructing rules (the set RR) and instead set the min_confidence to 0 (instead of the settings in Section 3). Under these settings, any frequent subtree in trees(Df)trees(D_{f}) and any AC in AfA_{f} that has more than one co-occurrence will be considered as a rule. As a result, this version of DocTer without the conditional probabilities extracts 1,621 imprecise rules and extracts constraints with an F-1 of 27.9% on the same evaluation set (Table 2). In contrast, our DocTer, with the conditional probabilities, constructs 1,338 rules (Table 3) and extracts constraints with a much higher F-1 of 81.6% (Table 2) on the same data.

Overall, DocTer extracts tens of thousands of correct constraints for these libraries, which enables the generation of valid inputs for detecting 94 bugs. We show the breakdown of the number of rules and constraints extracted for all three libraries in Table 3. Note that a subtree can be mapped to multiple categories of ACs.

Sensitivity study: Since one can choose to annotate fewer parameters to save manual effort at the cost of a reduced F-1 score, we quantify the trade-off between the effectiveness of our approach (measured by F-1) and manual effort, which is measured by the amount of parameters to annotate. Specifically, we evaluate the F-1 scores of our approach by using different amounts of the annotated data, i.e., 5%, 10%, and 30% of parameters. The 5% of parameters are a subset of the 10% of the parameters, which is a subset of the 30% of the parameters. DocTer achieves an overall F-1 score of 66.0% with just 514 annotated parameters (5% of the parameters), 73.9% with 1,028 annotated parameters (10% of parameters), and 81.6% with 3,086 annotated parameters (30% of the parameters).

Generality of rules: To evaluate the generality across libraries, we apply the rules constructed from TensorFlow and MxNet to the documentation of PyTorch, and get the constraints with precision, recall, and F1 of 87.9%, 70.3%, and 78.1%. In addition, with the rules DocTer constructed from all three libraries, we extract 2,312 constraints from 223 scikit-learn APIs’ documents. We manually inspect the extracted constraints on 5% (59) randomly sampled parameters of scikit-learn APIs. DocTer achieves a precision/recall/F1 of 71.3/66.1/68.6%. The results suggest that DocTer can be applied to new libraries completely automatically without requiring annotating any documents of the new libraries.

To evaluate the generality across versions of the same library, we apply the rules that DocTer constructed from TensorFlow v2.1.0, PyTorch v1.5.0, and MXNet v1.6.0 to six versions: two more recent versions from each library respectively (TensorFlow v2.2.0 and v2.3.0, PyTorch v1.6.0 and v1.7.0, and MXNet v1.7.0 and 1.8.0). DocTer extracts 59,936 constraints from 7,684 APIs containing 580,187 words. For evaluation, we randomly sample 603 parameters (same number of parameters as the evaluation in Table 2) that are either with updated descriptions or newly added to the more recent versions. The results show that the rules that DocTer constructed are general across versions and can extract constraints from other versions with a precision/recall/F1 of 81.9/77.7/79.7%.

2. RQ2: Comparison with Existing Approaches

We compare DocTer with grep and state-of-the-art constraint extraction approaches (e.g., Jdoctor (Blasi et al., 2018)).

Comparison with grep: While it may appear to be straightforward to use a grep-like technique (i.e., matching existing keywords in documents) to extract constraints, such a technique can only identify relevant API document sentences. DocTer, on the other hand, extracts concrete constraints automatically. The grep-like approach could assign a constraint to a match, e.g., if a sentence contains the keyword “integer”, the corresponding parameter would be assigned the constraint pT∈{int}p_{T}\in\{\texttt{\small int}\}. We implement this approach by searching in the documents for keywords of dtype constraints (e.g., “int” and “integer”) , and structure constraints (e.g., “list” and “tensor”). We manually collect such keywords in the API documents. This approach misses 47.8% of the constraints that DocTer extracts, i.e, all shape, all valid value, 33% of dtype, and 4% of structure constraints.

Comparison with existing constraint-extraction techniques: We compare DocTer with the state-of-the-art constraint-extraction techniques, including Jdocter (Blasi et al., 2018), DASE (Wong et al., 2015), Zhou et al. (Zhou et al., 2018), and Advance (Lv et al., 2020). We exclude C2S (Zhai et al., 2020) because it requires formal specifications (JML (jml, 1999)), which is unavailable for the three libraries. We exclude Pytype (pyt, 2016) because it cannot analyze across Python and C++ code, therefore, cannot precisely infer types for DL libraries. Jdocter (Blasi et al., 2018), DASE (Wong et al., 2015), and Advance (Lv et al., 2020) can only extract constraints for valid value (e.g., range). With the assumption that they can extract all valid value constraints correctly, the best (upper bound) recall that these tools can achieve is 11.9%. Aside from valid value, Zhou et al. (Zhou et al., 2018) is able to extract specifications for type restrictions. However, we found that their heuristics that can be applied to DL document, e.g., “[something] be not [SpecClassName]”, extract at most 28 constraints (0.2% of DocTer constraints). This results in their best recall of 12.3%, while DocTer has a recall of 78.2%. Overall, shape constraints are DL-specific that DocTer extracts while existing techniques do not consider. The results show that DocTer complements existing constraint-extraction techniques by extracting DL-specific constraints automatically.

3. RQ3: Bug Detection Results

Approach: We demonstrate the effectiveness of DocTer’s constraint extraction using the constraints to guide input generation to detect bugs in API documents and library code. For documentation bugs, DocTer detects inconsistencies within API documents when extracting constraints, which will be discussed later in this section. For library code bugs, we use all 16,035 (Table 2) constraints extracted by DocTer to generate inputs for API functions that have at least one extracted constraint. Table 2 shows the numbers of these API functions (row #APIs with extracted constr.). We set maxIter to 2,000. For each API function, DocTer generates 2,0002,000 test inputs (1,000 conforming and 1,000 violating inputs), evaluates them, and returns bug-triggering inputs that cause serious failures (details in Section 2.5). We manually examine those bug-triggering inputs to check if they reveal real bugs. For those inputs that still trigger the same failures in the nightly version, we report the bugs to the developers.

We implement an unguided input generation tool as the baseline. The only difference between DocTer and the baseline is that the baseline has no knowledge of constraints. Specifically, the baseline generates 2,000 random inputs for parameters without any constraint knowledge. For a fair comparison, we convert the generated array inputs to tensors assuming that the baseline minimally knows which input arguments should be tensors. Without this conversion, non-tensor input arguments are trivially rejected by PyTorch and MXNet, thus very ineffective in exercising the code in depth.

The extracted constraints can be used together with other input generation tools to improve their testing effectiveness. In this paper, we choose to implement our own baseline instead of using existing fuzzers (afl, 2013; Swiecki, 2015; lib, 2015) such as AFL (afl, 2013) for practical reasons. These fuzzers cannot test Python code: the most popular language for DL. Moreover, these fuzzers require code coverage, which is currently unavailable across Python and C++. Instead of code coverage, DocTer uses constraints extracted from documents to guide the testing of both Python and C++ code, by generating inputs for the Python API functions, in which C++ code is invoked. In addition, existing fuzzers (afl, 2013; Swiecki, 2015; lib, 2015) generate inputs in the format of a sequence of byte arrays. Randomly mutating some bytes is unlikely to generate valid DL-specific inputs. Our baseline is similar to AFL with two enhancements: (1) knowledge of tensors and (2) automatically testing Python and C++ code.

Bugs in libraries code: Table 4 presents the number of verified new bugs, new bugs, all bugs, and buggy APIs (in “()”) found by the baseline and DocTer. A bug is verified if it has been fixed or confirmed by the developers. A new bug refers to a previously unknown bug that we reported. The unverified new bugs are reproducible and waiting for developers’ responses. We count one bug for each required fixing location.

DocTer detects 94 bugs, including 63 previously unknown bugs, 54 of which have been verified by the developers (49 fixed and 5 confirmed). Of the 49 fixed bugs, 19 are fixed in C++, 11 are fixed in Python, 7 are fixed in both, and 12 is fixed silently after we reported. The 94 bugs cause 174 APIs to fail because one bug can cause failures in multiple APIs but are fixed in one location. We count them as 94 instead of 174 bugs. Of the 174 buggy APIs, 12 have parameters with constraint dependencies. DocTer has also detected 31 (94−6394-63) known bugs that have been fixed in the nightly versions.

The baseline detects only 59 bugs with 41 new bugs causing 108 APIs to crash. DocTer detects 52 bugs that the baseline cannot while missing 7 bugs found by the baseline due to the randomness of the input generation process.

False positives: Only 3 out of 66 newly reported bugs receive “won’t fix” responses from the developers. They claim such inputs are not supported, which is not stated in the document. We do not count these 3 bugs in our results.

DocTer uses the automatically extracted constraints without any manual examination. It is possible that documents themselves are incorrect or incomplete, causing incorrect constraints to be extracted, leading to false positives, where the code is correct, but the API documentation is incorrect. Since we focus on severe bugs such as crashes, all detected bugs are in library code bugs, as well said by a developer after we reported a crash bug “A segmentation fault is never OK and we should fix it with high priority.”

Conforming and violating inputs: DocTer generates both conforming inputs (CIs) and violating inputs (VIs). Rows “CI” and “VI” in Table 4 present the breakdown of the bug detection for CIs and VIs with conform_ratio = 50%. We find that DocTer is insensitive to conform_ratio. When it is between 20%–60% (with a 10% increment): the number of detected bugs differs by at most one. Thus, we use conform_ratio = 50% as the default ratio to be more general.

The results show that the CIs alone, with only 50% (1,000) of the number of test inputs of the baseline (2,000), finds more bugs (75 bugs) than the baseline (59 bugs), and the VIs alone (with 50% of the test inputs) finds more bugs (81 bugs) than the baseline. We manually verify the generated CIs and VIs: out of 75 CI bugs we found, 5757 of them are caused by valid inputs conforming to the ground truth constraints. The rest of the CI bugs are caused by invalid inputs generated by conforming to inaccurate constraints; out of 81 VI bugs we found, all of them are caused by invalid inputs violating the ground truth constraints.

Many bugs are detected by both CIs and VIs (comparing the “All” row with the “CI” and “VI” rows in Table 4) because DocTer violates the constraints of one parameter only when generating VIs. When a crash is caused by one of the conforming parameters of a VI, it is likely to be triggered by a CI also. However, both CIs and VIs trigger bugs in unique APIs, thus both are effective in finding bugs.

Without the constraints, a baseline is much worse than the results from any of the ratio setups. Table 4 shows that a key contribution of our work is the ability to extract constraints from documents. One cannot choose to focus on valid or invalid inputs without knowing the definition of valid inputs for an API. DocTer enables this choice since it extracts input constraints automatically.

Impact of fuzzer’s nondeterminism: Since the fuzzing process is non-deterministic, we investigate the impact of this nondeterminism to ensure the validity of our results, i.e., the reported improvement of DocTer is not due the randomness in the fuzzer. Our results suggest that it is statistically significant that DocTer (which is guided by constraints) outperforms a baseline fuzzer. Specifically, we repeat the fuzzing experiment (with the same set of extracted constraints) eight times. For each run, we generate 2,000 test inputs for the baseline and 2,000 test inputs for DocTer for all APIs from the three libraries. In each run, we use the same random seed for both the baseline and DocTer. Different runs use different seeds. Since it requires significant manual effort to inspect the detected bugs from all eight runs, which are 2,301 API crashes to examine, we use the number of buggy APIs (i.e., the number of APIs that crash) to indicate the fuzzers’ effectiveness. Overall, among the eight runs, DocTer on average detects 172.0 buggy APIs while the baseline on average detects 115.6 buggy APIs. We perform the Mann-Whitney U-test and confirm the improvement is statistically significant with a p-value of 0.0004 and the Cohen’s d effect size of 8.96 (effect size more than 2.0 is huge). The detailed results are in (sup, 2022).

Bugs in API documents: DocTer detects three types of documentation bugs: (1) formatting bugs (e.g., indentation issue); (2) signature-description mismatch (the description refers to parameters that are not specified in the API signature); and (3) unclear constraint dependency (Section 2.4). DocTer detects 43 previously unknown documentation bugs in 46 APIs (11 formatting bugs, 29 signature-description mismatches, 3 unclear constraint dependencies). Majority (39 of 43) are fixed or confirmed after we report, indicating that DocTer detects documentation bugs that developers care to fix.

Bug examples: We present three bugs detected by DocTer that the baseline fails to detect. All of them have been fixed by developers after we report them. Bug 3 is also reported as a security vulnerability CVE-2020-15265.

Bug 1: The previously unknown bug in TensorFlow tf.nn.max_pool3d discussed in the Introduction (Fig. 1).

Bug 2: In the API tf.image.combined_non_max_suppression, DocTer detects a previously-unknown bug and triggers a memory overflow by passing a large value of 311452676677046672 for the parameter max_total_size. To successfully trigger this bug, DocTer needs to generate correct shaped values for parameters boxes and scores. Specifically, the parameter boxes needs to be 4-D with the size of the last dimension equals to 4 while the parameter scores needs to be 3-D. DocTer also needs to follow the dependencies between those two parameters – the sizes of the first two dimensions of boxes and scores need to be the same. DocTer does this by extracting relevant shape constraints and the dependencies correctly from the API document. Without such knowledge, random input generation fails to produce valid input for boxes and scores to trigger this bug.

Bug 3: DocTer triggers a segmentation fault bug in the TensorFlow API tf.quantization.quantize_and_dequantize using an input tensor of any shape with an out-of-bound axis value (i.e., the value of axis is larger than the number of input dimensions). After we report the bug, TensorFlow developers report this as a security vulnerability CVE-2020-15265 to the national vulnerability database (NVD). The extracted constraints enable DocTer to trigger this bug by ensuring the generation of many valid input values for all parameters of this API other than the axis values. For example, the extracted constraints contain the boolean type for parameters narrow_range, range_given, and signed_input and the valid values (“HALF_TO_EVEN” and “HALF_UP”) for parameter round_mode, which help DocTer generate valid values for these parameters. In contrast, since the baseline is unaware of these constraints, the baseline generates invalid values for these parameters, which are rejected by TensorFlow’s input validation code, therefore avoiding exposing this security vulnerability.

4. RQ4: Valid-Input Generation Results

Approach: As discussed in the Introduction, generating valid inputs is essential to exercise the core functionality of the API function. While DocTer attempts to generate CIs, these CIs may still be invalid if the constraints extracted are incorrect or incomplete. We study the percentage of generated CIs that are valid inputs. We compute the ratio out of 1,000 CIs (confirming_ratio = 50%) with the first 1,000 baseline inputs for each API function. Since manually examining the validity of all inputs is impractical and the validity checking of mature projects (e.g., our subjects) is generally reliable, we make a reasonable approximation by counting the number of passing inputs whose executions terminate normally.

Results: Fig. 3 presents the ratio of passing inputs for each subject and the average. On average, DocTer achieves 33.4% ratio of passing inputs, which outperforms the baseline (21.5%) by generating 55.3% more passing inputs. The results suggest that DocTer is more effective in generating valid inputs than the baseline to detect more bugs. Although DocTer outperforms the baseline, the ratio of passing inputs is still low (33.4%), because API documents are often incomplete. DocTer might convince developers to write more complete documents since documents can help them find bugs.

Threats to Validity

Complex constraints: DocTer does not work with complex constraints that require a nested structure or indirect dependency with the constraints of another parameter. However, these complex constraints are uncommon in DL libraries (appeared in only 6.4% of our sampled parameters).

Testing Python and C++ code: Since DL libraries’ core computations are in C++, it may appear to be more reasonable to directly test C++ code. However, since Python APIs are the most popular for DL, testing them is testing the common use cases. DocTer tests Python APIs which invoke the computations in C++, so DocTer finds bugs in both Python (26 bugs) code and C++ (18 bugs) code.

Manual annotations: There is a one-time cost of up to 36 man hours of manually annotating 30% of parameters with their ACs (Section 2.2). Since the DocTer-generated rules are applicable to other libraries and versions (Section 4.1), this one-time cost is reasonable. Such manual annotation cost is widely accepted in other domains (e.g., supervised learning). Moreover, to minimize biases with the manual annotation, our process involves three co-authors. Two authors independently annotate with 98.2% agreement. All disagreements are resolved with a third author to reach a consensus.

Related Work

DocTer is the first technique to extract DL-specific constraints from API documentation, and the first DL library testing technique that is guided by such input-constraints.

Constraint extraction: Existing constraint-extraction techniques are insufficient for extracting DL-specific constraints (pyt, 2016; Blasi et al., 2018; Zhai et al., 2020; Wong et al., 2015; Lv et al., 2020; Tan et al., 2012; Zhou et al., 2018), because they miss most of the DL-specific constraints, cannot analyze across Python and C++ code, or requires formal specifications. Many existing techniques (Blasi et al., 2018; Wong et al., 2015; Tan et al., 2012; Wu et al., 2013; Goffi et al., 2016) use a handful of manually-designed rules to extract constraints. Instead, DocTer uses subtree matching to automatically construct rules to extract constraints.

Analyzing software text to detect bugs: Prior work leverages documents (Lv et al., 2020) and comments (Tan et al., 2007, 2011; Zhou et al., 2018; Tan et al., 2012) to detect inconsistency bugs between code and its specifications. Some prior work translates software specifications into assertions (Liu et al., 2014) and oracles (Goffi et al., 2016; Motwani and Brun, 2019). Different from these techniques, DocTer uses frequent subtree mining and association rule learning to extract constraints from API documents to guide input generation for testing DL libraries.

Testing DL libraries and fuzzing: The constraints extracted may be used to improve existing testing techniques. The DL library testing techniques focus on addressing the test oracle challenge, by using differential testing (Pham et al., 2019; Vanover et al., 2020; Wang et al., 2020; Srisakaokul et al., 2018; Dutta et al., 2018; Guo et al., 2020) or oracle approximation (Nejadgholi and Yang, 2019; Zheng et al., 2019). DocTer uses crashes instead and addresses the challenge of obtaining input constraints automatically.

Existing techniques are designed to detect specific types of bugs such as shape-related (e.g., tensor shape mismatch) (Dutta et al., 2018; Lagouvardos et al., 2020), numerical (Guo et al., 2020; Dutta et al., 2018) (e.g., returns NaN/Inf), decreased accuracy (Dutta et al., 2018), and performance (Tizpaz-Niari et al., 2020). On the other hand, DocTer finds general bugs that lead to serious crashes.

TensorFlow developers use OSS-Fuzz (oss, 2016) along with libFuzzer (lib, 2015) to test only 1919 TensorFlow’s C++ API functions. It requires developers to manually encode test inputs from the byte-arrays returned by libFuzzer. This would take a prohibitive amount of manual effort to test the 2,415 APIs that DocTer tests.

Fuzzers (afl, 2013; Fuz, 2019; lib, 2015) have been adopted to test non-DL libraries (Lemieux and Sen, 2018; Peng et al., 2018). They would not work well for DL libraries (Section 4.3). Since Randoop (Pacheco and Ernst, 2007) works only for a statically-typed language (e.g., Java), it would fail to create valid dynamically-typed objects for Python (the most popular language for DL (pop, 2017)).

Testing DL models: Many fuzzing techniques test the robustness of DL models instead of DL libraries (Odena et al., 2019; Guo et al., 2018; Xie et al., 2019; Udeshi and Chattopadhyay, 2019; Wang et al., 2019; Hu et al., 2019; Gao et al., 2020). DocTer tests DL libraries since testing DL models alone is insufficient as DL libraries contain bugs (Islam et al., 2019; Humbatova et al., 2020; Pham et al., 2019).

Conclusion

We propose DocTer, which features a novel method to derive general rules to translate API documents to precise parameter constraints. We apply these rules to popular DL libraries to extract a large number of DL-specific constraints. We use the constraints to guide the input generation of DL API functions. The constraints enable DocTer to generate valid and invalid inputs to detect more bugs in code and documents.

Data-Availability Statement

We release our artifacts at 10.1145/3462323.

Acknowledgement

The authors thank the anonymous reviewers for their invaluable feedback. The research is partially supported by NSF 1901242 award.

References