re: issue https://github.com/Unidata/netcdf-c/issues/1156 Starting with HDF5 version 1.10.x, the plugin code MUST be careful when using the standard *malloc()*, *realloc()*, and *free()* function. In the event that the code is allocating, reallocating, or free'ing memory that either came from -- or will be exported to -- the calling HDF5 library, then one MUST use the corresponding HDF5 functions *H5allocate_memory()*, *H5resize_memory()*, *H5free_memory()* [5] to avoid memory failures. Additionally, if your filter code leaks memory, then the HDF5 library generates a failure something like this. ```` H5MM.c:232: H5MM_final_sanity_check: Assertion `0 == H5MM_curr_alloc_bytes_s' failed. ```` This PR modifies the code in the plugins directory to conform to these new requirements. This raises a question about the libhdf5 code where this same problem may occur. We need to scan especially nc4hdf.c to look for this problem.
19 KiB
NetCDF-4 Filter Support
NetCDF-4 Filter Support
[TOC]
Introduction
The HDF5 library (1.8.11 and later) supports a general filter mechanism to apply various kinds of filters to datasets before reading or writing. The netCDF enhanced (aka netCDF-4) library inherits this capability since it depends on the HDF5 library.
Filters assume that a variable has chunking defined and each chunk is filtered before writing and "unfiltered" after reading and before passing the data to the user.
The most common kind of filter is a compression-decompression filter, and that is the focus of this document.
HDF5 supports dynamic loading of compression filters using the following process for reading of compressed data.
-
Assume that we have a dataset with one or more variables that were compressed using some algorithm. How the dataset was compressed will be discussed subsequently.
-
Shared libraries or DLLs exist that implement the compress/decompress algorithm. These libraries have a specific API so that the HDF5 library can locate, load, and utilize the compressor. These libraries are expected to installed in a specific directory.
Enabling A Compression Filter
In order to compress a variable, the netcdf-c library must be given three pieces of information: (1) some unique identifier for the filter to be used, (2) a vector of parameters for controlling the action of the compression filter, and (3) a shared library implementation of the filter.
The meaning of the parameters is, of course, completely filter dependent and the filter description [3] needs to be consulted. For bzip2, for example, a single parameter is provided representing the compression level. It is legal to provide a zero-length set of parameters. Defaults are not provided, so this assumes that the filter can operate with zero parameters.
Filter ids are assigned by the HDF group. See [4] for a current list of assigned filter ids. Note that ids above 32767 can be used for testing without registration.
The first two pieces of information can be provided in one of three ways: using ncgen, via an API call, or via command line parameters to nccopy. In any case, remember that filtering also requires setting chunking, so the variable must also be marked with chunking information.
Using The API
The necessary API methods are included in netcdf.h by default. One API method is for setting the filter to be used when writing a variable. The relevant signature is as follows.
int nc_def_var_filter(int ncid, int varid, unsigned int id, size_t nparams, const unsigned int* parms);
This must be invoked after the variable has been created and before nc_enddef is invoked.
A second API methods makes it possible to query a variable to obtain information about any associated filter using this signature.
int nc_inq_var_filter(int ncid, int varid, unsigned int* idp, size_t* nparams, unsigned int* params);
The filter id will be returned in the idp argument (if non-NULL), the number of parameters in nparamsp and the actual parameters in params. As is usual with the netcdf API, one is expected to call this function twice. The first time to get nparams and the second to get the parameters in client-allocated memory.
Using ncgen
In a CDL file, compression of a variable can be specified by annotating it with the following attribute:
- ''_Filter'' -- a string containing a comma separated list of constants specifying (1) the filter id to apply, and (2) a vector of constants representing the parameters for controlling the operation of the specified filter. See the section on the parameter encoding syntax for the details on the allowable kinds of constants.
This is a "special" attribute, which means that it will normally be invisible when using ncdump unless the -s flag is specified.
Example CDL File (Data elided)
netcdf bzip2 {
dimensions:
dim0 = 4 ; dim1 = 4 ; dim2 = 4 ; dim3 = 4 ;
variables:
float var(dim0, dim1, dim2, dim3) ;
var:_Filter = "307,9" ;
var:_Storage = "chunked" ;
var:_ChunkSizes = 4, 4, 4, 4 ;
data:
...
}
Using nccopy
When copying a netcdf file using nccopy it is possible to specify filter information for any output variable by using the "-F" option on the command line; for example:
nccopy -F "var,307,9" unfiltered.nc filtered.nc
Assume that unfiltered.nc has a chunked but not bzip2 compressed variable named "var". This command will create that variable in the filtered.nc output file but using filter with id 307 (i.e. bzip2) and with parameter(s) 9 indicating the compression level. See the section on the parameter encoding syntax for the details on the allowable kinds of constants.
The "-F" option can be used repeatedly as long as the variable name part is different. A different filter id and parameters can be specified for each occurrence.
As a rule, any input filter on an input variable will be applied to the equivalent output variable -- assuming the output file type is netcdf-4. It is, however, sometimes convenient to suppress output compression either totally or on a per-variable basis. Total suppression of output filters can be accomplished by specifying a special case of "-F", namely this.
nccopy -F "none" input.nc output.nc
Suppression of output filtering for a specific variable can be accomplished using this format.
nccopy -F "var,none" input.nc output.nc
where "var" is the fully qualified name of the variable.
The rules for all possible cases of the "-F" flag are defined by this table.
-F none | -Fvar,... | Input Filter | Applied Output Filter |
---|---|---|---|
true | unspecified | NA | unfiltered |
true | -Fvar,none | NA | unfiltered |
true | -Fvar,... | NA | use output filter |
false | unspecified | defined | use input filter |
false | -Fvar,none | NA | unfiltered |
false | -Fvar,... | NA | use output filter |
Parameter Encoding
The parameters passed to a filter are encoded internally as a vector of 32-bit unsigned integers. It may be that the parameters required by a filter can naturally be encoded as unsigned integers. The bzip2 compression filter, for example, expects a single integer value from zero thru nine. This encodes naturally as a single unsigned integer.
Note that signed integers and single-precision (32-bit) float values also can easily be represented as 32 bit unsigned integers by proper casting to an unsigned integer so that the bit pattern is preserved. Simple integer values of type short or char (or the unsigned versions) can also be mapped to an unsigned integer by truncating to 16 or 8 bits respectively and then zero extending.
Machine byte order (aka endian-ness) is an issue for passing some kinds of parameters. You might define the parameters when compressing on a little endian machine, but later do the decompression on a big endian machine. Byte order is not an issue for 32-bit values because HDF5 takes care of converting them between the local machine byte order and network byte order.
Parameters whose size is larger than 32-bits present a byte order problem. This typically includes double precision floats and (signed or unsigned) 64-bit integers. For these cases, the machine byte order must be handled by the compression code. This is because HDF5 will treat, for example, an unsigned long long as two 32-bit unsigned integers and will convert each to network order separately. This means that on a machine whose byte order is different than the machine in which the parameters were initially created, the two integers are out of order and must be swapped to get the correct unsigned long long value. Consider this example. Suppose we have this little endian unsigned long long.
1000000230000004
In network byte order, it will be stored as two 32-bit integers.
20000001 40000003
On a big endian machine, this will be given to the filter in that form.
2000000140000003
But note that the proper big endian unsigned long long form is this.
4000000320000001
So, the two words need to be swapped.
But consider the case when both original and final machines are big endian.
- 4000000320000001
- 40000003 20000001
- 40000003 20000001
where #1 is the original number, #2 is the network order and #3 is the what is given to the filter. In this case we do not want to swap words.
The solution is to forcibly encode the original number using some specified endianness so that the filter always assumes it is getting its parameters in that order and will always do swapping as needed. This is irritating, but one needs to be aware of it. Since most machines are little-endian. We choose to use that as the endianness for handling 64 bit entities.
Filter Specification Syntax
Both of the utilities ncgen and nccopy allow the specification of filter parameters. These specifications consist of a sequence of comma separated constants. The constants are converted within the utility to a proper set of unsigned int constants (see the parameter encoding section).
To simplify things, various kinds of constants can be specified rather than just simple unsigned integers. The utilities will encode them properly using the rules specified in the parameter encoding section.
The currently supported constants are as follows.
Example | Type | Format Tag | Notes |
---|---|---|---|
-17b | signed 8-bit byte | b|B | Truncated to 8 bits and zero extended to 32 bits |
23ub | unsigned 8-bit byte | u|U b|B | Truncated to 8 bits and zero extended to 32 bits |
-25S | signed 16-bit short | s|S | Truncated to 16 bits and zero extended to 32 bits |
27US | unsigned 16-bit short | u|U s|S | Truncated to 16 bits and zero extended to 32 bits |
-77 | implicit signed 32-bit integer | Leading minus sign and no tag | |
77 | implicit unsigned 32-bit integer | No tag | |
93U | explicit unsigned 32-bit integer | u|U | |
789f | 32-bit float | f|F | |
12345678.12345678d | 64-bit double | d|D | Network byte order |
-9223372036854775807L | 64-bit signed long long | l|L | Network byte order |
18446744073709551615UL | 64-bit unsigned long long | u|U l|L | Network byte order |
- In all cases, except for an untagged positive integer, the format tag is required and determines how the constant is converted to one or two unsigned int values. The positive integer case is for backward compatibility.
- For signed byte and short, the value is sign extended to 32 bits and then treated as an unsigned int value.
- For double, and signed|unsigned long long, they are converted to network byte order and then treated as two unsigned int values. This is consistent with the parameter encoding.
Dynamic Loading Process
The documentation[1,2] for the HDF5 dynamic loading was (at the time this was written) out-of-date with respect to the actual HDF5 code (see HDF5PL.c). So, the following discussion is largely derived from looking at the actual code. This means that it is subject to change.
Plugin directory
The HDF5 loader expects plugins to be in a specified plugin directory. The default directory is:
- "/usr/local/hdf5/lib/plugin” for linux/unix operating systems (including Cygwin)
- “%ALLUSERSPROFILE%\hdf5\lib\plugin” for Windows systems, although the code does not appear to explicitly use this path.
The default may be overridden using the environment variable HDF5_PLUGIN_PATH.
Plugin Library Naming
Given a plugin directory, HDF5 examines every file in that directory that conforms to a specified name pattern as determined by the platform on which the library is being executed.
Platform | Basename | Extension |
---|---|---|
Linux | lib* | .so* |
OSX | lib* | .so* |
Cygwin | cyg* | .dll* |
Windows | * | .dll |
Plugin Verification
For each dynamic library located using the previous patterns, HDF5 attempts to load the library and attempts to obtain information from it. Specifically, It looks for two functions with the following signatures.
- H5PL_type_t H5PLget_plugin_type(void) -- This function is expected to return the constant value H5PL_TYPE_FILTER to indicate that this is a filter library.
- const void* H5PLget_plugin_info(void) -- This function returns a pointer to a table of type H5Z_class2_t. This table contains the necessary information needed to utilize the filter both for reading and for writing. In particular, it specifies the filter id implemented by the library and if must match that id specified for the variable in nc_def_var_filter in order to be used.
If plugin verification fails, then that plugin is ignored and the search continues for another, matching plugin.
Debugging
Debugging plugins can be very difficult. You will probably need to use the old printf approach for debugging the filter itself.
One case worth mentioning is when you have a dataset that is using an unknown filter. For this situation, you need to identify what filter(s) are used in the dataset. This can be accomplished using this command.
ncdump -s -h <dataset filename>
Since ncdump is not being asked to access the data (the -h flag), it can obtain the filter information without failures. Then it can print out the filter id and the parameters (the -s flag).
Test Case
Within the netcdf-c source tree, the directory netcdf-c/nc_test4 contains a test case (test_filter.c) for testing dynamic filter writing and reading using bzip2. Another test (test_filter_misc.c) validates parameter passing. These tests are disabled if --enable-shared is not set or if --enable-netcdf-4 is not set.
Example
A slightly simplified version of the filter test case is also available as an example within the netcdf-c source tree directory __netcdf-c/examples/C. The test is called filter_example.c and it is executed as part of the run_examples4.sh shell script. The test case demonstrates dynamic filter writing and reading.
The files example/C/hdf5plugins/Makefile.am and example/C/hdf5plugins/CMakeLists.txt demonstrate how to build the hdf5 plugin for bzip2.
Notes
Memory Allocation Issues
Starting with HDF5 version 1.10.x, the plugin code MUST be careful when using the standard malloc(), realloc(), and free() function.
In the event that the code is allocating, reallocating, for free'ing memory that either came from or will be exported to the calling HDF5 library, then one MUST use the corresponding HDF5 functions H5allocate_memory(), H5resize_memory(), H5free_memory() [5] to avoid memory failures.
Additionally, if your filter code leaks memory, then the HDF5 library generates a failure something like this.
H5MM.c:232: H5MM_final_sanity_check: Assertion `0 == H5MM_curr_alloc_bytes_s' failed.
One can look at the the code in plugins/H5Zbzip2.c and H5Zmisc.c to see this.
SZIP Issues
The current szip plugin code in the HDF5 library has some behaviors that can catch the unwary. Specifically, this filter may do two things.
- Add extra parameters to the filter parameters: going from the two parameters provided by the user to four parameters for internal use. It turns out that the two parameters provided when calling nc_def_var_filter correspond to the first two parameters of the four parameters returned by nc_inq_var_filter.
- Change the values of some parameters: the value of the options_mask argument is known to add additional flag bits, and the pixels_per_block parameter may be modified.
The reason for these changes is has to do with the fact that the szip API provided by the underlying H5Pset_szip function is actually a subset of the capabilities of the real szip implementation. Presumably this is for historical reasons.
In any case, if the caller uses the nc_inq_var_szip, then the values returned may differ from those originally specified. If one used the nc_inq_var_filter API calls, it may be the case that both the number of parameters and the values will differ from the original call to nc_def_var_filter.
Supported Systems
The current matrix of OS X build systems known to work is as follows.
Build System | Supported OS |
---|---|
Automake | Linux, Cygwin |
Cmake | Linux, Cygwin, Visual Studio |
Generic Plugin Build
If you do not want to use Automake or Cmake, the following has been known to work.
gcc -g -O0 -shared -o libbzip2.so <plugin source files> -L${HDF5LIBDIR} -lhdf5_hl -lhdf5 -L${ZLIBDIR} -lz
Appendix A. Byte Swap Code
Since in some cases, it is necessary for a filter to byte swap from little-endian to big-endian, This appendix provides sample code for doing this. It also provides a code snippet for testing if the machine the endianness of a machine.
Byte swap an 8-byte chunk of memory
static void
byteswap8(unsigned char* mem)
{
register unsigned char c;
c = mem[0];
mem[0] = mem[7];
mem[7] = c;
c = mem[1];
mem[1] = mem[6];
mem[6] = c;
c = mem[2];
mem[2] = mem[5];
mem[5] = c;
c = mem[3];
mem[3] = mem[4];
mem[4] = c;
}
Test for Machine Endianness
static const unsigned char b[4] = {0x0,0x0,0x0,0x1}; /* value 1 in big-endian*/
int endianness = (1 == *(unsigned int*)b); /* 1=>big 0=>little endian
References
- https://support.hdfgroup.org/HDF5/doc/Advanced/DynamicallyLoadedFilters/HDF5DynamicallyLoadedFilters.pdf
- https://support.hdfgroup.org/HDF5/doc/TechNotes/TechNote-HDF5-CompressionTroubleshooting.pdf
- https://portal.hdfgroup.org/display/support/Contributions#Contributions-filters
- https://support.hdfgroup.org/services/contributions.html#filters
- https://support.hdfgroup.org/HDF5/doc/RM/RM_H5.html
Point of Contact
Author: Dennis Heimbigner
Email: dmh at ucar dot edu
Initial Version: 1/10/2018
Last Revised: 2/5/2018