This is the kernel generator (kerngen
) responsible for producing HERACLES ISA
kernels for various polynomial operations that occur in cryptography (or
elsewhere) such as in homomorphic encryption (HE). A kernel is a code snippet
of P-ISA instructions with the purpose of implementing some high level
polynomial operation.
kerngen
is written as a pure python program. Requirements required to be
installed,
- python >= 3.10
- pip (recommend >= 24.0)
- and python requirements.
To install the python dependencies and development tools simply run,
pip install -r requirements.txt
The design is a simplified interpreter pattern. A domain specific language
defined as a 'kernel language' is received as input to the kernel generator.
This kernel language describes (which can be used for HE schemes) operations on
polynomials with given context parameters. This language is interpreted as a
high level instruction
which is then mapped to its corresponding low level P-ISA instruction
. kerngen
uses a common unix command line utility
convention and the resulting P-ISA kernel is sent to stdout
.
Under the high_parser
directory is the core of the kerngen
logic with the
principal classes being Parser
and Generator
. For completeness, we take a
quick look through the files.
-
__init__.py
is worth a mention because it contains imports to the most commonly used types. This makes it easier to import those types from the top level including when writing kernels e.g.from high_parser import Polys
. -
config.py
is a minor file primarily containing theConfig
class. The class itself is used as a global singleton object to hold configuration information ofkerngen
. It was introduced to not disturb the existing APIs while changing global behaviour i.e. a legacy mode. -
generators.py
contains theGenerator
class responsible for dealing with the manifest file and loading the appropriate kernel class. Instances ofParser
have an instance of this class for a given manifest file. Instances should be created using the factory class methodfrom_manifest
and providing the path to the manifest file and ascheme
. Although it is referred to asscheme
it is in fact just a key label mapping to a collection of grouped kernels. Lookup can then be performed usingget_kernel
given a valid kernel operation name. -
parser.py
containsParser
responsible for parsing the input kernel language and creating the correct corresponding command objects for the interpreter to process. The output of parsing is given by an instance ofParserResult
, an object containing all the parsed information. Note that the object is lazy in generating the P-ISA operations which are returned as a python generator when theget_pisa_ops
method is called. Note that one current peculiarity is that once a parser is instantiated that a generator must be set before parsing can be done. This is a current limitation thatkerngen
currently can only have kernels defined under thepisa_generators
directory. -
pisa_operations.py
contains all the known P-ISA operations. These are essentially objects that are responsible for formatting the operation strings forkerngen
outputs. The class names reflect the P-ISA operations as closely as possible. -
types.py
has many types used bykerngen
by theParser
and by kernel classes. This is where the parser types are implemented, namely,Context
,Data
,EmptyLine
,Comment
,Immediate
,HighOp
. Note thatHighOp
is the class that all kernels inherit from. The other main type of note that is implemented here is thePolys
class. This is the type that is heavily utilized by kernel writers to represent polynomials. At the most basic, these polynomial objects contain: apart
, elements/coefficients of the high-level data structure which themselves represent polynomials; anrns
number that describes up to how many RNS numbers apart
is decomposed by; and aunit
represents how many units (a.k.a. a block) apart
has to be divided by. Theunit
is both an underlying hardware division, but also for NTT operations to work apart
, thepart
must be at least split in two to feed in as inputs to the operation.
There are several kinds of commands. Keywords that cannot be used for kernel names,
- `CONTEXT`
- `DATA`
- `IMM`
All other commands are assumed to be operations. All operations are case insensitive, but the convention we use is the operations are capitalized. These are defined in the manifest.json file.
CONTEXT BGV 8192 4
DATA a 2
DATA b 2
DATA c 2
ADD c a b
Context defines the global properties (scheme, poly_order, max_rns, key_rns(optional))
of the input script.
CONTEXT
sets a global context for properties required by the kernels.
- first field defines what we call scheme. In reality, it specifies the set of kernel instructions given in the manifest file, see manifest.json.
- second field defines the polynomial size for the
DATA
. This is required by the generating kernels to define how many units (multiples of the native polynomial size, 8192 in HERACLES silicon case) are required and handled. - third field defines the max RNS, the global max number of how many 32 bit prime number moduli (HERACLES silicon case) are in the modulus chain that the kernels can have or need to handle.
- (optional) fourth field defines the key RNS, the number of additional moduli
that the relinearization key has relative to the third field. i.e. If
max_rns
is 3 andkey_rns
is 1 the total max RNS of the relinearization key will be 4. Note this field is only required for calling therelin
kernel.
DATA
defines polynomial symbols to be used and their attribute(s) (num_parts
) where
num_parts
is the number of polynomials that comprise the data variable.
IMM
declares a fixed symbol name that can be used for operations that
expect and immediate value(s).
The main entry point to the kernel generator is kerngen.py. This
script expects input from stdin
in the form of the input high language
described above. It can be called with
./kerngen.py < addition.data
where addition.data
is a text file containing the high language for an ADD
operation.
The kernel generator prints two comments, a context and kernel descriptor
respectively, followed by the P-ISA kernel. If desired, the comments can be
disabled by passing the -q
or --quiet
flag to the kernel generator, i.e.,
./kerngen.py -q < addition.data
You can add new kernel generators that you have developed by creating a class
that inherits from the HighOp
abstract class (interface) and implementing the
to_pisa
method; turning this instruction into a P-ISA instruction class.
Examples can be seen in the simpler implementations given in
basic.py. Also, provide a class method
from_string
that will be passed the args for that command.
For kerngen
to know of your class that represents a new command of the high
language, simply add an entry into the JSON object in the
manifest.json file. The key of the outermost
JSON object is the FHE scheme {BGV, CKKS, ...}
; this key corresponds to a set
of associated operations. Each operation (inner JSON object) consists of the
operation name OPNAME
as its key and a list containing the class name as the
first entry and the file it is located in as its second. e.g.
{
"SCHEME": {
"OPNAME": ["ClassName", "filename.py"]
}
}
For kernel writers the reserved words that cannot be used as OPNAME
are:
- CONTEXT
- DATA
- IMM
The kernel generator has been designed to make it easy to add new kernels. Kernel files are typically placed in the pisa_generators directory to simplify the manifest file as the paths are relative to this directory.
Before writing the kernel you will require to import the pisa_operations
module and any relevant types from the high_parser
such as the HighOp
and
Context
import high_parser.pisa_operations as pisa_op
from high_parser.pisa_operations import PIsaOp
from high_parser import Context, HighOp, Polys
The Polys
class will be the most commonly used type in most kernels to
represent the inputs and outputs of the operation. This type represents the
polynomials and holds information such as the name
of symbol that represents
the polynomial, the number of parts
, and the rns
.
At a high level kernels convert high-level operations into low-level P-ISA
operations, thus all kernels will need to inherit from HighOp
and define the
conversion function to_pisa
as follows
@dataclass
class NewKernel(HighOp):
"""Class representing the high-level NewKernel operation"""
context: Context
output: Polys
input0: Polys
def to_pisa(self) -> list[PIsaOp]:
"""Return the p-ISA equivalent of the NewKernel operation"""
If the kernel consists of an algorithm already represented by an existing
kernel it is possible to import the necessary kernel and compose the new kernel
using existing kernels. For example the Square
kernel requires a Mul
operation
from .basic import Mul
class Square(HighOp):
...
mul = Mul(...)
see square.py for a complete example of this.
You will find that during kernel writing, you will end up with a collection of
either P-ISA operation objects, other kernel objects, or a mixture of both. For
your convenience a useful function mixed_to_pisa_ops
is provided that can
take all of these sequentially and outputs the required list[PIsaOp]
.
Tests are provided in the tests directory and use the pytest framework. To run the tests run the following
pytest <test-directory>