Keywords

figure a
figure b

1 Introduction

To meet the continuously growing demands on software performance, parallelism is increasingly often needed [13]. However, introducing parallelism tends to increase the risk of introducing errors, as the interactions between parallel computations can be hard to predict. Moreover, a plethora of optimisation techniques exists [10], so identifying when an optimisation can be applied safely, without breaking correctness, can be very challenging. Also, applying optimisations tends to make a program more complex, making it harder to reason about.

To address this, on the one hand, various domain-specific languages (DSLs) have been proposed that separate the algorithm (what it does) from the parallelisation schedule (how it does it). These are called scheduling languages [3, 6,7,8, 22, 23, 28]. Given an algorithm and a schedule, a compiler generates an optimised parallel program. This approach crucially depends on the schedule not introducing any errors in the functionality, which is not always obvious.

On the other hand, deductive program verification [9] has been successfully applied to verify the functionality of parallel programs [4]. This requires that the intended functionality is formalised as a contract, for instance using permission-based separation logic [1, 5]. A major hurdle, preventing this technique from being adopted at a large scale, is that if a program becomes more complicated, the required annotations rapidly grow in size and complexity [25, 26].

Fig. 1.
figure 1

High level overview of our approach.

In this paper, we combine the best of both worlds. We propose the HaliVer tool, which focusses on Halide  [22, 23], a scheduling language for portable image computations and array processing. It has been widely adopted in industry, for instance to produce parts of Adobe Photoshop and to implement the YouTube video-ingestion pipeline. For verification, we use the VerCors program verifier [4]. In this paper we define two verification approaches (1) front-end and (2) back-end, as seen in Figure 1. Our approaches verify that the program adheres to the same functional specification. This specification is detailed by annotating the algorithmic part of a Halide program, thereby keeping the annotations focussed on the functionality, and therefore relatively straightforward. With the front-end verification approach we verify the correctness of the algorithmic part of a Halide program. HaliVer transforms the algorithm and the annotations to an annotated VerCors program. With back-end verification approach we verify the C code that the Halide compiler generates, given a Halide algorithm and a schedule. HaliVer transforms the given annotations to match the generated code. Furthermore, where possible, HaliVer generates annotations, such as permission specifications, to relieve the user from having to manually write these. This contributes to making the annotation process straightforward.

In this way, HaliVer allows the user to succinctly specify the intended functionality of optimised, parallel code, and it checks that the resulting program indeed has the desired functionality. A major advantage of our approach is that it is flexible to use in a setting where multiple compiler passes are made. Also, it can be easily extended if a new compiler pass or schedule optimisation is added. An alternative would be to prove correctness of the compiler, but this would require a large amount of initial work and additionally for each change to the compiler.

Concretely, this paper provides the following contributions:

  • An annotation language to describe the functionality of Halide algorithms, which is integrated into the Halide algorithm language;

  • Tool support for the front-end verification approach of Halide algorithms;

  • Tool support for the back-end verification approach, which can verify programs generated by the Halide compiler from an algorithm and a schedule;

  • Evaluation of the HaliVer tool on Halide examples using all but one of the essential scheduling directives available in various combinations. We evaluated the tool on 23 different optimised programs, generated from eight characteristic Halide algorithms, to prove memory safety with no annotation effort. For 21 cases, HaliVer proves safety, for the remaining two cases we discuss the limitations. For 20 programs, based on five algorithms, we also add annotations for functional correctness properties. For 19 of these programs HaliVer proves correctness, for the remaining one we run into a similar limitation.

The remainder of this paper is organised as follows. Section 2 gives brief background information on Halide and VerCors. Section 3 introduces Halide annotations, and describes how HaliVer supports the verification of an algorithm and an optimised program. The approach is illustrated on characteristic examples. Section 4 evaluates the HaliVer tool, and Sections 5 and 6 address related work, conclusions and future work.

2 Background

\({{\boldsymbol{halide.}}}\) Halide is a DSL embedded in , targeting image processing pipelines and array computations [22, 23].Footnote 1 Halide separates the algorithm, defining what you want to calculate, from the schedule, defining how the calculation should be performed. Typically, when optimising code for a specific architecture, the code becomes much more complex and loses portability. By separating the schedule, the code expressing the functionality is not altered.

figure d

Listing 1 presents the Halide algorithm for a box filter, or blur function. The reader can ignore the and annotations for now. Images are represented as pure (side-effect free) functions that point-wise map coordinates to values. A blur function defines how every pixel, referred to by its two-dimensional coordinates, should be updated. In the example, the coordinates are represented by the variables and . Halide uses a functional style, allowing algorithms to be compact and loop-free. Halide functions are denoted by the keyword . In the example, the input image is stored in a two-dimensional integer buffer , and the output is given by defining the function , a reference to which is a parameter of . A pipeline of function calls is defined: the function is applied on the input image (line 5).The output of that function is used to compute the final image with the function (line 7). With and we refer to the minimum and maximum value of the dimension , respectively.

A function may involve update definitions, which (partially) update the value of a function. A reduction domain is a way to apply an update a finite number of times and is typically used to express sums or histograms in Halide. A function is called a reduction when such a domain is used, and an initialisation and an update definition are given. Listing 2 presents a reduction example. For now, ignore the and lines. The reduction domain () ranges from to , i.e. it consists of values. The initial value of the function is defined at line 3, and line 5 is executed once for every value in . The statement returns if evaluates to true, otherwise. For a given matrix of integers , counts the number of non-zeros at the first ten positions of each row in .

A Halide schedule is given in Listing 5 and further explained in Section 3.3.

figure ah

VerCors. VerCorsFootnote 2 [4] is a deductive verifier to verify the functional correctness of, possibly concurrent, software. Its specification language uses permission-based separation logic [5], a combination of first-order logic and read/write permissions. The latter are used for concurrency-related verification, to express which data can be accessed by a thread at which moment. Programs written in a number of languages, such as Java and C, can be verified. VerCors also has its own language, Pvl. VerCors ’s verification engine relies on Viper  [16], which applies symbolic execution to analyse programs with persistent mutable state.

Intended functional behaviour can be specified by means of pre- and postconditions, indicated by the keywords and , respectively. The statement is an abbreviation for . Loop invariants and assertions can be added to the code to help VerCors in proving the pre- and postconditions. We refer to the pre- and postconditions, loop invariants and assertions together as the annotations of a code fragment. A permission gives permission to memory location , where is a fractional, with indicating a write and anything between and a read. For a statement s, we have the Hoare triple \(\big \{ P \big \}s\big \{ Q \big \}\). This indicates that if P holds in the pre-state then after s, Q holds in the post-state. A pure function is without side-effects, thus can be used in annotations. It has the keyword in the header, and its body is a single expression. Annotations and pure function definitions in C files are given in special comments, like for multi-line comments. (See Listing 6 for examples.)

VerCors can prove termination of recursive functions. Whenever the clause is added to a function contract, VerCors will try to prove that the function terminates, by showing that all recursive calls will strictly decrease the value of while has a lower bound.

3 Verification of Scheduling Languages with HaliVer

HaliVer works directly on a Halide program and its intermediate representations, adding and transforming annotations where necessary. The tool is embedded in the Halide compiler. From a user’s point of view, the general approach is as follows, using the front-end and back-end approach as in Figure 1.

  1. 1.

    Write a Halide algorithm and add annotations. Annotations are the functional specification of the Halide algorithm. Since a user can write an incorrect Halide algorithm, its correctness is ideally checked against a user-supplied specification.

  2. 2.

    The front-end approach produces a Pvl encoding. This encoding contains the algorithm and the specified annotations.

  3. 3.

    VerCors verifies the encoding. If verification succeeds, we know that the front-end algorithm conforms to the functional specification. Otherwise, the verification fails; VerCors produces a counterexample and we return to step 1.

  4. 4.

    Write a Halide schedule.

  5. 5.

    The back-end approach produces an annotated C file. The tool automatically generates permission annotations. These allow us to prove data-race freedom and the absence of out-of-bound errors. The tool transforms the annotations and generates additional annotations to match the scheduled back-end code. This is highly non-trivial, as each for-loop requires precise annotations to guide VerCors in the verification. However, it is ensured that the same property is verified.

  6. 6.

    VerCors verifies the back-end C file. If the verification fails, the lines of C code that caused the failure are given, which can be traced back to the Halide algorithm. The cause of a verification failure may be that

    • The Halide compiler produced incorrect code w.r.t. the specifications.

    • More auxiliary annotations from step 1 are needed to guide VerCors.

    • A limitation has been encounter of the tools HaliVer relies on, e.g., VerCors or the underlying SMT solver.

In the remainder of this section we explain how to write annotations, and address front-end and back-end verification approaches. We also discuss the soundness and current limitations of the technique.

3.1 Halide Annotations

HaliVer makes it possible to add annotations when writing a Halide algorithm. Intuitively, these annotations are added as a Hoare triple. We consider three types of annotation: pipeline, intermediate and reduction invariant annotations.

In Listing 1 annotations have been added. The lines 1–2 are pipeline annotations: they specify the pre- and postconditions of the whole function and can only contain references to input buffers or output functions. Note that the results are stored directly in the function. Line 1 specifies how the input and output bounds should be related. Line 2 indicates what the output values are. One can add intermediate annotations after any (update) function call to specify state predicates for particular locations in the pipeline. Examples are the and state predicates of Listing 1 (lines 6 and 8).

Halide functions map coordinates to values pointwise. To achieve a one-to-one relationship between function and annotations, the intermediate annotations for a function should also specify how coordinates relate to values pointwise. However, input buffers can be used freely with any point. For example, is valid, but is not, because the latter refers to as opposed to . HaliVer requires this because each point of the function may be computed in parallel in the back-end, so it must be possible to reason about the points individually.

For ease of annotation, HaliVer automatically generates a pipeline postcondition. This postcondition is derived from the intermediate annotation of the last pipeline function in the algorithm. For Listing 1, HaliVer can generate line 2, which is included here for completeness, based on line 8.

To prove that a reduction is correct, reduction invariant annotations must be provided for reduction domains. In Listing 2, an example is given of a reduction (line 5) together with its reduction invariant (line 6) and post-state predicate (line 7). Intuitively, a reduction invariant is similar to a loop invariant. First, it must hold before the reduction starts. In our example this means that has the value , which is ensured by the previous definition of (line 4). Second, it must be preserved by each step of the reduction. In our example, is bounded by the reduction variable. Finally, after each reduction variable has reached its maximum value, the reduction invariant should imply the post-state predicate of the function. For the example, note that the invariant implies the post-state predicate when has reached the value . The actual used value goes to , and indicates that the reduction is done.

figure bm

3.2 Front-end Verification Approach

For verifying the algorithm part of a Halide program, an annotated Halide algorithm is encoded into annotated Pvl code. Listings 3 and 4 show how HaliVer translates the examples of Listings 1 and 2, respectively. Input buffers are translated into abstract functions to verify the pipeline w.r.t. arbitrary input. The bounds of input buffers and functions are modelled via functions that are abstract if the bound is unknown or otherwise return a concrete value. For example, the buffer of the blur example is translated to a function in Listing 3 (line 1), with its bounds represented by the pure functions on line 2.

Update-free Halide functions are translated directly into Pvl functions, and post-state predicates are translated into postconditions of these functions. In the example, and are translated to the functions on lines 6–7 and 9–12 of Listing 3, respectively, and the lines express the postconditions of those functions, using to refer to the expected result.

The pre- and postconditions of a Halide algorithm are translated into a Pvl lemma to be checked by VerCors. In the example, lines 14–19 of Listing 3 address the pre- and postconditions on lines 1–2 of Listing 1. On line 20, a method called is given, which represents the Halide pipeline.

For an update definition, references to itself are replaced by references to the previous definition, thus the output of one definition is the input of the next.

For a reduction, the initialisation and update parts are translated into separate functions, and reduction domain variables are explicitly added as function parameters. Listing 4 illustrates this for the example. The function on line 8 corresponds to the initialisation (line 3 of Listing 2), with the translated post-state predicate on line 6. The function (lines 13–14) corresponds to the update function (line 5 of Listing 2). Note that the annotation on line 10 refers to the reduction domain. The reason for using references to on line 14 is that the result of the whole computation corresponds to with its maximum value (see line 18). This is computed by recursively decrementing . The invariant on line 6 of Listing 2 is translated into the postcondition of (line 11), reflecting that the invariant should hold after each reduction iteration. For the annotation added on line 12, VerCors will try to prove that this recursive function terminates. The reduction postcondition is represented by the annotation on line 16.

Guarantees. For the front-end verification approach, HaliVer straightforwardly encodes a Halide function without reductions, as it defines the function pointwise in Pvl. For reductions, HaliVer mimics the iterative updates with recursion, as shown in the example of Listings 2 and 4. HaliVer adds clauses to check that the recursive functions terminate.

With HaliVer ’s approach, functional correctness of the algorithm part can be proven. Since memory safety depends on how a Halide algorithm is compiled into actual code according to a schedule, this is checked using the back-end verification approach.

figure ch

3.3 Back-end Verification Approach

For verifying a Halide algorithm with a schedule, HaliVer adds annotations to the generated C code that can be checked by VerCors. First, HaliVer generates read and write permissions and preconditions for functions used in definitions. This generation of permissions makes it possible to keep the annotations of Halide algorithms concise, since the user does not have to specify permissions. Second, HaliVer transforms the annotations and adds them to the intermediate representation used by the Halide compiler. Finally, HaliVer adds the annotations to the code, during the code generation of the Halide compiler.

figure ci

Annotation Generation. Since Halide algorithms consist of pure point-wise functions, permissions are relatively straightforward: for a function , HaliVer generates the write permission . For the blur example from Listing 1, HaliVer generates and for function and , respectively.

For update functions and reductions, HaliVer generates (1) read permissions for function values that are not being updated, and (2) a pre-state predicate, using the post-state predicate of the previous update step.

Once a function is fully defined, read permission is given to all values wherever the function is used, along with a context predicate containing any intermediate annotations of the function.

Transformation of Annotations. Next, HaliVer transforms the annotations according to the schedule given by the user and associates them with the corresponding parts of the optimised Halide program expressed in Halide ’s intermediate language.

HaliVer supports the , , , , , and scheduling directives. Of the most commonly used directives in the Halide example appsFootnote 3, only is not supported because VerCors does not yet support verification of vectorised code as produced by Halide.Footnote 4 With these directives, HaliVer provides the means to verify optimised programs w.r.t. memory locality, parallelism and recomputation. This is the optimisation space in which Halide resides [22]. We illustrate the meaning of these directives with an example. Listing 5 shows a schedule for blur on lines 1–2, and below that the loop nest structure of the resulting program. Loop nests are program statements of nested for loops. The loops can be sequentially executed or be parallelized, unrolled or vectorized. The allocation of space for a function result is indicated by , and and refer to writing and reading function results, respectively. This loop nesting corresponds to the actual code produced by the Halide compiler.

Assuming that the output dimensions in the example are both of size 1,024, the directive (line 1 of Listing 5) splits the dimension into two nested dimensions (line 5) and (line 7) of sizes and , respectively. HaliVer similarly renames references to in annotations. The directive (line 1) expresses that should be executed in parallel (line 5). The directive (line 2) expresses that must be stored at the start of the loop (line 6). The directive (line 2) defines that the values for should be produced at (line 8). The directive (line 1 and 2) expresses that the dimension should be completely unrolled.

The loops are sequential. In this example, and are not used; they express that two dimensions should be fused into one and the nesting order of the loops should be changed, respectively.

HaliVer moves bottom-up through the program, constructing loop invariants for each loop by taking the constructed state predicates from the loop body and extending them with quantifications over the loop variables. Below, we give an example of this exact process for the blur example of Listing 1.Footnote 5

figure du

Encoding of Halide Program. Finally, HaliVer adds annotations to the C code during the code generation of the Halide compiler. As an example, we show how HaliVer adds annotations of the function of Listing 1 with the schedule of Listing 5. The result of this can be found in Listing 6. It shows the structure of the whole program, but is focussed on the code below the node (line 13 of Listing 5).

First, HaliVer updates its pipeline annotations (lines 1–2 of Listing 1), to match the flattened array structure the Halide back-end uses, and adds them to the function contract (lines 8–15 of Listing 6). HaliVer also uses the Halide definition of division (), i.e., EuclideanFootnote 6 [12] with \(x/0 \equiv 0\).

Next, HaliVer transforms the annotations added to the function, before it adds them to any loop nest. The Halide compiler flattens the two-dimensional function into a one-dimensional array , so HaliVer does the same for all function references in the annotations. Next, from the schedule, the directive splits into and of sizes and , respectively. A similar split is performed for . The generated annotation becomes .

For the annotation , HaliVer replaces the calls to with calls to an abstract pure function . This is done because quantification instantiation in VerCors can become unstable if is used frequently. Where is used in the code, HaliVer adds annotations stating that and have the same value (line 7 of Listing 6).

HaliVer adds these annotations to the first loop nest, starting bottom up. In Listing 5, this is , but since this loop is unrolled, additional annotations are not needed. After passing this loop nest, anything for and now holds. HaliVer changes the annotations by quantifying over ’s domain. It uses as variable and changes any references to towards . The resulting permissions are . The other annotations are processed in a similar way.

Next, HaliVer arrives at the loop nest for , which needs loop invariants. First, the tool adds the bounds of the dimension (line 33 of Listing 6). The annotation is transformed depending on whether it was a , or annotation. The write permission (), should hold before the loop starts and after the loop ends. Therefore, HaliVer adds the permission, but quantifies over dimension , which results in a loop invariant (lines 38–39 of Listing 6). The annotation does not hold at the start of the loop, but after each iteration of the loop, one more value for holds. Therefore, HaliVer quantifies over bounded by zero and the iteration variable , and replaces occurrences to with , which leads to a loop invariant (lines 40–43 of Listing 6). For loops above this loop nest, the annotations hold for the whole domain of , resulting in . This annotation is added to the parallel for loop.

After constructing the node for , the node for is constructed in a similar way. The bound inferencer of Halide detects it only needs to calculate for values of up to . The annotations are transformed respecting that fact. After the node, the is consumed (line 30 of Listing 6). So for each loop below the statement, HaliVer adds read permission (lines 34–35 of Listing 6s) and the post-state predicate of (lines 36-37 of Listing 6) as context annotations. For the loop of , this means they are valid for any value of .

Guarantees. With the back-end verification approach, HaliVer can prove that the optimised code produced by the Halide compiler is correct w.r.t. specifications. Memory safety is proven without any additional effort, as the permission annotations for this are generated automatically. For functional correctness, a specification needs to be provided. For any non-inlined function, an intermediate annotation is required to guide VerCors in correct functional verification.

The approach is sound, but not necessarily complete. One concern is that, since we have not formally proved the correctness of the transformation, our implementation could in principle be wrong. HaliVer addresses this by keeping the pipeline annotations very close to what the user has written as annotations. These pipeline annotations act as the formal contract that will be verified, and the user can inspect these at any time. If an intermediate annotation is not correctly transformed, the verification will fail, thus remaining sound but not complete. Of course we have not constructed any transformations to be wrong, but even if there is an oversight, we will remain sound. Moreover, in Section 4, we show that our approach works for real world examples.

Table 1. Number of lines of code and annotations for different Halide algorithms, schedules and resulting programs, and the verification times required by VerCors to prove memory safety, given that no annotations are provided by the user. The letters after each schedule denote the used directives: compute_at, fuse, parallel, reorder, split, store_at and unroll. F stands for verification failed. Times with \(^{\dag }\) are inconsistent, i.e. they are succesfully verified, but can also sometimes fail or timeout.
Table 2. Number of lines of code and annotations for different Halide algorithms, schedules and resulting programs, and the verification times required by VerCors.

4 Evaluation

The goal of the evaluation of HaliVer is four-fold. (1) We evaluate that the front-end verification approach of HaliVer can verify functional correctness properties for a representative set of Halide algorithms. (2) For the back-end verification approach, the annotations that HaliVer generates and transforms should lead to successful verification for a representative set of Halide programs, with schedules that use the most important scheduling directives in different combinations. (3) We evaluate the verification speed for front-end and back-end verification. (4) Lastly, we evaluate how many annotations are needed in HaliVer compared to manually annotating the generated C programs.Footnote 7

Set-up. We used a machine with an 11th Gen Intel(R) Core(TM) i7-11800H @ 2.30GHz with 32GB running Ubuntu 23.04.

We used eight characteristic programs from the Halide repository.Footnote 8 These are representative Halide algorithm examples. They cover all scheduling directives supported by HaliVer, in commonly-used combinations. We removed any scheduling directives that we do not support. VerCors is unable to deal with large dimensions that are unrolled, thus we removed some directives as well.Footnote 9

The original schedule, as found in the Halide repository, is indicated with V3 if there are multiple schedules present. For five of these programs we defined annotations that express functional properties. These five programs are also evaluated with the standard schedule (V0), which tries to inline functions as much as possible, and two additional schedules (V1 and V2) we constructed.

Memory Safety Results. We evaluate 8 Halide programs, with in total 23 schedules, and prove data race freedom and memory safety for 21 of them. No user provided annotations are needed. The results can be found in Table 1.

For each case, we provide: the number of lines of code (LoC)Footnote 10 for the Halide algorithm, without the schedule and number of scheduling directives (Sched. Dir.). For the generated programs (C) we list: lines of code (LoC), lines of annotations (LoA.), number of (parallel) loops (Loops). These numbers indicate how large programs tend to become w.r.t. Halide algorithms, and how much annotation effort would be required to manually annotate the programs. Verification running times (T. (s)) are given in seconds, averaged over five runs.

For camera_pipe, VerCors gives a verification failure. It could not prove a , but after simplifying parts of the generated C program not related to this specific invariant, it leads to a successful verification. This indicates that the program is too complex for the underlying solvers. We also coded this example in similar Pvl code instead of C, which verifies in 193s. We suspect the failure is caused by quantifier instantiation, which instantiates too many quantifiers, resulting in the SMT solver on which VerCors relies stopping the exploration of quantifiers that are needed for successful verification.

For gemm V3, verification fails due to VerCors not sufficiently rewriting annotations of the directive.Footnote 11

Functional correctness results. Next, we evaluate fiveFootnote 12 algorithms with annotations and 20 schedules, both for the front-end and back-end. HaliVer proves functional correctness for the front-end, and both functional correctness and data race freedom and memory safety for the back-end for 19 of the 20 schedules. These results are given in Table 2. The table additionally has the amount of user provided annotations (LoA.) and the last column (Ann. incr.) indicates the growth of the annotations. The annotations of the C file (LoC) contain both the generated annotations, which are already present in Table 1 and the transformed user annotations.

For optimised programs, the annotation size is strongly related to the number of loops, as each loop needs its own loop invariants. Front-end verification is successful for all examples and is relatively fast compared to back-end verification. In verification of the C files produced by the back-end verification approach, time increases as the number of scheduling directives increases. Here, gemm V3 also fails for the same reason as outlined above.

Inconsistent Results. For gemm V2 for the memory benchmarks and for blur V3 and auto_viz V0, V2 and V3, VerCors does not always succeed with the verification. In the case of gemm V2, the verification sometimes hangs, which is timed out after 10 minutes. In the other cases, VerCors sometimes gave a verification failure. This inconsistency is due to the non-deterministic nature of the underlying SMT solvers.

Conclusions. With the front-end verification approach of HaliVer we are able to prove functional correctness properties for representative Halide algorithms. Using HaliVer ’s back-end verification approach, the tool provides correct annotations for the generated C programs. VerCors successfully verifies all but two programs. However, in the unsuccessful cases, HaliVer runs into limitations of the underlying tools. The verified programs are all verified within ten minutes. Finally, the manual annotation effort required is an order of magnitude larger than the effort required for HaliVer ’s approach.

5 Related Work

There is much work on optimising program transformations, either applied automatically or manually [2, 11], sometimes using scheduling languages [3, 6,7,8, 22, 23, 28]. The vast majority of this does not address functional correctness.

Work on functional correctness consists of techniques that apply verification every time a program is transformed, and techniques that verify the compiler.

Liu et al. [15] propose an approach inspired by scheduling languages, with proof obligations generated when a program is optimised, for automatic verification using Coq. The Cogent language [20] uses refinement proofs, to be verified in Isabelle/HOL. However, it does not separate algorithms from schedules. In [17, 18] an integer constraint solver and a proof checker are used, respectively, to verify the transformation of a program. In all these approaches, semantics-preservation is the focus, as opposed to specifying the intended behaviour. Model-to-model transformations can be verified w.r.t. the preservation of functional properties [21]. However, that work targets models, not code.

Regarding the verification of compilers, CompCert  [14] is a framework involving a formally verified C compiler. In [19], Halide ’s Term Rewriting System, used to reason about the applicability of schedules, is verified using Z3 and Coq. These approaches do not require verification every time an optimisation is applied, but verifying the compiler is time-consuming and complex, and has to be redone whenever the compiler is updated. Furthermore, they focus on semantics-preservation, not the intended behaviour of individual programs.

Alpinist  [27] is most closely related. This tool automatically optimises Pvl code, along with its annotations, for verification with VerCors. It allows the specification of intended behaviour, but it does not separate algorithms from schedules, forcing the user to reason about the technical details of parallelisation.

6 Conclusions & Future Work

We presented HaliVer, a tool for verifying optimised code by exploiting the strengths of scheduling languages and deductive verification. It allows focussing on functionality when annotating programs, keeping annotations succinct.

For future work, we want to extend the HaliVer tool with aspects not directly supported by VerCors, such as vectorisation. The master thesis of [24] defines a natural semantics for Halide. We want to formalise our front-end Pvl encoding with an axiomatic semantics to match this semantics. We also want to investigate the inconsistent results and see whether annotations with quantifiers can be rephrased to allow VerCors to be more consistent. In this work we have focussed on parallel CPU code, but we have designed our approach to be extendable to GPU code produced by Halide.

With the current expressiveness of the annotations, when reduction domains are present, HaliVer proves functional correctness for specific inputs. For example, in Listing 2 we can prove that if we require that . This can also be done for any input if the reduction domain is of known size, but then many annotations are needed. To make the annotations concise, a user needs to be able to use axiomatic data typesFootnote 13 and pure functions in their annotations. We expect that these annotations can be similarly transformed by our approach, and that is thus orthogonal to this contribution, but this is planned as future work.

Most Halide programs use floating point numbers. These are currently modelled as reals in VerCors. How to efficiently verify programs with floats using deductive verifiers is still an open research question. Once this is addressed, HaliVer will be able to give better guarantees.

We require that the bounds of a Halide program are set to concrete values for our back-end verification approach. HaliVer transforms the annotations the same way for not know bounds, but the underlying tools have difficulty verifying these programs. With unknown bounds, we end up with nonlinear arithmetic due to the flattening of multi-dimensional functions on one-dimensional arrays. This is generally undecidable, so the SMT solvers that VerCors rely on cannot handle it. We will investigate if there are ways to tackle this in our domain-specific case.