com.ibm.bi.dml.runtime.controlprogram.parfor.opt.OptimizerRuleBased.java Source code

Java tutorial

Introduction

Here is the source code for com.ibm.bi.dml.runtime.controlprogram.parfor.opt.OptimizerRuleBased.java

Source

/**
 * (C) Copyright IBM Corp. 2010, 2015
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
*/

package com.ibm.bi.dml.runtime.controlprogram.parfor.opt;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;

import com.ibm.bi.dml.conf.ConfigurationManager;
import com.ibm.bi.dml.conf.DMLConfig;
import com.ibm.bi.dml.hops.AggBinaryOp;
import com.ibm.bi.dml.hops.DataOp;
import com.ibm.bi.dml.hops.FunctionOp;
import com.ibm.bi.dml.hops.Hop;
import com.ibm.bi.dml.hops.AggBinaryOp.MMultMethod;
import com.ibm.bi.dml.hops.Hop.MultiThreadedHop;
import com.ibm.bi.dml.hops.Hop.ReOrgOp;
import com.ibm.bi.dml.hops.HopsException;
import com.ibm.bi.dml.hops.IndexingOp;
import com.ibm.bi.dml.hops.LeftIndexingOp;
import com.ibm.bi.dml.hops.LiteralOp;
import com.ibm.bi.dml.hops.OptimizerUtils;
import com.ibm.bi.dml.hops.ReorgOp;
import com.ibm.bi.dml.hops.rewrite.HopRewriteUtils;
import com.ibm.bi.dml.hops.rewrite.ProgramRewriteStatus;
import com.ibm.bi.dml.hops.rewrite.ProgramRewriter;
import com.ibm.bi.dml.hops.rewrite.RewriteInjectSparkLoopCheckpointing;
import com.ibm.bi.dml.hops.recompile.Recompiler;
import com.ibm.bi.dml.lops.LopProperties;
import com.ibm.bi.dml.lops.LopsException;
import com.ibm.bi.dml.parser.DMLProgram;
import com.ibm.bi.dml.parser.Expression.DataType;
import com.ibm.bi.dml.parser.FunctionStatementBlock;
import com.ibm.bi.dml.parser.LanguageException;
import com.ibm.bi.dml.parser.ParForStatement;
import com.ibm.bi.dml.parser.ParForStatementBlock;
import com.ibm.bi.dml.parser.StatementBlock;
import com.ibm.bi.dml.runtime.DMLRuntimeException;
import com.ibm.bi.dml.runtime.DMLUnsupportedOperationException;
import com.ibm.bi.dml.runtime.controlprogram.ForProgramBlock;
import com.ibm.bi.dml.runtime.controlprogram.FunctionProgramBlock;
import com.ibm.bi.dml.runtime.controlprogram.LocalVariableMap;
import com.ibm.bi.dml.runtime.controlprogram.ParForProgramBlock;
import com.ibm.bi.dml.runtime.controlprogram.Program;
import com.ibm.bi.dml.runtime.controlprogram.ProgramBlock;
import com.ibm.bi.dml.runtime.controlprogram.ParForProgramBlock.PDataPartitionFormat;
import com.ibm.bi.dml.runtime.controlprogram.ParForProgramBlock.PDataPartitioner;
import com.ibm.bi.dml.runtime.controlprogram.ParForProgramBlock.PExecMode;
import com.ibm.bi.dml.runtime.controlprogram.ParForProgramBlock.POptMode;
import com.ibm.bi.dml.runtime.controlprogram.ParForProgramBlock.PResultMerge;
import com.ibm.bi.dml.runtime.controlprogram.ParForProgramBlock.PTaskPartitioner;
import com.ibm.bi.dml.runtime.controlprogram.caching.MatrixObject;
import com.ibm.bi.dml.runtime.controlprogram.context.ExecutionContext;
import com.ibm.bi.dml.runtime.controlprogram.context.SparkExecutionContext;
import com.ibm.bi.dml.runtime.controlprogram.parfor.ProgramConverter;
import com.ibm.bi.dml.runtime.controlprogram.parfor.ResultMergeLocalFile;
import com.ibm.bi.dml.runtime.controlprogram.parfor.opt.OptNode.ExecType;
import com.ibm.bi.dml.runtime.controlprogram.parfor.opt.OptNode.NodeType;
import com.ibm.bi.dml.runtime.controlprogram.parfor.opt.OptNode.ParamType;
import com.ibm.bi.dml.runtime.controlprogram.parfor.opt.PerfTestTool.TestMeasure;
import com.ibm.bi.dml.runtime.controlprogram.parfor.stat.InfrastructureAnalyzer;
import com.ibm.bi.dml.runtime.instructions.Instruction;
import com.ibm.bi.dml.runtime.instructions.cp.Data;
import com.ibm.bi.dml.runtime.instructions.cp.FunctionCallCPInstruction;
import com.ibm.bi.dml.runtime.instructions.spark.data.RDDObject;
import com.ibm.bi.dml.runtime.matrix.MatrixCharacteristics;
import com.ibm.bi.dml.runtime.matrix.MatrixFormatMetaData;
import com.ibm.bi.dml.runtime.matrix.data.MatrixBlock;
import com.ibm.bi.dml.runtime.matrix.data.OutputInfo;
import com.ibm.bi.dml.runtime.matrix.data.SparseRow;
import com.ibm.bi.dml.yarn.ropt.YarnClusterAnalyzer;

/**
 * Rule-Based ParFor Optimizer (time: O(n)):
 * 
 * Applied rule-based rewrites
 * - 1) rewrite set data partitioner (incl. recompile RIX)
 * - 2) rewrite remove unnecessary compare matrix
 * - 3) rewrite result partitioning (incl. recompile LIX)
 * - 4) rewrite set execution strategy
 * - 5) rewrite set operations exec type (incl. recompile)
 * - 6) rewrite use data colocation       
 * - 7) rewrite set partition replication factor
 * - 8) rewrite set export replication factor 
 * - 9) rewrite use nested parallelism 
 * - 10) rewrite set degree of parallelism
 * - 11) rewrite set task partitioner
 * - 12) rewrite set fused data partitioning and execution
 * - 13) rewrite transpose vector operations (for sparse)
 * - 14) rewrite set in-place result indexing
 * - 15) rewrite disable caching (prevent sparse serialization)
 * - 16) rewrite enable runtime piggybacking
 * - 17) rewrite inject spark loop checkpointing 
 * - 18) rewrite inject spark repartition (for zipmm)
 * - 19) rewrite set spark eager rdd caching 
 * - 20) rewrite set result merge               
 * - 21) rewrite set recompile memory budget
 * - 22) rewrite remove recursive parfor   
 * - 23) rewrite remove unnecessary parfor      
 *     
 * TODO fuse also result merge into fused data partitioning and execute
 *      (for writing the result directly from execute we need to partition
 *      columns/rows according to blocksize -> rewrite (only applicable if 
 *      numCols/blocksize>numreducers)+custom MR partitioner)
 * 
 * 
 * TODO take remote memory into account in data/result partitioning rewrites (smaller/larger)
 * TODO memory estimates with shared reads
 * TODO memory estimates of result merge into plan tree 
 * TODO blockwise partitioning
 *  
 */
public class OptimizerRuleBased extends Optimizer {

    public static final double PROB_SIZE_THRESHOLD_REMOTE = 100; //wrt # top-level iterations (min)
    public static final double PROB_SIZE_THRESHOLD_PARTITIONING = 2; //wrt # top-level iterations (min)
    public static final double PROB_SIZE_THRESHOLD_MB = 256 * 1024 * 1024; //wrt overall memory consumption (min)
    public static final int MAX_REPLICATION_FACTOR_PARTITIONING = 5;
    public static final int MAX_REPLICATION_FACTOR_EXPORT = 7;
    public static final boolean ALLOW_REMOTE_NESTED_PARALLELISM = false;
    public static final boolean APPLY_REWRITE_NESTED_PARALLELISM = false;
    public static final String FUNCTION_UNFOLD_NAMEPREFIX = "__unfold_";

    public static final double PAR_K_FACTOR = OptimizationWrapper.PAR_FACTOR_INFRASTRUCTURE;
    public static final double PAR_K_MR_FACTOR = 1.0 * OptimizationWrapper.PAR_FACTOR_INFRASTRUCTURE;

    //problem and infrastructure properties
    protected long _N = -1; //problemsize
    protected long _Nmax = -1; //max problemsize (including subproblems)
    protected int _lk = -1; //local par
    protected int _lkmaxCP = -1; //local max par (if only CP inst)
    protected int _lkmaxMR = -1; //local max par (if also MR inst)
    protected int _rnk = -1; //remote num nodes
    protected int _rk = -1; //remote par (mappers)
    protected int _rk2 = -1; //remote par (reducers)
    protected int _rkmax = -1; //remote max par (mappers)
    protected int _rkmax2 = -1; //remote max par (reducers)
    protected double _lm = -1; //local memory constraint
    protected double _rm = -1; //remote memory constraint (mappers)
    protected double _rm2 = -1; //remote memory constraint (reducers)

    protected CostEstimator _cost = null;

    @Override
    public CostModelType getCostModelType() {
        return CostModelType.STATIC_MEM_METRIC;
    }

    @Override
    public PlanInputType getPlanInputType() {
        return PlanInputType.ABSTRACT_PLAN;
    }

    @Override
    public POptMode getOptMode() {
        return POptMode.RULEBASED;
    }

    /**
     * Main optimization procedure.
     * 
     * Transformation-based heuristic (rule-based) optimization
     * (no use of sb, direct change of pb).
     */
    @Override
    public boolean optimize(ParForStatementBlock sb, ParForProgramBlock pb, OptTree plan, CostEstimator est,
            ExecutionContext ec) throws DMLRuntimeException, DMLUnsupportedOperationException {
        LOG.debug("--- " + getOptMode() + " OPTIMIZER -------");

        OptNode pn = plan.getRoot();
        double M0 = -1, M1 = -1, M2 = -1; //memory consumption

        //early abort for empty parfor body 
        if (pn.isLeaf())
            return true;

        //ANALYZE infrastructure properties
        analyzeProblemAndInfrastructure(pn);

        _cost = est;

        //debug and warnings output
        LOG.debug(getOptMode() + " OPT: Optimize w/ max_mem=" + toMB(_lm) + "/" + toMB(_rm) + "/" + toMB(_rm2)
                + ", max_k=" + _lk + "/" + _rk + "/" + _rk2 + ").");
        if (_rnk <= 0 || _rk <= 0)
            LOG.warn(getOptMode() + " OPT: Optimize for inactive cluster (num_nodes=" + _rnk + ", num_map_slots="
                    + _rk + ").");

        //ESTIMATE memory consumption 
        pn.setSerialParFor(); //for basic mem consumption 
        M0 = _cost.getEstimate(TestMeasure.MEMORY_USAGE, pn);
        LOG.debug(getOptMode() + " OPT: estimated mem (serial exec) M=" + toMB(M0));

        //OPTIMIZE PARFOR PLAN

        // rewrite 1: data partitioning (incl. log. recompile RIX)
        HashMap<String, PDataPartitionFormat> partitionedMatrices = new HashMap<String, PDataPartitionFormat>();
        rewriteSetDataPartitioner(pn, ec.getVariables(), partitionedMatrices);
        M1 = _cost.getEstimate(TestMeasure.MEMORY_USAGE, pn); //reestimate

        // rewrite 2: remove unnecessary compare matrix (before result partitioning)
        rewriteRemoveUnnecessaryCompareMatrix(pn, ec);

        // rewrite 3: rewrite result partitioning (incl. log/phy recompile LIX) 
        boolean flagLIX = rewriteSetResultPartitioning(pn, M1, ec.getVariables());
        M1 = _cost.getEstimate(TestMeasure.MEMORY_USAGE, pn); //reestimate 
        M2 = _cost.getEstimate(TestMeasure.MEMORY_USAGE, pn, LopProperties.ExecType.CP);
        LOG.debug(getOptMode() + " OPT: estimated new mem (serial exec) M=" + toMB(M1));
        LOG.debug(getOptMode() + " OPT: estimated new mem (serial exec, all CP) M=" + toMB(M2));

        // rewrite 4: execution strategy
        boolean flagRecompMR = rewriteSetExecutionStategy(pn, M0, M1, M2, flagLIX);

        //exec-type-specific rewrites
        if (pn.getExecType() == ExecType.MR || pn.getExecType() == ExecType.SPARK) {
            if (flagRecompMR) {
                //rewrite 5: set operations exec type
                rewriteSetOperationsExecType(pn, flagRecompMR);
                M1 = _cost.getEstimate(TestMeasure.MEMORY_USAGE, pn); //reestimate       
            }

            // rewrite 6: data colocation
            rewriteDataColocation(pn, ec.getVariables());

            // rewrite 7: rewrite set partition replication factor
            rewriteSetPartitionReplicationFactor(pn, partitionedMatrices, ec.getVariables());

            // rewrite 8: rewrite set partition replication factor
            rewriteSetExportReplicationFactor(pn, ec.getVariables());

            // rewrite 9: nested parallelism (incl exec types)   
            boolean flagNested = rewriteNestedParallelism(pn, M1, flagLIX);

            // rewrite 10: determine parallelism
            rewriteSetDegreeOfParallelism(pn, M1, flagNested);

            // rewrite 11: task partitioning 
            rewriteSetTaskPartitioner(pn, flagNested, flagLIX);

            // rewrite 12: fused data partitioning and execution
            rewriteSetFusedDataPartitioningExecution(pn, M1, flagLIX, partitionedMatrices, ec.getVariables());

            // rewrite 13: transpose sparse vector operations
            rewriteSetTranposeSparseVectorOperations(pn, partitionedMatrices, ec.getVariables());

            // rewrite 14: set in-place result indexing
            HashSet<String> inplaceResultVars = new HashSet<String>();
            rewriteSetInPlaceResultIndexing(pn, M1, ec.getVariables(), inplaceResultVars);

            // rewrite 15: disable caching
            rewriteDisableCPCaching(pn, inplaceResultVars, ec.getVariables());
        } else //if( pn.getExecType() == ExecType.CP )
        {
            // rewrite 10: determine parallelism
            rewriteSetDegreeOfParallelism(pn, M1, false);

            // rewrite 11: task partitioning
            rewriteSetTaskPartitioner(pn, false, false); //flagLIX always false 

            // rewrite 14: set in-place result indexing
            HashSet<String> inplaceResultVars = new HashSet<String>();
            rewriteSetInPlaceResultIndexing(pn, M1, ec.getVariables(), inplaceResultVars);

            if (!OptimizerUtils.isSparkExecutionMode()) {
                // rewrite 16: runtime piggybacking
                rewriteEnableRuntimePiggybacking(pn, ec.getVariables(), partitionedMatrices);
            } else {
                //rewrite 17: checkpoint injection for parfor loop body
                rewriteInjectSparkLoopCheckpointing(pn);

                //rewrite 18: repartition read-only inputs for zipmm 
                rewriteInjectSparkRepartition(pn, ec.getVariables());

                //rewrite 19: eager caching for checkpoint rdds
                rewriteSetSparkEagerRDDCaching(pn, ec.getVariables());
            }
        }

        // rewrite 20: set result merge
        rewriteSetResultMerge(pn, ec.getVariables(), true);

        // rewrite 21: set local recompile memory budget
        rewriteSetRecompileMemoryBudget(pn);

        ///////
        //Final rewrites for cleanup / minor improvements

        // rewrite 22: parfor (in recursive functions) to for
        rewriteRemoveRecursiveParFor(pn, ec.getVariables());

        // rewrite 23: parfor (par=1) to for 
        rewriteRemoveUnnecessaryParFor(pn);

        //info optimization result
        _numTotalPlans = -1; //_numEvaluatedPlans maintained in rewrites;
        return true;
    }

    /**
     * 
     * @param pn
     */
    protected void analyzeProblemAndInfrastructure(OptNode pn) {
        _N = Long.parseLong(pn.getParam(ParamType.NUM_ITERATIONS));
        _Nmax = pn.getMaxProblemSize();
        _lk = InfrastructureAnalyzer.getLocalParallelism();
        _lkmaxCP = (int) Math.ceil(PAR_K_FACTOR * _lk);
        _lkmaxMR = (int) Math.ceil(PAR_K_MR_FACTOR * _lk);
        _rnk = InfrastructureAnalyzer.getRemoteParallelNodes();
        _rk = InfrastructureAnalyzer.getRemoteParallelMapTasks();
        _rk2 = InfrastructureAnalyzer.getRemoteParallelReduceTasks();
        _rkmax = (int) Math.ceil(PAR_K_FACTOR * _rk);
        _rkmax2 = (int) Math.ceil(PAR_K_FACTOR * _rk2);
        _lm = OptimizerUtils.getLocalMemBudget();
        _rm = OptimizerUtils.getRemoteMemBudgetMap(false);
        _rm2 = OptimizerUtils.getRemoteMemBudgetReduce();

        //correction of max parallelism if yarn enabled because yarn
        //does not have the notion of map/reduce slots and hence returns 
        //small constants of map=10*nodes, reduce=2*nodes
        //(not doing this correction would loose available degree of parallelism)
        if (InfrastructureAnalyzer.isYarnEnabled()) {
            long tmprk = YarnClusterAnalyzer.getNumCores();
            _rk = (int) Math.max(_rk, tmprk);
            _rk2 = (int) Math.max(_rk2, tmprk / 2);
        }

        //correction of max parallelism and memory if spark runtime enabled because
        //spark limits the available parallelism by its own executor configuration
        if (OptimizerUtils.isSparkExecutionMode()) {
            _rk = (int) SparkExecutionContext.getDefaultParallelism(true);
            _rk2 = _rk; //equal map/reduce unless we find counter-examples 
            _rkmax = (int) Math.ceil(PAR_K_FACTOR * _rk);
            _rkmax2 = (int) Math.ceil(PAR_K_FACTOR * _rk2);
            int cores = SparkExecutionContext.getDefaultParallelism(true) / SparkExecutionContext.getNumExecutors();
            int ccores = (int) Math.min(cores, _N);
            _rm = SparkExecutionContext.getBroadcastMemoryBudget() / ccores;
            _rm2 = SparkExecutionContext.getBroadcastMemoryBudget() / ccores;
        }
    }

    ///////
    //REWRITE set data partitioner
    ///

    /**
     * 
     * @param n
     * @param partitionedMatrices  
     * @throws DMLRuntimeException 
     */
    protected boolean rewriteSetDataPartitioner(OptNode n, LocalVariableMap vars,
            HashMap<String, PDataPartitionFormat> partitionedMatrices) throws DMLRuntimeException {
        if (n.getNodeType() != NodeType.PARFOR)
            LOG.warn(getOptMode() + " OPT: Data partitioner can only be set for a ParFor node.");

        boolean blockwise = false;

        //preparations
        long id = n.getID();
        Object[] o = OptTreeConverter.getAbstractPlanMapping().getMappedProg(id);
        ParForStatementBlock pfsb = (ParForStatementBlock) o[0];
        ParForProgramBlock pfpb = (ParForProgramBlock) o[1];

        //search for candidates
        boolean apply = false;
        if (OptimizerUtils.isHybridExecutionMode() //only if we are allowed to recompile
                && (_N >= PROB_SIZE_THRESHOLD_PARTITIONING || _Nmax >= PROB_SIZE_THRESHOLD_PARTITIONING)) //only if beneficial wrt problem size
        {
            ArrayList<String> cand = pfsb.getReadOnlyParentVars();
            HashMap<String, PDataPartitionFormat> cand2 = new HashMap<String, PDataPartitionFormat>();
            for (String c : cand) {
                PDataPartitionFormat dpf = pfsb.determineDataPartitionFormat(c);
                //System.out.println("Partitioning Format: "+dpf);
                if (dpf != PDataPartitionFormat.NONE && dpf != PDataPartitionFormat.BLOCK_WISE_M_N) //FIXME
                {
                    cand2.put(c, dpf);
                }

            }

            apply = rFindDataPartitioningCandidates(n, cand2, vars);
            if (apply)
                partitionedMatrices.putAll(cand2);
        }

        PDataPartitioner REMOTE = OptimizerUtils.isSparkExecutionMode() ? PDataPartitioner.REMOTE_SPARK
                : PDataPartitioner.REMOTE_MR;
        PDataPartitioner pdp = (apply) ? REMOTE : PDataPartitioner.NONE;
        //NOTE: since partitioning is only applied in case of MR index access, we assume a large
        //      matrix and hence always apply REMOTE_MR (the benefit for large matrices outweigths
        //      potentially unnecessary MR jobs for smaller matrices)

        // modify rtprog 
        pfpb.setDataPartitioner(pdp);
        // modify plan
        n.addParam(ParamType.DATA_PARTITIONER, pdp.toString());

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set data partitioner' - result=" + pdp.toString() + " ("
                + ProgramConverter.serializeStringCollection(partitionedMatrices.keySet()) + ")");

        return blockwise;
    }

    /**
     * 
     * @param n
     * @param cand
     * @return
     * @throws DMLRuntimeException 
     */
    protected boolean rFindDataPartitioningCandidates(OptNode n, HashMap<String, PDataPartitionFormat> cand,
            LocalVariableMap vars) throws DMLRuntimeException {
        boolean ret = false;

        if (!n.isLeaf()) {
            for (OptNode cn : n.getChilds())
                if (cn.getNodeType() != NodeType.FUNCCALL) //prevent conflicts with aliases
                    ret |= rFindDataPartitioningCandidates(cn, cand, vars);
        } else if (n.getNodeType() == NodeType.HOP && n.getParam(ParamType.OPSTRING).equals(IndexingOp.OPSTRING)) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            String inMatrix = h.getInput().get(0).getName();
            if (cand.containsKey(inMatrix)) //Required Condition: partitioning applicable
            {
                PDataPartitionFormat dpf = cand.get(inMatrix);
                double mnew = getNewRIXMemoryEstimate(n, inMatrix, dpf, vars);
                //NOTE: for the moment, we do not partition according to the remote mem, because we can execute 
                //it even without partitioning in CP. However, advanced optimizers should reason about this                   
                //double mold = h.getMemEstimate();
                if (n.getExecType() == ExecType.MR || n.getExecType() == ExecType.SPARK) //Opt Condition: MR/Spark
                // || (mold > _rm && mnew <= _rm)   ) //Opt Condition: non-MR special cases (for remote exec)
                {
                    //NOTE: subsequent rewrites will still use the MR mem estimate
                    //(guarded by subsequent operations that have at least the memory req of one partition)
                    //if( mnew < _lm ) //apply rewrite if partitions fit into memory
                    //   n.setExecType(ExecType.CP);
                    //else
                    //   n.setExecType(ExecType.CP); //CP_FILE, but hop still in MR 
                    n.setExecType(ExecType.CP);
                    n.addParam(ParamType.DATA_PARTITION_FORMAT, dpf.toString());
                    h.setMemEstimate(mnew); //CP vs CP_FILE in ProgramRecompiler bases on mem_estimate
                    ret = true;
                }
            }
        }

        return ret;
    }

    /**
     * TODO consolidate mem estimation with Indexing Hop
     * 
     * NOTE: Using the dimensions without sparsity is a conservative worst-case consideration.
     * 
     * @param n
     * @param varName
     * @param dpf
     * @return
     * @throws DMLRuntimeException 
     */
    protected double getNewRIXMemoryEstimate(OptNode n, String varName, PDataPartitionFormat dpf,
            LocalVariableMap vars) throws DMLRuntimeException {
        double mem = -1;

        //not all intermediates need to be known on optimize
        Data dat = vars.get(varName);
        if (dat != null) {
            MatrixObject mo = (MatrixObject) dat;

            //those are worst-case (dense) estimates
            switch (dpf) {
            case COLUMN_WISE:
                mem = OptimizerUtils.estimateSize(mo.getNumRows(), 1);
                break;
            case ROW_WISE:
                mem = OptimizerUtils.estimateSize(1, mo.getNumColumns());
                break;
            case BLOCK_WISE_M_N:
                mem = Integer.MAX_VALUE; //TODO
                break;

            default:
                //do nothing
            }
        }

        return mem;
    }

    /**
     * 
     * @param mo
     * @param dpf
     * @return
     * @throws DMLRuntimeException
     */
    protected static LopProperties.ExecType getRIXExecType(MatrixObject mo, PDataPartitionFormat dpf)
            throws DMLRuntimeException {
        return getRIXExecType(mo, dpf, false);
    }

    /**
     * 
     * @param mo
     * @param dpf
     * @return
     * @throws DMLRuntimeException
     */
    protected static LopProperties.ExecType getRIXExecType(MatrixObject mo, PDataPartitionFormat dpf,
            boolean withSparsity) throws DMLRuntimeException {
        double mem = -1;

        long rlen = mo.getNumRows();
        long clen = mo.getNumColumns();
        long brlen = mo.getNumRowsPerBlock();
        long bclen = mo.getNumColumnsPerBlock();
        long nnz = mo.getNnz();
        double lsparsity = ((double) nnz) / rlen / clen;
        double sparsity = withSparsity ? lsparsity : 1.0;

        switch (dpf) {
        case COLUMN_WISE:
            mem = OptimizerUtils.estimateSizeExactSparsity(mo.getNumRows(), 1, sparsity);
            break;
        case COLUMN_BLOCK_WISE:
            mem = OptimizerUtils.estimateSizeExactSparsity(mo.getNumRows(), bclen, sparsity);
            break;
        case ROW_WISE:
            mem = OptimizerUtils.estimateSizeExactSparsity(1, mo.getNumColumns(), sparsity);
            break;
        case ROW_BLOCK_WISE:
            mem = OptimizerUtils.estimateSizeExactSparsity(brlen, mo.getNumColumns(), sparsity);
            break;

        default:
            //do nothing   
        }

        if (mem < OptimizerUtils.getLocalMemBudget())
            return LopProperties.ExecType.CP;
        else
            return LopProperties.ExecType.CP_FILE;
    }

    /**
     * 
     * @param mo
     * @param dpf
     * @return
     * @throws DMLRuntimeException
     */
    public static PDataPartitionFormat decideBlockWisePartitioning(MatrixObject mo, PDataPartitionFormat dpf)
            throws DMLRuntimeException {
        long rlen = mo.getNumRows();
        long clen = mo.getNumColumns();
        long brlen = mo.getNumRowsPerBlock();
        long bclen = mo.getNumColumnsPerBlock();
        long k = InfrastructureAnalyzer.getRemoteParallelMapTasks();

        PDataPartitionFormat ret = dpf;
        if (getRIXExecType(mo, dpf) == LopProperties.ExecType.CP)
            if (ret == PDataPartitionFormat.ROW_WISE) {
                if (rlen / brlen > 4 * k && //note: average sparsity, read must deal with it
                        getRIXExecType(mo, PDataPartitionFormat.ROW_BLOCK_WISE,
                                false) == LopProperties.ExecType.CP) {
                    ret = PDataPartitionFormat.ROW_BLOCK_WISE;
                }
            } else if (ret == PDataPartitionFormat.COLUMN_WISE) {
                if (clen / bclen > 4 * k && //note: average sparsity, read must deal with it
                        getRIXExecType(mo, PDataPartitionFormat.COLUMN_BLOCK_WISE,
                                false) == LopProperties.ExecType.CP) {
                    ret = PDataPartitionFormat.COLUMN_BLOCK_WISE;
                }
            }

        return ret;
    }

    /**
     * 
     * @return
     * @throws DMLRuntimeException 
     */
    public static boolean allowsBinaryCellPartitions(MatrixObject mo, PDataPartitionFormat dpf)
            throws DMLRuntimeException {
        return (getRIXExecType(mo, PDataPartitionFormat.COLUMN_BLOCK_WISE, false) == LopProperties.ExecType.CP);
    }

    ///////
    //REWRITE set result partitioning
    ///

    /**
     * 
     * @param n
     * @throws DMLRuntimeException
     */
    protected boolean rewriteSetResultPartitioning(OptNode n, double M, LocalVariableMap vars)
            throws DMLRuntimeException {
        //preparations
        long id = n.getID();
        Object[] o = OptTreeConverter.getAbstractPlanMapping().getMappedProg(id);
        ParForProgramBlock pfpb = (ParForProgramBlock) o[1];

        //search for candidates
        Collection<OptNode> cand = n.getNodeList(ExecType.MR);

        //determine if applicable
        boolean apply = M < _rm //ops fit in remote memory budget
                && !cand.isEmpty() //at least one MR
                && isResultPartitionableAll(cand, pfpb.getResultVariables(), vars,
                        pfpb.getIterablePredicateVars()[0]); // check candidates

        //recompile LIX
        if (apply) {
            try {
                for (OptNode lix : cand)
                    recompileLIX(lix, vars);
            } catch (Exception ex) {
                throw new DMLRuntimeException("Unable to recompile LIX.", ex);
            }
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set result partitioning' - result=" + apply);

        return apply;
    }

    /**
     * 
     * @param nlist
     * @param resultVars
     * @param vars
     * @param iterVarname
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean isResultPartitionableAll(Collection<OptNode> nlist, ArrayList<String> resultVars,
            LocalVariableMap vars, String iterVarname) throws DMLRuntimeException {
        boolean ret = true;
        for (OptNode n : nlist) {
            ret &= isResultPartitionable(n, resultVars, vars, iterVarname);
            if (!ret) //early abort
                break;
        }

        return ret;
    }

    /**
     * 
     * @param n
     * @param resultVars
     * @param vars
     * @param iterVarname
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean isResultPartitionable(OptNode n, ArrayList<String> resultVars, LocalVariableMap vars,
            String iterVarname) throws DMLRuntimeException {
        boolean ret = true;

        //check left indexing operator
        String opStr = n.getParam(ParamType.OPSTRING);
        if (opStr == null || !opStr.equals(LeftIndexingOp.OPSTRING))
            ret = false;

        Hop h = null;
        Hop base = null;

        if (ret) {
            h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            base = h.getInput().get(0);

            //check result variable
            if (!resultVars.contains(base.getName()))
                ret = false;
        }

        //check access pattern, memory budget
        if (ret) {
            int dpf = 0;
            Hop inpRowL = h.getInput().get(2);
            Hop inpRowU = h.getInput().get(3);
            Hop inpColL = h.getInput().get(4);
            Hop inpColU = h.getInput().get(5);
            if ((inpRowL.getName().equals(iterVarname) && inpRowU.getName().equals(iterVarname)))
                dpf = 1; //rowwise
            if ((inpColL.getName().equals(iterVarname) && inpColU.getName().equals(iterVarname)))
                dpf = (dpf == 0) ? 2 : 3; //colwise or cellwise

            if (dpf == 0)
                ret = false;
            else {
                //check memory budget
                MatrixObject mo = (MatrixObject) vars.get(base.getName());
                if (mo.getNnz() != 0) //-1 valid because result var known during opt
                    ret = false;

                //Note: for memory estimation the common case is sparse since remote_mr and individual tasks;
                //and in the dense case, we would not benefit from result partitioning
                boolean sparse = MatrixBlock.evalSparseFormatInMemory(base.getDim1(), base.getDim2(),
                        base.getDim1());

                if (sparse) {
                    //custom memory estimatation in order to account for structural properties
                    //e.g., for rowwise we know that we only pay one sparserow overhead per task
                    double memSparseBlock = estimateSizeSparseRowBlock(base.getDim1());
                    double memSparseRow1 = estimateSizeSparseRow(base.getDim2(), base.getDim2());
                    double memSparseRowMin = estimateSizeSparseRowMin(base.getDim2());

                    double memTask1 = -1;
                    int taskN = -1;
                    switch (dpf) {
                    case 1: //rowwise
                        //sparse block and one sparse row per task
                        memTask1 = memSparseBlock + memSparseRow1;
                        taskN = (int) ((_rm - memSparseBlock) / memSparseRow1);
                        break;
                    case 2: //colwise
                        //sparse block, sparse row per row but shared over tasks
                        memTask1 = memSparseBlock + memSparseRowMin * base.getDim1();
                        taskN = estimateNumTasksSparseCol(_rm - memSparseBlock, base.getDim1());
                        break;
                    case 3: //cellwise
                        //sparse block and one minimal sparse row per task
                        memTask1 = memSparseBlock + memSparseRowMin;
                        taskN = (int) ((_rm - memSparseBlock) / memSparseRowMin);
                        break;
                    }

                    if (memTask1 > _rm || memTask1 < 0)
                        ret = false;
                    else
                        n.addParam(ParamType.TASK_SIZE, String.valueOf(taskN));
                } else {
                    //dense (no result partitioning possible)
                    ret = false;
                }
            }
        }

        return ret;
    }

    /**
     * 
     * @param rows
     * @return
     */
    private double estimateSizeSparseRowBlock(long rows) {
        //see MatrixBlock.estimateSizeSparseInMemory
        return 44 + rows * 8;
    }

    /**
     * 
     * @param cols
     * @param nnz
     * @return
     */
    private double estimateSizeSparseRow(long cols, long nnz) {
        //see MatrixBlock.estimateSizeSparseInMemory
        long cnnz = Math.max(SparseRow.initialCapacity, Math.max(cols, nnz));
        return (116 + 12 * cnnz); //sparse row
    }

    /**
     * 
     * @param cols
     * @return
     */
    private double estimateSizeSparseRowMin(long cols) {
        //see MatrixBlock.estimateSizeSparseInMemory
        long cnnz = Math.min(SparseRow.initialCapacity, cols);
        return (116 + 12 * cnnz); //sparse row
    }

    /**
     * 
     * @param budget
     * @param rows
     * @return
     */
    private int estimateNumTasksSparseCol(double budget, long rows) {
        //see MatrixBlock.estimateSizeSparseInMemory
        double lbudget = budget - rows * 116;
        return (int) Math.floor(lbudget / 12);
    }

    /**
     * 
     * @param n
     * @throws DMLRuntimeException
     * @throws HopsException
     * @throws LopsException
     * @throws DMLUnsupportedOperationException
     * @throws IOException
     */
    protected void recompileLIX(OptNode n, LocalVariableMap vars) throws DMLRuntimeException, HopsException,
            LopsException, DMLUnsupportedOperationException, IOException {
        Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());

        //set forced exec type
        h.setForcedExecType(LopProperties.ExecType.CP);
        n.setExecType(ExecType.CP);

        //recompile parent pb
        long pid = OptTreeConverter.getAbstractPlanMapping().getMappedParentID(n.getID());
        OptNode nParent = OptTreeConverter.getAbstractPlanMapping().getOptNode(pid);
        Object[] o = OptTreeConverter.getAbstractPlanMapping().getMappedProg(pid);
        StatementBlock sb = (StatementBlock) o[0];
        ProgramBlock pb = (ProgramBlock) o[1];

        //keep modified estimated of partitioned rix (in same dag as lix)
        HashMap<Hop, Double> estRix = getPartitionedRIXEstimates(nParent);

        //construct new instructions
        ArrayList<Instruction> newInst = Recompiler.recompileHopsDag(sb, sb.get_hops(), vars, null, false, 0);
        pb.setInstructions(newInst);

        //reset all rix estimated (modified by recompile)
        resetPartitionRIXEstimates(estRix);

        //set new mem estimate (last, otherwise overwritten from recompile)
        h.setMemEstimate(_rm - 1);
    }

    /**
     * 
     * @param parent
     * @return
     */
    protected HashMap<Hop, Double> getPartitionedRIXEstimates(OptNode parent) {
        HashMap<Hop, Double> estimates = new HashMap<Hop, Double>();
        for (OptNode n : parent.getChilds())
            if (n.getParam(ParamType.DATA_PARTITION_FORMAT) != null) {
                Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
                estimates.put(h, h.getMemEstimate());
            }
        return estimates;
    }

    /**
     * 
     * @param parent
     * @param estimates
     */
    protected void resetPartitionRIXEstimates(HashMap<Hop, Double> estimates) {
        for (Entry<Hop, Double> e : estimates.entrySet()) {
            Hop h = e.getKey();
            double val = e.getValue();
            h.setMemEstimate(val);
        }
    }

    ///////
    //REWRITE set execution strategy
    ///

    /**
     * 
     * @param n
     * @param M
     * @throws DMLRuntimeException 
     */
    protected boolean rewriteSetExecutionStategy(OptNode n, double M0, double M, double M2, boolean flagLIX)
            throws DMLRuntimeException {
        boolean isCPOnly = n.isCPOnly();
        boolean isCPOnlyPossible = isCPOnly || isCPOnlyPossible(n, _rm);

        String datapartitioner = n.getParam(ParamType.DATA_PARTITIONER);
        ExecType REMOTE = OptimizerUtils.isSparkExecutionMode() ? ExecType.SPARK : ExecType.MR;
        PDataPartitioner REMOTE_DP = OptimizerUtils.isSparkExecutionMode() ? PDataPartitioner.REMOTE_SPARK
                : PDataPartitioner.REMOTE_MR;

        //deciding on the execution strategy
        if ((isCPOnly && M <= _rm) //Required: all instruction can be be executed in CP
                || (isCPOnlyPossible && M2 <= _rm)) //Required: cp inst fit into remote JVM mem 
        {
            //at this point all required conditions for REMOTE_MR given, now its an opt decision
            int cpk = (int) Math.min(_lk, Math.floor(_lm / M)); //estimated local exploited par  

            //MR if local par cannot be exploited due to mem constraints (this implies that we work on large data)
            //(the factor of 2 is to account for hyper-threading and in order prevent too eager remote parfor)
            if (2 * cpk < _lk && 2 * cpk < _N && 2 * cpk < _rk) {
                n.setExecType(REMOTE); //remote parfor
            }
            //MR if problem is large enough and remote parallelism is larger than local   
            else if (_lk < _N && _lk < _rk && isLargeProblem(n, M0)) {
                n.setExecType(REMOTE); //remote parfor
            }
            //MR if MR operations in local, but CP only in remote (less overall MR jobs)
            else if ((!isCPOnly) && isCPOnlyPossible) {
                n.setExecType(REMOTE); //remote parfor
            }
            //MR if necessary for LIX rewrite (LIX true iff cp only and rm valid)
            else if (flagLIX) {
                n.setExecType(REMOTE); //remote parfor
            }
            //MR if remote data partitioning, because data will be distributed on all nodes 
            else if (datapartitioner != null && datapartitioner.equals(REMOTE_DP.toString())
                    && !InfrastructureAnalyzer.isLocalMode()) {
                n.setExecType(REMOTE); //remote parfor
            }
            //otherwise CP
            else {
                n.setExecType(ExecType.CP); //local parfor   
            }
        } else //mr instructions in body, or rm too small
        {
            n.setExecType(ExecType.CP); //local parfor
        }

        //actual programblock modification
        long id = n.getID();
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(id)[1];

        PExecMode mode = n.getExecType().toParForExecMode();
        pfpb.setExecMode(mode);

        //decide if recompilation according to remote mem budget necessary
        boolean requiresRecompile = ((mode == PExecMode.REMOTE_MR || mode == PExecMode.REMOTE_SPARK) && !isCPOnly);

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set execution strategy' - result=" + mode + " (recompile="
                + requiresRecompile + ")");

        return requiresRecompile;
    }

    /**
     * 
     * @param pn
     * @return
     */
    protected boolean isLargeProblem(OptNode pn, double M0) {
        return ((_N >= PROB_SIZE_THRESHOLD_REMOTE || _Nmax >= 10 * PROB_SIZE_THRESHOLD_REMOTE)
                && M0 > PROB_SIZE_THRESHOLD_MB); //original operations at least larger than 256MB
    }

    /**
     * 
     * @param n
     * @param memBudget
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean isCPOnlyPossible(OptNode n, double memBudget) throws DMLRuntimeException {
        ExecType et = n.getExecType();
        boolean ret = (et == ExecType.CP);

        if (n.isLeaf() && (et == ExecType.MR || et == ExecType.SPARK)) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            if (h.getForcedExecType() != LopProperties.ExecType.MR //e.g., -exec=hadoop
                    && h.getForcedExecType() != LopProperties.ExecType.SPARK) {
                double mem = _cost.getLeafNodeEstimate(TestMeasure.MEMORY_USAGE, n, LopProperties.ExecType.CP);
                if (mem <= memBudget)
                    ret = true;
            }
        }

        if (!n.isLeaf())
            for (OptNode c : n.getChilds()) {
                if (!ret)
                    break; //early abort if already false
                ret &= isCPOnlyPossible(c, memBudget);
            }
        return ret;
    }

    ///////
    //REWRITE set operations exec type
    ///

    /**
     * 
     * @param pn
     * @param recompile
     * @throws DMLRuntimeException
     */
    protected void rewriteSetOperationsExecType(OptNode pn, boolean recompile) throws DMLRuntimeException {
        //set exec type in internal opt tree
        int count = setOperationExecType(pn, ExecType.CP);

        //recompile program (actual programblock modification)
        if (recompile && count <= 0)
            LOG.warn("OPT: Forced set operations exec type 'CP', but no operation requires recompile.");
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(pn.getID())[1];
        HashSet<String> fnStack = new HashSet<String>();
        Recompiler.recompileProgramBlockHierarchy2Forced(pfpb.getChildBlocks(), 0, fnStack,
                LopProperties.ExecType.CP);

        //debug output
        LOG.debug(getOptMode() + " OPT: rewrite 'set operation exec type CP' - result=" + count);
    }

    /**
     * 
     * @param n
     * @param et
     * @return
     */
    protected int setOperationExecType(OptNode n, ExecType et) {
        int count = 0;

        //set operation exec type to CP, count num recompiles
        if (n.getExecType() != ExecType.CP && n.getNodeType() == NodeType.HOP) {
            n.setExecType(ExecType.CP);
            count = 1;
        }

        //recursively set exec type of childs
        if (!n.isLeaf())
            for (OptNode c : n.getChilds())
                count += setOperationExecType(c, et);

        return count;
    }

    ///////
    //REWRITE enable data colocation
    ///

    /**
     * NOTE: if MAX_REPLICATION_FACTOR_PARTITIONING is set larger than 10, co-location may
     * throw warnings per split since this exceeds "max block locations"
     * 
     * @param n
     * @throws DMLRuntimeException 
     */
    protected void rewriteDataColocation(OptNode n, LocalVariableMap vars) throws DMLRuntimeException {
        // data colocation is beneficial if we have dp=REMOTE_MR, etype=REMOTE_MR
        // and there is at least one direct col-/row-wise access with the index variable
        // on the partitioned matrix
        boolean apply = false;
        String varname = null;
        String partitioner = n.getParam(ParamType.DATA_PARTITIONER);
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(n.getID())[1];

        if (partitioner != null && partitioner.equals(PDataPartitioner.REMOTE_MR.toString())
                && n.getExecType() == ExecType.MR) {
            //find all candidates matrices (at least one partitioned access via iterVar)
            HashSet<String> cand = new HashSet<String>();
            rFindDataColocationCandidates(n, cand, pfpb.getIterablePredicateVars()[0]);

            //select largest matrix for colocation (based on nnz to account for sparsity)
            long nnzMax = Long.MIN_VALUE;
            for (String c : cand) {
                MatrixObject tmp = (MatrixObject) vars.get(c);
                if (tmp != null) {
                    long nnzTmp = tmp.getNnz();
                    if (nnzTmp > nnzMax) {
                        nnzMax = nnzTmp;
                        varname = c;
                        apply = true;
                    }
                }
            }
        }

        //modify the runtime plan (apply true if at least one candidate)
        if (apply)
            pfpb.enableColocatedPartitionedMatrix(varname);

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'enable data colocation' - result=" + apply
                + ((apply) ? " (" + varname + ")" : ""));
    }

    /**
     * 
     * @param n
     * @param cand
     * @param iterVarname
     * @return
     * @throws DMLRuntimeException
     */
    protected void rFindDataColocationCandidates(OptNode n, HashSet<String> cand, String iterVarname)
            throws DMLRuntimeException {
        if (!n.isLeaf()) {
            for (OptNode cn : n.getChilds())
                rFindDataColocationCandidates(cn, cand, iterVarname);
        } else if (n.getNodeType() == NodeType.HOP && n.getParam(ParamType.OPSTRING).equals(IndexingOp.OPSTRING)
                && n.getParam(ParamType.DATA_PARTITION_FORMAT) != null) {
            PDataPartitionFormat dpf = PDataPartitionFormat.valueOf(n.getParam(ParamType.DATA_PARTITION_FORMAT));
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            String inMatrix = h.getInput().get(0).getName();
            String indexAccess = null;
            switch (dpf) {
            case ROW_WISE: //input 1 and 2 eq
                if (h.getInput().get(1) instanceof DataOp)
                    indexAccess = h.getInput().get(1).getName();
                break;
            case COLUMN_WISE: //input 3 and 4 eq
                if (h.getInput().get(3) instanceof DataOp)
                    indexAccess = h.getInput().get(3).getName();
                break;
            default:
                //do nothing
            }

            if (indexAccess != null && indexAccess.equals(iterVarname))
                cand.add(inMatrix);
        }
    }

    ///////
    //REWRITE set partition replication factor
    ///

    /**
     * Increasing the partition replication factor is beneficial if partitions are
     * read multiple times (e.g., in nested loops) because partitioning (done once)
     * gets slightly slower but there is a higher probability for local access
     * 
     * NOTE: this rewrite requires 'set data partitioner' to be executed in order to
     * leverage the partitioning information in the plan tree. 
     *  
     * @param n
     * @throws DMLRuntimeException 
     */
    protected void rewriteSetPartitionReplicationFactor(OptNode n,
            HashMap<String, PDataPartitionFormat> partitionedMatrices, LocalVariableMap vars)
            throws DMLRuntimeException {
        boolean apply = false;
        double sizeReplicated = 0;
        int replication = ParForProgramBlock.WRITE_REPLICATION_FACTOR;

        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(n.getID())[1];

        if (n.getExecType() == ExecType.MR
                && n.getParam(ParamType.DATA_PARTITIONER).equals(PDataPartitioner.REMOTE_MR.toString())
                && n.hasNestedParallelism(false) && n.hasNestedPartitionReads(false)) {
            apply = true;

            //account for problem and cluster constraints
            replication = (int) Math.min(_N, _rnk);

            //account for internal max constraint (note hadoop will warn if max > 10)
            replication = (int) Math.min(replication, MAX_REPLICATION_FACTOR_EXPORT);

            //account for remaining hdfs capacity
            try {
                FileSystem fs = FileSystem.get(ConfigurationManager.getCachedJobConf());
                long hdfsCapacityRemain = fs.getStatus().getRemaining();
                long sizeInputs = 0; //sum of all input sizes (w/o replication)
                for (String var : partitionedMatrices.keySet()) {
                    MatrixObject mo = (MatrixObject) vars.get(var);
                    Path fname = new Path(mo.getFileName());
                    if (fs.exists(fname)) //non-existing (e.g., CP) -> small file
                        sizeInputs += fs.getContentSummary(fname).getLength();
                }
                replication = (int) Math.min(replication, Math.floor(0.9 * hdfsCapacityRemain / sizeInputs));

                //ensure at least replication 1
                replication = Math.max(replication, ParForProgramBlock.WRITE_REPLICATION_FACTOR);
                sizeReplicated = replication * sizeInputs;
            } catch (Exception ex) {
                throw new DMLRuntimeException("Failed to analyze remaining hdfs capacity.", ex);
            }
        }

        //modify the runtime plan 
        if (apply)
            pfpb.setPartitionReplicationFactor(replication);

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set partition replication factor' - result=" + apply
                + ((apply) ? " (" + replication + ", " + toMB(sizeReplicated) + ")" : ""));
    }

    ///////
    //REWRITE set export replication factor
    ///

    /**
     * Increasing the export replication factor is beneficial for remote execution
     * because each task will read the full input data set. This only applies to
     * matrices that are created as in-memory objects before parfor execution. 
     * 
     * NOTE: this rewrite requires 'set execution strategy' to be executed. 
     *  
     * @param n
     * @param partitionedMatrices 
     * @throws DMLRuntimeException 
     */
    protected void rewriteSetExportReplicationFactor(OptNode n, LocalVariableMap vars) throws DMLRuntimeException {
        boolean apply = false;
        int replication = -1;

        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(n.getID())[1];

        //decide on the replication factor 
        if (n.getExecType() == ExecType.MR || n.getExecType() == ExecType.SPARK) {
            apply = true;

            //account for problem and cluster constraints
            replication = (int) Math.min(_N, _rnk);

            //account for internal max constraint (note hadoop will warn if max > 10)
            replication = (int) Math.min(replication, MAX_REPLICATION_FACTOR_EXPORT);
        }

        //modify the runtime plan 
        if (apply)
            pfpb.setExportReplicationFactor(replication);

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set export replication factor' - result=" + apply
                + ((apply) ? " (" + replication + ")" : ""));
    }

    ///////
    //REWRITE enable nested parallelism
    ///

    /**
     * 
     * @param n
     * @param M
     * @return
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     */
    @SuppressWarnings("all")
    protected boolean rewriteNestedParallelism(OptNode n, double M, boolean flagLIX)
            throws DMLRuntimeException, DMLUnsupportedOperationException {
        boolean nested = false;

        if (APPLY_REWRITE_NESTED_PARALLELISM && !flagLIX // if not applied left indexing rewrite   
                && _N >= _rnk // at least exploit all nodes
                && !n.hasNestedParallelism(false)// only for 1D problems, otherwise potentially bad load balance
                && M * _lkmaxCP <= _rm) // only if we can exploit full local parallelism in the map task JVM memory
        {
            //modify tree
            ArrayList<OptNode> tmpOld = n.getChilds();
            OptNode nest = new OptNode(NodeType.PARFOR, ExecType.CP);
            ArrayList<OptNode> tmpNew = new ArrayList<OptNode>();
            tmpNew.add(nest);
            n.setChilds(tmpNew);
            nest.setChilds(tmpOld);

            //modify rtprog
            long id = n.getID();
            ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                    .getMappedProg(id)[1];
            ArrayList<ProgramBlock> tmpPBOld = pfpb.getChildBlocks();

            //create new program block structure and modify parameters (from, to, incr, types,)
            String[] iterVars = pfpb.getIterablePredicateVars(); //from, to stay original
            String[] iterVars2 = iterVars.clone(); //itervar, incr stay original
            int outIncr = (int) Math.ceil(((double) _N) / _rnk);
            iterVars[0] = ParForStatementBlock.INTERAL_FN_INDEX_ROW; // already checked for uniqueness in ParForStatementBlock
            iterVars[3] = String.valueOf(outIncr);
            iterVars2[1] = ParForStatementBlock.INTERAL_FN_INDEX_ROW; //sub start
            iterVars2[2] = null;
            HashMap<String, String> params = pfpb.getParForParams();
            HashMap<String, String> params2 = (HashMap<String, String>) params.clone();
            ParForProgramBlock pfpb2 = new ParForProgramBlock(pfpb.getProgram(), iterVars2, params2);
            OptTreeConverter.getAbstractPlanMapping().putProgMapping(null, pfpb2, nest);

            ArrayList<ProgramBlock> tmpPBNew = new ArrayList<ProgramBlock>();
            tmpPBNew.add(pfpb2);
            pfpb.setChildBlocks(tmpPBNew);
            pfpb.setIterablePredicateVars(iterVars);
            pfpb.setIncrementInstructions(new ArrayList<Instruction>());
            pfpb.setExecMode(PExecMode.REMOTE_MR);
            pfpb2.setChildBlocks(tmpPBOld);
            pfpb2.setResultVariables(pfpb.getResultVariables());
            pfpb2.setFromInstructions(new ArrayList<Instruction>());
            pfpb2.setToInstructions(ProgramRecompiler.createNestedParallelismToInstructionSet(
                    ParForStatementBlock.INTERAL_FN_INDEX_ROW, String.valueOf(outIncr - 1)));
            pfpb2.setIncrementInstructions(new ArrayList<Instruction>());
            pfpb2.setExecMode(PExecMode.LOCAL);

            nested = true;
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'enable nested parallelism' - result=" + nested);

        return nested;
    }

    ///////
    //REWRITE set degree of parallelism
    ///

    /**
     * 
     * @param n
     * @param M
     * @param kMax
     * @param mMax  (per node)
     * @param nested
     * @throws DMLRuntimeException 
     */
    protected void rewriteSetDegreeOfParallelism(OptNode n, double M, boolean flagNested)
            throws DMLRuntimeException {
        ExecType type = n.getExecType();
        long id = n.getID();

        //special handling for different exec models (CP, MR, MR nested)
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(id)[1];

        if (type == ExecType.CP) {
            //determine local max parallelism constraint
            int kMax = -1;
            if (n.isCPOnly())
                kMax = _lkmaxCP;
            else
                kMax = _lkmaxMR;

            //ensure local memory constraint (for spark more conservative in order to 
            //prevent unnecessary guarded collect)
            double mem = OptimizerUtils.isSparkExecutionMode() ? _lm / 2 : _lm;
            kMax = Math.min(kMax, (int) Math.floor(mem / M));
            kMax = Math.max(kMax, 1);

            //constrain max parfor parallelism by problem size
            int parforK = (int) ((_N < kMax) ? _N : kMax);

            //set parfor degree of parallelism
            pfpb.setDegreeOfParallelism(parforK);
            n.setK(parforK);

            //distribute remaining parallelism 
            int remainParforK = (int) Math.ceil(((double) (kMax - parforK + 1)) / parforK);
            int remainOpsK = Math.max(_lkmaxCP / parforK, 1);
            rAssignRemainingParallelism(n, remainParforK, remainOpsK);
        } else // ExecType.MR/ExecType.SPARK
        {
            int kMax = -1;
            if (flagNested) {
                //determine remote max parallelism constraint
                pfpb.setDegreeOfParallelism(_rnk); //guaranteed <= _N (see nested)
                n.setK(_rnk);

                kMax = _rkmax / _rnk; //per node (CP only inside)
            } else //not nested (default)
            {
                //determine remote max parallelism constraint
                int tmpK = (int) ((_N < _rk) ? _N : _rk);
                pfpb.setDegreeOfParallelism(tmpK);
                n.setK(tmpK);

                kMax = _rkmax / tmpK; //per node (CP only inside)
            }

            //ensure remote memory constraint
            kMax = Math.min(kMax, (int) Math.floor(_rm / M)); //guaranteed >= 1 (see exec strategy)
            if (kMax < 1)
                kMax = 1;

            //disable nested parallelism, if required
            if (!ALLOW_REMOTE_NESTED_PARALLELISM)
                kMax = 1;

            //distribute remaining parallelism and recompile parallel instructions
            rAssignRemainingParallelism(n, kMax, 1);
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set degree of parallelism' - result=(see EXPLAIN)");
    }

    /**
     * 
     * @param n
     * @param par
     * @throws DMLRuntimeException 
     */
    protected void rAssignRemainingParallelism(OptNode n, int parforK, int opsK) throws DMLRuntimeException {
        ArrayList<OptNode> childs = n.getChilds();
        if (childs != null) {
            boolean recompileSB = false;
            for (OptNode c : childs) {
                //NOTE: we cannot shortcut with c.setSerialParFor() on par=1 because
                //this would miss to recompile multi-threaded hop operations

                if (c.getNodeType() == NodeType.PARFOR) {
                    //constrain max parfor parallelism by problem size
                    int tmpN = Integer.parseInt(c.getParam(ParamType.NUM_ITERATIONS));
                    int tmpK = (tmpN < parforK) ? tmpN : parforK;

                    //set parfor degree of parallelism
                    long id = c.getID();
                    c.setK(tmpK);
                    ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                            .getMappedProg(id)[1];
                    pfpb.setDegreeOfParallelism(tmpK);

                    //distribute remaining parallelism 
                    int remainParforK = (int) Math.ceil(((double) (parforK - tmpK + 1)) / tmpK);
                    int remainOpsK = Math.max(opsK / tmpK, 1);
                    rAssignRemainingParallelism(c, remainParforK, remainOpsK);
                } else if (c.getNodeType() == NodeType.HOP) {
                    //set degree of parallelism for multi-threaded leaf nodes
                    Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(c.getID());
                    if (OptimizerUtils.PARALLEL_CP_MATRIX_MULTIPLY && h instanceof MultiThreadedHop) //abop, datagenop, qop
                    {
                        MultiThreadedHop mhop = (MultiThreadedHop) h;
                        mhop.setMaxNumThreads(opsK); //set max constraint in hop
                        c.setK(opsK); //set optnode k (for explain)
                        //need to recompile SB, if changed constraint
                        recompileSB = true;
                    }
                } else
                    rAssignRemainingParallelism(c, parforK, opsK);
            }

            //recompile statement block if required
            if (recompileSB) {
                try {
                    //guaranteed to be a last-level block (see hop change)
                    ProgramBlock pb = (ProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                            .getMappedProg(n.getID())[1];
                    Recompiler.recompileProgramBlockInstructions(pb);
                } catch (Exception ex) {
                    throw new DMLRuntimeException(ex);
                }
            }
        }
    }

    ///////
    //REWRITE set task partitioner
    ///

    /**
     * 
     * @param n
     * @param partitioner
     */
    protected void rewriteSetTaskPartitioner(OptNode pn, boolean flagNested, boolean flagLIX) {
        //assertions (warnings of corrupt optimizer decisions)
        if (pn.getNodeType() != NodeType.PARFOR)
            LOG.warn(getOptMode() + " OPT: Task partitioner can only be set for a ParFor node.");
        if (flagNested && flagLIX)
            LOG.warn(getOptMode()
                    + " OPT: Task partitioner decision has conflicting input from rewrites 'nested parallelism' and 'result partitioning'.");

        boolean jvmreuse = ConfigurationManager.getConfig().getBooleanValue(DMLConfig.JVM_REUSE);

        //set task partitioner
        if (flagNested) {
            setTaskPartitioner(pn, PTaskPartitioner.STATIC);
            setTaskPartitioner(pn.getChilds().get(0), PTaskPartitioner.FACTORING);
        } else if (flagLIX) {
            setTaskPartitioner(pn, PTaskPartitioner.FACTORING_CMAX);
        } else if (pn.getExecType() == ExecType.MR && !jvmreuse && pn.hasOnlySimpleChilds()) {
            //for simple body programs without loops, branches, or function calls, we don't
            //expect much load imbalance and hence use static partitioning in order to
            //(1) reduce task latency, (2) prevent repeated read (w/o jvm reuse), and (3)
            //preaggregate results (less write / less read by result merge)
            setTaskPartitioner(pn, PTaskPartitioner.STATIC);
        } else if (_N / 4 >= pn.getK()) //to prevent imbalance due to ceiling
        {
            setTaskPartitioner(pn, PTaskPartitioner.FACTORING);
        } else {
            setTaskPartitioner(pn, PTaskPartitioner.NAIVE);
        }
    }

    /**
     * 
     * @param n
     * @param partitioner
     * @param flagLIX
     */
    protected void setTaskPartitioner(OptNode n, PTaskPartitioner partitioner) {
        long id = n.getID();

        // modify rtprog
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(id)[1];
        pfpb.setTaskPartitioner(partitioner);

        // modify plan
        n.addParam(ParamType.TASK_PARTITIONER, partitioner.toString());

        //handle specific case of LIX recompile
        boolean flagLIX = (partitioner == PTaskPartitioner.FACTORING_CMAX);
        if (flagLIX) {
            long maxc = n.getMaxC(_N);
            pfpb.setTaskSize(maxc); //used as constraint 
            pfpb.disableJVMReuse();
            n.addParam(ParamType.TASK_SIZE, String.valueOf(maxc));
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set task partitioner' - result=" + partitioner
                + ((flagLIX) ? "," + n.getParam(ParamType.TASK_SIZE) : ""));
    }

    ///////
    //REWRITE set fused data partitioning / execution
    ///

    /**
     * This dedicated execution mode can only be applied if all of the 
     * following conditions are true:
     * - Only cp instructions in the parfor body
     * - Only one partitioned input 
     * - number of iterations is equal to number of partitions (nrow/ncol)
     * - partitioned matrix access via plain iteration variables (no composed expressions)
     *   (this ensures that each partition is exactly read once)
     * - no left indexing (since by default static task partitioning)
     * 
     * Furthermore, it should be only chosen if we already decided for remote partitioning
     * and otherwise would create a large number of partition files.
     * 
     * NOTE: We already respect the reducer memory budget for plan correctness. However,
     * we miss optimization potential if the reducer budget is larger than the mapper budget
     * (if we were not able to select REMOTE_MR as execution strategy wrt mapper budget)
     * TODO modify 'set exec strategy' and related rewrites for conditional data partitioning.
     * 
     * 
     * @param M 
     * @param partitionedMatrices, ExecutionContext ec 
     * 
     * @param n
     * @param partitioner
     * @throws DMLRuntimeException 
     */
    protected void rewriteSetFusedDataPartitioningExecution(OptNode pn, double M, boolean flagLIX,
            HashMap<String, PDataPartitionFormat> partitionedMatrices, LocalVariableMap vars)
            throws DMLRuntimeException {
        //assertions (warnings of corrupt optimizer decisions)
        if (pn.getNodeType() != NodeType.PARFOR)
            LOG.warn(getOptMode()
                    + " OPT: Fused data partitioning and execution is only applicable for a ParFor node.");

        boolean apply = false;
        String partitioner = pn.getParam(ParamType.DATA_PARTITIONER);
        PDataPartitioner REMOTE_DP = OptimizerUtils.isSparkExecutionMode() ? PDataPartitioner.REMOTE_SPARK
                : PDataPartitioner.REMOTE_MR;
        PExecMode REMOTE_DPE = OptimizerUtils.isSparkExecutionMode() ? PExecMode.REMOTE_SPARK_DP
                : PExecMode.REMOTE_MR_DP;

        //precondition: rewrite only invoked if exec type MR 
        // (this also implies that the body is CP only)

        // try to merge MR data partitioning and MR exec 
        if ((pn.getExecType() == ExecType.MR || pn.getExecType() == ExecType.SPARK) //MR/SP EXEC and CP body
                && M < _rm2 //fits into remote memory of reducers   
                && partitioner != null && partitioner.equals(REMOTE_DP.toString()) //MR/SP partitioning
                && partitionedMatrices.size() == 1) //only one partitioned matrix
        {
            ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                    .getMappedProg(pn.getID())[1];

            //partitioned matrix
            String moVarname = partitionedMatrices.keySet().iterator().next();
            PDataPartitionFormat moDpf = partitionedMatrices.get(moVarname);
            MatrixObject mo = (MatrixObject) vars.get(moVarname);

            //check if access via iteration variable and sizes match
            String iterVarname = pfpb.getIterablePredicateVars()[0];

            if (rIsAccessByIterationVariable(pn, moVarname, iterVarname)
                    && ((moDpf == PDataPartitionFormat.ROW_WISE && mo.getNumRows() == _N)
                            || (moDpf == PDataPartitionFormat.COLUMN_WISE && mo.getNumColumns() == _N))) {
                int k = (int) Math.min(_N, _rk2);

                pn.addParam(ParamType.DATA_PARTITIONER, REMOTE_DPE.toString() + "(fused)");
                pn.setK(k);

                pfpb.setExecMode(REMOTE_DPE); //set fused exec type   
                pfpb.setDataPartitioner(PDataPartitioner.NONE);
                pfpb.enableColocatedPartitionedMatrix(moVarname);
                pfpb.setDegreeOfParallelism(k);

                apply = true;
            }
        }

        LOG.debug(getOptMode() + " OPT: rewrite 'set fused data partitioning and execution' - result=" + apply);
    }

    /**
     * 
     * @param n
     * @param iterVarname
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean rIsAccessByIterationVariable(OptNode n, String varName, String iterVarname)
            throws DMLRuntimeException {
        boolean ret = true;

        if (!n.isLeaf()) {
            for (OptNode cn : n.getChilds())
                rIsAccessByIterationVariable(cn, varName, iterVarname);
        } else if (n.getNodeType() == NodeType.HOP && n.getParam(ParamType.OPSTRING).equals(IndexingOp.OPSTRING)
                && n.getParam(ParamType.DATA_PARTITION_FORMAT) != null) {
            PDataPartitionFormat dpf = PDataPartitionFormat.valueOf(n.getParam(ParamType.DATA_PARTITION_FORMAT));
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            String inMatrix = h.getInput().get(0).getName();
            String indexAccess = null;
            switch (dpf) {
            case ROW_WISE: //input 1 and 2 eq
                if (h.getInput().get(1) instanceof DataOp)
                    indexAccess = h.getInput().get(1).getName();
                break;
            case COLUMN_WISE: //input 3 and 4 eq
                if (h.getInput().get(3) instanceof DataOp)
                    indexAccess = h.getInput().get(3).getName();
                break;

            default:
                //do nothing
            }

            ret &= ((inMatrix != null && inMatrix.equals(varName))
                    && (indexAccess != null && indexAccess.equals(iterVarname)));
        }

        return ret;
    }

    ///////
    //REWRITE transpose sparse vector operations
    ///

    protected void rewriteSetTranposeSparseVectorOperations(OptNode pn,
            HashMap<String, PDataPartitionFormat> partitionedMatrices, LocalVariableMap vars)
            throws DMLRuntimeException {
        //assertions (warnings of corrupt optimizer decisions)
        if (pn.getNodeType() != NodeType.PARFOR)
            LOG.warn(getOptMode()
                    + " OPT: Transpose sparse vector operations is only applicable for a ParFor node.");

        boolean apply = false;

        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(pn.getID())[1];

        if (pfpb.getExecMode() == PExecMode.REMOTE_MR_DP && partitionedMatrices.size() == 1) //general applicable
        {
            String moVarname = partitionedMatrices.keySet().iterator().next();
            PDataPartitionFormat moDpf = partitionedMatrices.get(moVarname);
            Data dat = vars.get(moVarname);

            if (dat != null && dat instanceof MatrixObject && moDpf == PDataPartitionFormat.COLUMN_WISE
                    && ((MatrixObject) dat).getSparsity() <= MatrixBlock.SPARSITY_TURN_POINT //check for sparse matrix
                    && rIsTransposeSafePartition(pn, moVarname)) //tranpose-safe
            {
                pfpb.setTransposeSparseColumnVector(true);

                apply = true;
            }
        }

        LOG.debug(getOptMode() + " OPT: rewrite 'set transpose sparse vector operations' - result=" + apply);
    }

    /**
     * 
     * @param n
     * @param iterVarname
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean rIsTransposeSafePartition(OptNode n, String varName) throws DMLRuntimeException {
        boolean ret = true;

        if (!n.isLeaf()) {
            for (OptNode cn : n.getChilds())
                rIsTransposeSafePartition(cn, varName);
        } else if (n.getNodeType() == NodeType.HOP && n.getParam(ParamType.OPSTRING).equals(IndexingOp.OPSTRING)
                && n.getParam(ParamType.DATA_PARTITION_FORMAT) != null) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());

            String inMatrix = h.getInput().get(0).getName();
            if (inMatrix.equals(varName)) {
                //check that all parents are transpose-safe operations
                //(even a transient write would not be safe due to indirection into other DAGs)         
                ArrayList<Hop> parent = h.getParent();
                for (Hop p : parent)
                    ret &= p.isTransposeSafe();
            }
        }

        return ret;
    }

    ///////
    //REWRITE set in-place result indexing
    ///

    /**
     * 
     * @param pn
     * @param M
     * @param vars
     * @param inPlaceResultVars
     * @throws DMLRuntimeException
     */
    protected void rewriteSetInPlaceResultIndexing(OptNode pn, double M, LocalVariableMap vars,
            HashSet<String> inPlaceResultVars) throws DMLRuntimeException {
        //assertions (warnings of corrupt optimizer decisions)
        if (pn.getNodeType() != NodeType.PARFOR)
            LOG.warn(getOptMode() + " OPT: Set in-place result update is only applicable for a ParFor node.");

        boolean apply = false;

        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(pn.getID())[1];

        //note currently we decide for all result vars jointly, i.e.,
        //only if all fit pinned in remaining budget, we apply this rewrite.

        ArrayList<String> retVars = pfpb.getResultVariables();

        //compute total sum of pinned result variable memory
        double sum = computeTotalSizeResultVariables(retVars, vars, pfpb.getDegreeOfParallelism());

        //NOTE: currently this rule is too conservative (the result variable is assumed to be dense and
        //most importantly counted twice if this is part of the maximum operation)
        double totalMem = Math.max((M + sum), rComputeSumMemoryIntermediates(pn, new HashSet<String>()));

        //optimization decision
        if (rHasOnlyInPlaceSafeLeftIndexing(pn, retVars)) //basic correctness constraint
        {
            //result update in-place for MR/Spark (w/ remote memory constraint)
            if ((pfpb.getExecMode() == PExecMode.REMOTE_MR_DP || pfpb.getExecMode() == PExecMode.REMOTE_MR
                    || pfpb.getExecMode() == PExecMode.REMOTE_SPARK_DP
                    || pfpb.getExecMode() == PExecMode.REMOTE_SPARK) && totalMem < _rm) {
                apply = true;
            }
            //result update in-place for CP (w/ local memory constraint)
            else if (pfpb.getExecMode() == PExecMode.LOCAL && totalMem * pfpb.getDegreeOfParallelism() < _lm
                    && pn.isCPOnly()) //no forced mr/spark execution  
            {
                apply = true;
            }
        }

        //modify result variable meta data, if rewrite applied
        if (apply) {
            //add result vars to result and set state
            //will be serialized and transfered via symbol table 
            for (String var : retVars) {
                Data dat = vars.get(var);
                if (dat instanceof MatrixObject)
                    ((MatrixObject) dat).enableUpdateInPlace(true);
            }
            inPlaceResultVars.addAll(retVars);
        }

        LOG.debug(getOptMode() + " OPT: rewrite 'set in-place result indexing' - result=" + apply + " ("
                + ProgramConverter.serializeStringCollection(inPlaceResultVars) + ", M=" + toMB(totalMem) + ")");
    }

    /**
     * 
     * @param n
     * @param retVars
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean rHasOnlyInPlaceSafeLeftIndexing(OptNode n, ArrayList<String> retVars)
            throws DMLRuntimeException {
        boolean ret = true;

        if (!n.isLeaf()) {
            for (OptNode cn : n.getChilds())
                ret &= rHasOnlyInPlaceSafeLeftIndexing(cn, retVars);
        } else if (n.getNodeType() == NodeType.HOP
                && n.getParam(ParamType.OPSTRING).equals(LeftIndexingOp.OPSTRING)) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            if (retVars.contains(h.getInput().get(0).getName())) {
                ret &= (h.getParent().size() == 1
                        && h.getParent().get(0).getName().equals(h.getInput().get(0).getName()));
            }
        }

        return ret;
    }

    /**
     * 
     * @param retVars
     * @param vars
     * @return
     */
    private double computeTotalSizeResultVariables(ArrayList<String> retVars, LocalVariableMap vars, int k) {
        double sum = 1;
        for (String var : retVars) {
            Data dat = vars.get(var);
            if (dat instanceof MatrixObject) {
                MatrixObject mo = (MatrixObject) dat;
                double nnz = mo.getNnz();

                if (nnz == 0.0)
                    sum += OptimizerUtils.estimateSizeExactSparsity(mo.getNumRows(), mo.getNumColumns(), 1.0);
                else {
                    double sp = mo.getSparsity();
                    sum += (k + 1) * (OptimizerUtils.estimateSizeExactSparsity(mo.getNumRows(), mo.getNumColumns(),
                            Math.min((1.0 / k) + sp, 1.0))); // Every worker will consume memory for (MatrixSize/k + nnz) data.
                    // This is applicable only when there is non-zero nnz. 
                }
            }
        }

        return sum;
    }

    ///////
    //REWRITE disable CP caching  
    ///

    /**
     * 
     * @param pn
     * @param inplaceResultVars
     * @param vars
     * @throws DMLRuntimeException
     */
    protected void rewriteDisableCPCaching(OptNode pn, HashSet<String> inplaceResultVars, LocalVariableMap vars)
            throws DMLRuntimeException {
        //assertions (warnings of corrupt optimizer decisions)
        if (pn.getNodeType() != NodeType.PARFOR)
            LOG.warn(getOptMode() + " OPT: Disable caching is only applicable for a ParFor node.");

        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(pn.getID())[1];

        double M_sumInterm = rComputeSumMemoryIntermediates(pn, inplaceResultVars);
        boolean apply = false;

        if ((pfpb.getExecMode() == PExecMode.REMOTE_MR_DP || pfpb.getExecMode() == PExecMode.REMOTE_MR)
                && M_sumInterm < _rm) //all intermediates and operations fit into memory budget
        {
            pfpb.setCPCaching(false); //default is true         
            apply = true;
        }

        LOG.debug(getOptMode() + " OPT: rewrite 'disable CP caching' - result=" + apply + " (M=" + toMB(M_sumInterm)
                + ")");
    }

    /**
     * 
     * @param n
     * @param inplaceResultVars 
     * @return
     * @throws DMLRuntimeException
     */
    protected double rComputeSumMemoryIntermediates(OptNode n, HashSet<String> inplaceResultVars)
            throws DMLRuntimeException {
        double sum = 0;

        if (!n.isLeaf()) {
            for (OptNode cn : n.getChilds())
                sum += rComputeSumMemoryIntermediates(cn, inplaceResultVars);
        } else if (n.getNodeType() == NodeType.HOP) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());

            if (n.getParam(ParamType.OPSTRING).equals(IndexingOp.OPSTRING)
                    && n.getParam(ParamType.DATA_PARTITION_FORMAT) != null) {
                //set during partitioning rewrite
                sum += h.getMemEstimate();
            } else {
                //base intermediate (worst-case w/ materialized intermediates)
                sum += h.getOutputMemEstimate() + h.getIntermediateMemEstimate();

                //inputs not represented in the planopttree (worst-case no CSE)
                if (h.getInput() != null)
                    for (Hop cn : h.getInput())
                        if (cn instanceof DataOp && ((DataOp) cn).isRead() //read data
                                && !inplaceResultVars.contains(cn.getName())) //except in-place result vars
                        {
                            sum += cn.getMemEstimate();
                        }
            }
        }

        return sum;
    }

    ///////
    //REWRITE enable runtime piggybacking
    ///

    /**
     * 
     * @param n
     * @param partitionedMatrices.keySet() 
     * @param vars 
     * @throws DMLRuntimeException
     */
    protected void rewriteEnableRuntimePiggybacking(OptNode n, LocalVariableMap vars,
            HashMap<String, PDataPartitionFormat> partitionedMatrices) throws DMLRuntimeException {
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(n.getID())[1];

        HashSet<String> sharedVars = new HashSet<String>();
        boolean apply = false;

        //enable runtime piggybacking if MR jobs on shared read-only data set
        if (OptimizerUtils.ALLOW_RUNTIME_PIGGYBACKING) {
            //apply runtime piggybacking if hop in mr and shared input variable 
            //(any input variabled which is not partitioned and is read only and applies)
            apply = rHasSharedMRInput(n, vars.keySet(), partitionedMatrices.keySet(), sharedVars)
                    && n.getTotalK() > 1; //apply only if degree of parallelism > 1
        }

        if (apply)
            pfpb.setRuntimePiggybacking(apply);

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'enable runtime piggybacking' - result=" + apply + " ("
                + ProgramConverter.serializeStringCollection(sharedVars) + ")");
    }

    /**
     * 
     * @param n
     * @param inputVars
     * @param partitionedVars
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean rHasSharedMRInput(OptNode n, Set<String> inputVars, Set<String> partitionedVars,
            HashSet<String> sharedVars) throws DMLRuntimeException {
        boolean ret = false;

        if (!n.isLeaf()) {
            for (OptNode cn : n.getChilds())
                ret |= rHasSharedMRInput(cn, inputVars, partitionedVars, sharedVars);
        } else if (n.getNodeType() == NodeType.HOP && n.getExecType() == ExecType.MR) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            for (Hop ch : h.getInput()) {
                //note: we replaxed the contraint of non-partitioned inputs for additional 
                //latecy hiding and scan sharing of partitions which are read multiple times

                if (ch instanceof DataOp && ch.getDataType() == DataType.MATRIX && inputVars.contains(ch.getName()))
                //&& !partitionedVars.contains(ch.getName()))
                {
                    ret = true;
                    sharedVars.add(ch.getName());
                } else if (ch instanceof ReorgOp && ((ReorgOp) ch).getOp() == ReOrgOp.TRANSPOSE
                        && ch.getInput().get(0) instanceof DataOp
                        && ch.getInput().get(0).getDataType() == DataType.MATRIX
                        && inputVars.contains(ch.getInput().get(0).getName()))
                //&& !partitionedVars.contains(ch.getInput().get(0).getName()))
                {
                    ret = true;
                    sharedVars.add(ch.getInput().get(0).getName());
                }
            }
        }

        return ret;
    }

    ///////
    //REWRITE inject spark loop checkpointing
    ///

    /**
     * 
     * @param n
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     */
    protected void rewriteInjectSparkLoopCheckpointing(OptNode n)
            throws DMLRuntimeException, DMLUnsupportedOperationException {
        //get program blocks of root parfor
        Object[] progobj = OptTreeConverter.getAbstractPlanMapping().getMappedProg(n.getID());
        ParForStatementBlock pfsb = (ParForStatementBlock) progobj[0];
        ParForStatement fs = (ParForStatement) pfsb.getStatement(0);
        ParForProgramBlock pfpb = (ParForProgramBlock) progobj[1];

        boolean applied = false;

        try {
            //apply hop rewrite inject spark checkpoints (but without context awareness)
            RewriteInjectSparkLoopCheckpointing rewrite = new RewriteInjectSparkLoopCheckpointing(false);
            ProgramRewriter rewriter = new ProgramRewriter(rewrite);
            ProgramRewriteStatus state = new ProgramRewriteStatus();
            rewriter.rewriteStatementBlockHopDAGs(pfsb, state);
            fs.setBody(rewriter.rewriteStatementBlocks(fs.getBody(), state));

            //recompile if additional checkpoints introduced
            if (state.getInjectedCheckpoints()) {
                pfpb.setChildBlocks(
                        ProgramRecompiler.generatePartitialRuntimeProgram(pfpb.getProgram(), fs.getBody()));
                applied = true;
            }
        } catch (Exception ex) {
            throw new DMLRuntimeException(ex);
        }

        LOG.debug(getOptMode() + " OPT: rewrite 'inject spark loop checkpointing' - result=" + applied);
    }

    ///////
    //REWRITE inject spark repartition for zipmm
    ///

    /**
     * 
     * @param n
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     */
    protected void rewriteInjectSparkRepartition(OptNode n, LocalVariableMap vars)
            throws DMLRuntimeException, DMLUnsupportedOperationException {
        //get program blocks of root parfor
        Object[] progobj = OptTreeConverter.getAbstractPlanMapping().getMappedProg(n.getID());
        ParForStatementBlock pfsb = (ParForStatementBlock) progobj[0];
        ParForProgramBlock pfpb = (ParForProgramBlock) progobj[1];

        ArrayList<String> ret = new ArrayList<String>();

        if (OptimizerUtils.isSparkExecutionMode() //spark exec mode
                && n.getExecType() == ExecType.CP //local parfor 
                && _N > 1) //at least 2 iterations                             
        {
            //collect candidates from zipmm spark instructions
            HashSet<String> cand = new HashSet<String>();
            rCollectZipmmPartitioningCandidates(n, cand);

            //prune updated candidates
            HashSet<String> probe = new HashSet<String>(pfsb.getReadOnlyParentVars());
            for (String var : cand)
                if (probe.contains(var))
                    ret.add(var);

            //prune small candidates
            ArrayList<String> tmp = new ArrayList<String>(ret);
            ret.clear();
            for (String var : tmp)
                if (vars.get(var) instanceof MatrixObject) {
                    MatrixObject mo = (MatrixObject) vars.get(var);
                    double sp = OptimizerUtils.getSparsity(mo.getNumRows(), mo.getNumColumns(), mo.getNnz());
                    double size = OptimizerUtils.estimateSizeExactSparsity(mo.getNumRows(), mo.getNumColumns(), sp);
                    if (size > OptimizerUtils.getLocalMemBudget())
                        ret.add(var);
                }

            //apply rewrite to parfor pb
            if (!ret.isEmpty()) {
                pfpb.setSparkRepartitionVariables(ret);
            }
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'inject spark input repartition' - result=" + ret.size() + " ("
                + ProgramConverter.serializeStringCollection(ret) + ")");
    }

    /**
     * 
     * @param n
     * @param cand
     */
    private void rCollectZipmmPartitioningCandidates(OptNode n, HashSet<String> cand) {
        //collect zipmm inputs
        if (n.getNodeType() == NodeType.HOP) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            if (h instanceof AggBinaryOp && (((AggBinaryOp) h).getMMultMethod() == MMultMethod.ZIPMM
                    || ((AggBinaryOp) h).getMMultMethod() == MMultMethod.CPMM)) {

                if (h.getInput().get(0) instanceof DataOp)
                    cand.add(h.getInput().get(0).getName());
                if (h.getInput().get(1) instanceof DataOp)
                    cand.add(h.getInput().get(1).getName());
            }
        }

        //recursively process childs
        if (!n.isLeaf())
            for (OptNode c : n.getChilds())
                rCollectZipmmPartitioningCandidates(c, cand);
    }

    ///////
    //REWRITE set spark eager rdd caching
    ///

    /**
     * 
     * @param n
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     */
    protected void rewriteSetSparkEagerRDDCaching(OptNode n, LocalVariableMap vars)
            throws DMLRuntimeException, DMLUnsupportedOperationException {
        //get program blocks of root parfor
        Object[] progobj = OptTreeConverter.getAbstractPlanMapping().getMappedProg(n.getID());
        ParForStatementBlock pfsb = (ParForStatementBlock) progobj[0];
        ParForProgramBlock pfpb = (ParForProgramBlock) progobj[1];

        ArrayList<String> ret = new ArrayList<String>();

        if (OptimizerUtils.isSparkExecutionMode() //spark exec mode
                && n.getExecType() == ExecType.CP //local parfor 
                && _N > 1) //at least 2 iterations                             
        {
            Set<String> cand = pfsb.variablesRead().getVariableNames();
            Collection<String> rpVars = pfpb.getSparkRepartitionVariables();
            for (String var : cand) {
                Data dat = vars.get(var);

                if (dat != null && dat instanceof MatrixObject && ((MatrixObject) dat).getRDDHandle() != null) {
                    MatrixObject mo = (MatrixObject) dat;
                    MatrixCharacteristics mc = mo.getMatrixCharacteristics();
                    RDDObject rdd = mo.getRDDHandle();
                    if ((rpVars == null || !rpVars.contains(var)) //not a repartition var
                            && rdd.rHasCheckpointRDDChilds() //is cached rdd 
                            && _lm / n.getK() < //is out-of-core dataset
                            OptimizerUtils.estimateSizeExactSparsity(mc)) {
                        ret.add(var);
                    }
                }
            }

            //apply rewrite to parfor pb
            if (!ret.isEmpty()) {
                pfpb.setSparkEagerCacheVariables(ret);
            }
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set spark eager rdd caching' - result=" + ret.size() + " ("
                + ProgramConverter.serializeStringCollection(ret) + ")");
    }

    ///////
    //REWRITE remove compare matrix (for result merge, needs to be invoked before setting result merge)
    ///

    /**
     *
     * 
     * @param n
     * @throws DMLRuntimeException 
     */
    protected void rewriteRemoveUnnecessaryCompareMatrix(OptNode n, ExecutionContext ec)
            throws DMLRuntimeException {
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(n.getID())[1];

        ArrayList<String> cleanedVars = new ArrayList<String>();
        ArrayList<String> resultVars = pfpb.getResultVariables();
        String itervar = pfpb.getIterablePredicateVars()[0];

        for (String rvar : resultVars) {
            Data dat = ec.getVariable(rvar);
            if (dat instanceof MatrixObject && ((MatrixObject) dat).getNnz() != 0 //subject to result merge with compare
                    && n.hasOnlySimpleChilds() //guaranteed no conditional indexing   
                    && rContainsResultFullReplace(n, rvar, itervar, (MatrixObject) dat) //guaranteed full matrix replace 
                    //&& !pfsb.variablesRead().containsVariable(rvar)                  //never read variable in loop body
                    && !rIsReadInRightIndexing(n, rvar) //never read variable in loop body
                    && ((MatrixObject) dat).getNumRows() <= Integer.MAX_VALUE
                    && ((MatrixObject) dat).getNumColumns() <= Integer.MAX_VALUE) {
                //replace existing matrix object with empty matrix
                MatrixObject mo = (MatrixObject) dat;
                ec.cleanupMatrixObject(mo);
                ec.setMatrixOutput(rvar, new MatrixBlock((int) mo.getNumRows(), (int) mo.getNumColumns(), false));

                //keep track of cleaned result variables
                cleanedVars.add(rvar);
            }
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'remove unnecessary compare matrix' - result="
                + (!cleanedVars.isEmpty()) + " (" + ProgramConverter.serializeStringCollection(cleanedVars) + ")");
    }

    /**
     * 
     * @param n
     * @param resultVar
     * @param iterVarname
     * @param mo
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean rContainsResultFullReplace(OptNode n, String resultVar, String iterVarname, MatrixObject mo)
            throws DMLRuntimeException {
        boolean ret = false;

        //process hop node
        if (n.getNodeType() == NodeType.HOP)
            ret |= isResultFullReplace(n, resultVar, iterVarname, mo);

        //process childs recursively
        if (!n.isLeaf()) {
            for (OptNode c : n.getChilds())
                ret |= rContainsResultFullReplace(c, resultVar, iterVarname, mo);
        }

        return ret;
    }

    /**
     * 
     * @param n
     * @param resultVar
     * @param iterVarname
     * @param mo
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean isResultFullReplace(OptNode n, String resultVar, String iterVarname, MatrixObject mo)
            throws DMLRuntimeException {
        //check left indexing operator
        String opStr = n.getParam(ParamType.OPSTRING);
        if (opStr == null || !opStr.equals(LeftIndexingOp.OPSTRING))
            return false;

        Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
        Hop base = h.getInput().get(0);

        //check result variable
        if (!resultVar.equals(base.getName()))
            return false;

        //check access pattern, memory budget
        Hop inpRowL = h.getInput().get(2);
        Hop inpRowU = h.getInput().get(3);
        Hop inpColL = h.getInput().get(4);
        Hop inpColU = h.getInput().get(5);
        //check for rowwise overwrite
        if ((inpRowL.getName().equals(iterVarname) && inpRowU.getName().equals(iterVarname))
                && inpColL instanceof LiteralOp && HopRewriteUtils.getDoubleValueSafe((LiteralOp) inpColL) == 1
                && inpColU instanceof LiteralOp
                && HopRewriteUtils.getDoubleValueSafe((LiteralOp) inpColU) == mo.getNumColumns()) {
            return true;
        }

        //check for colwise overwrite
        if ((inpColL.getName().equals(iterVarname) && inpColU.getName().equals(iterVarname))
                && inpRowL instanceof LiteralOp && HopRewriteUtils.getDoubleValueSafe((LiteralOp) inpRowL) == 1
                && inpRowU instanceof LiteralOp
                && HopRewriteUtils.getDoubleValueSafe((LiteralOp) inpRowU) == mo.getNumRows()) {
            return true;
        }

        return false;
    }

    /**
     * 
     * @param n
     * @param var
     * @return
     */
    protected boolean rIsReadInRightIndexing(OptNode n, String var) {
        //NOTE: This method checks if a given variables is used in right indexing
        //expressions. This is sufficient for "remove unnecessary compare matrix" because
        //we already checked for full replace, which is only valid if we dont access
        //the entire matrix in any other operation.
        boolean ret = false;

        if (n.getNodeType() == NodeType.HOP) {
            Hop h = OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
            if (h instanceof IndexingOp && h.getInput().get(0) instanceof DataOp
                    && h.getInput().get(0).getName().equals(var)) {
                ret |= true;
            }
        }

        //process childs recursively
        if (!n.isLeaf())
            for (OptNode c : n.getChilds())
                ret |= rIsReadInRightIndexing(c, var);

        return ret;
    }

    ///////
    //REWRITE set result merge
    ///

    /**
     *
     * 
     * @param n
     * @throws DMLRuntimeException 
     */
    protected void rewriteSetResultMerge(OptNode n, LocalVariableMap vars, boolean inLocal)
            throws DMLRuntimeException {
        ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                .getMappedProg(n.getID())[1];

        PResultMerge REMOTE = OptimizerUtils.isSparkExecutionMode() ? PResultMerge.REMOTE_SPARK
                : PResultMerge.REMOTE_MR;
        PResultMerge ret = null;

        //investigate details of current parfor node
        boolean flagRemoteParFOR = (n.getExecType() == ExecType.MR || n.getExecType() == ExecType.SPARK);
        boolean flagLargeResult = hasLargeTotalResults(n, pfpb.getResultVariables(), vars, true);
        boolean flagRemoteLeftIndexing = hasResultMRLeftIndexing(n, pfpb.getResultVariables(), vars, true);
        boolean flagCellFormatWoCompare = determineFlagCellFormatWoCompare(pfpb.getResultVariables(), vars);
        boolean flagOnlyInMemResults = hasOnlyInMemoryResults(n, pfpb.getResultVariables(), vars, true);

        //optimimality decision on result merge
        //MR, if remote exec, and w/compare (prevent huge transfer/merge costs)
        if (flagRemoteParFOR && flagLargeResult) {
            ret = REMOTE;
        }
        //CP, if all results in mem   
        else if (flagOnlyInMemResults) {
            ret = PResultMerge.LOCAL_MEM;
        }
        //MR, if result partitioning and copy not possible
        //NOTE: 'at least one' instead of 'all' condition of flagMRLeftIndexing because the 
        //      benefit for large matrices outweigths potentially unnecessary MR jobs for smaller matrices)
        else if ((flagRemoteParFOR || flagRemoteLeftIndexing)
                && !(flagCellFormatWoCompare && ResultMergeLocalFile.ALLOW_COPY_CELLFILES)) {
            ret = REMOTE;
        }
        //CP, otherwise (decide later if in mem or file-based)
        else {
            ret = PResultMerge.LOCAL_AUTOMATIC;
        }

        // modify rtprog   
        pfpb.setResultMerge(ret);

        // modify plan
        n.addParam(ParamType.RESULT_MERGE, ret.toString());

        //recursively apply rewrite for parfor nodes
        if (n.getChilds() != null)
            rInvokeSetResultMerge(n.getChilds(), vars, inLocal && !flagRemoteParFOR);

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set result merge' - result=" + ret);
    }

    /**
     * 
     * @param resultVars
     * @param vars
     * @return
     */
    protected boolean determineFlagCellFormatWoCompare(ArrayList<String> resultVars, LocalVariableMap vars) {
        boolean ret = true;

        for (String rVar : resultVars) {
            Data dat = vars.get(rVar);
            if (dat == null || !(dat instanceof MatrixObject)) {
                ret = false;
                break;
            } else {
                MatrixObject mo = (MatrixObject) dat;
                MatrixFormatMetaData meta = (MatrixFormatMetaData) mo.getMetaData();
                OutputInfo oi = meta.getOutputInfo();
                long nnz = meta.getMatrixCharacteristics().getNonZeros();

                if (oi == OutputInfo.BinaryBlockOutputInfo || nnz != 0) {
                    ret = false;
                    break;
                }
            }
        }

        return ret;
    }

    /**
     * 
     * @param n
     * @param resultVars
     * @return
     * @throws DMLRuntimeException 
     */
    protected boolean hasResultMRLeftIndexing(OptNode n, ArrayList<String> resultVars, LocalVariableMap vars,
            boolean checkSize) throws DMLRuntimeException {
        boolean ret = false;

        if (n.isLeaf()) {
            String opName = n.getParam(ParamType.OPSTRING);
            //check opstring and exec type
            if (opName != null && opName.equals(LeftIndexingOp.OPSTRING)
                    && (n.getExecType() == ExecType.MR || n.getExecType() == ExecType.SPARK)) {
                LeftIndexingOp hop = (LeftIndexingOp) OptTreeConverter.getAbstractPlanMapping()
                        .getMappedHop(n.getID());
                //check agains set of varname
                String varName = hop.getInput().get(0).getName();
                if (resultVars.contains(varName)) {
                    ret = true;
                    if (checkSize && vars.keySet().contains(varName)) {
                        //dims of result vars must be known at this point in time
                        MatrixObject mo = (MatrixObject) vars.get(hop.getInput().get(0).getName());
                        long rows = mo.getNumRows();
                        long cols = mo.getNumColumns();
                        ret = !isInMemoryResultMerge(rows, cols, OptimizerUtils.getRemoteMemBudgetMap(false));
                    }
                }
            }
        } else {
            for (OptNode c : n.getChilds())
                ret |= hasResultMRLeftIndexing(c, resultVars, vars, checkSize);
        }

        return ret;
    }

    /**
     * Heuristically compute total result sizes, if larger than local mem budget assumed to be large.
     * 
     * @param n
     * @param resultVars
     * @param vars
     * @param checkSize
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean hasLargeTotalResults(OptNode pn, ArrayList<String> resultVars, LocalVariableMap vars,
            boolean checkSize) throws DMLRuntimeException {
        double totalSize = 0;

        //get num tasks according to task partitioning 
        PTaskPartitioner tp = PTaskPartitioner.valueOf(pn.getParam(ParamType.TASK_PARTITIONER));
        int k = pn.getK();
        long W = estimateNumTasks(tp, _N, k);

        for (String var : resultVars) {
            //Potential unknowns: for local result var of child parfor (but we're only interested in top level)
            //Potential scalars: for disabled dependency analysis and unbounded scoping         
            Data dat = vars.get(var);
            if (dat != null && dat instanceof MatrixObject) {
                MatrixObject mo = (MatrixObject) vars.get(var);

                long rows = mo.getNumRows();
                long cols = mo.getNumColumns();
                long nnz = mo.getNnz();

                if (nnz > 0) //w/ compare
                {
                    totalSize += W * OptimizerUtils.estimateSizeExactSparsity(rows, cols, 1.0);
                } else //in total at most as dimensions (due to disjoint results)
                {
                    totalSize += OptimizerUtils.estimateSizeExactSparsity(rows, cols, 1.0);
                }
            }
        }

        return (totalSize >= _lm); //heuristic:  large if >= local mem budget 
    }

    /**
     * 
     * @param tp
     * @param N
     * @param k
     * @return
     */
    protected long estimateNumTasks(PTaskPartitioner tp, long N, int k) {
        long W = -1;

        switch (tp) {
        case NAIVE:
        case FIXED:
            W = N;
            break;
        case STATIC:
            W = N / k;
            break;
        case FACTORING:
        case FACTORING_CMIN:
        case FACTORING_CMAX:
            W = k * (long) (Math.log(((double) N) / k) / Math.log(2.0));
            break;
        default:
            W = N;
            break; //N as worst case estimate
        }

        return W;
    }

    /**
     * 
     * @param n
     * @param resultVars
     * @param vars
     * @return
     * @throws DMLRuntimeException
     */
    protected boolean hasOnlyInMemoryResults(OptNode n, ArrayList<String> resultVars, LocalVariableMap vars,
            boolean inLocal) throws DMLRuntimeException {
        boolean ret = true;

        if (n.isLeaf()) {
            String opName = n.getParam(ParamType.OPSTRING);
            //check opstring and exec type
            if (opName.equals(LeftIndexingOp.OPSTRING)) {
                LeftIndexingOp hop = (LeftIndexingOp) OptTreeConverter.getAbstractPlanMapping()
                        .getMappedHop(n.getID());
                //check agains set of varname
                String varName = hop.getInput().get(0).getName();
                if (resultVars.contains(varName) && vars.keySet().contains(varName)) {
                    //dims of result vars must be known at this point in time
                    MatrixObject mo = (MatrixObject) vars.get(hop.getInput().get(0).getName());
                    long rows = mo.getNumRows();
                    long cols = mo.getNumColumns();
                    double memBudget = inLocal ? OptimizerUtils.getLocalMemBudget()
                            : OptimizerUtils.getRemoteMemBudgetMap();
                    ret &= isInMemoryResultMerge(rows, cols, memBudget);
                }
            }
        } else {
            for (OptNode c : n.getChilds())
                ret &= hasOnlyInMemoryResults(c, resultVars, vars, inLocal);
        }

        return ret;
    }

    /**
     * 
     * @param nodes
     * @param vars
     * @throws DMLRuntimeException 
     */
    protected void rInvokeSetResultMerge(Collection<OptNode> nodes, LocalVariableMap vars, boolean inLocal)
            throws DMLRuntimeException {
        for (OptNode n : nodes)
            if (n.getNodeType() == NodeType.PARFOR) {
                rewriteSetResultMerge(n, vars, inLocal);
                if (n.getExecType() == ExecType.MR || n.getExecType() == ExecType.SPARK)
                    inLocal = false;
            } else if (n.getChilds() != null)
                rInvokeSetResultMerge(n.getChilds(), vars, inLocal);
    }

    /**
     * 
     * @param rows
     * @param cols
     * @return
     */
    public static boolean isInMemoryResultMerge(long rows, long cols, double memBudget) {
        if (!ParForProgramBlock.USE_PARALLEL_RESULT_MERGE) {
            //1/4 mem budget because: 2xout (incl sparse-dense change), 1xin, 1xcompare  
            return (rows >= 0 && cols >= 0 && MatrixBlock.estimateSizeInMemory(rows, cols, 1.0) < memBudget / 4);
        } else
            return (rows >= 0 && cols >= 0 && rows * cols < Math.pow(Hop.CPThreshold, 2));
    }

    ///////
    //REWRITE set recompile memory budget
    ///

    /**
     * 
     * @param n
     * @param M
     */
    protected void rewriteSetRecompileMemoryBudget(OptNode n) {
        double newLocalMem = _lm;

        //check et because recompilation only happens at the master node
        if (n.getExecType() == ExecType.CP) {
            //compute local recompile memory budget
            int par = n.getTotalK();
            newLocalMem = _lm / par;

            //modify runtime plan
            ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                    .getMappedProg(n.getID())[1];
            pfpb.setRecompileMemoryBudget(newLocalMem);
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'set recompile memory budget' - result=" + toMB(newLocalMem));
    }

    ///////
    //REWRITE remove recursive parfor
    ///

    /**
     * 
     * @param n
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     */
    protected void rewriteRemoveRecursiveParFor(OptNode n, LocalVariableMap vars)
            throws DMLRuntimeException, DMLUnsupportedOperationException {
        int count = 0; //num removed parfor

        //find recursive parfor
        HashSet<ParForProgramBlock> recPBs = new HashSet<ParForProgramBlock>();
        rFindRecursiveParFor(n, recPBs, false);

        if (!recPBs.isEmpty()) {
            //unfold if necessary
            try {
                ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                        .getMappedProg(n.getID())[1];
                if (recPBs.contains(pfpb))
                    rFindAndUnfoldRecursiveFunction(n, pfpb, recPBs, vars);
            } catch (Exception ex) {
                throw new DMLRuntimeException(ex);
            }

            //remove recursive parfor (parfor to for)
            count = removeRecursiveParFor(n, recPBs);
        }

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'remove recursive parfor' - result=" + recPBs.size() + "/" + count);
    }

    /**
     * 
     * @param n
     * @param cand
     * @param recContext
     * @return
     */
    protected void rFindRecursiveParFor(OptNode n, HashSet<ParForProgramBlock> cand, boolean recContext) {
        //recursive invocation
        if (!n.isLeaf())
            for (OptNode c : n.getChilds()) {
                if (c.getNodeType() == NodeType.FUNCCALL && c.isRecursive())
                    rFindRecursiveParFor(c, cand, true);
                else
                    rFindRecursiveParFor(c, cand, recContext);
            }

        //add candidate program blocks
        if (recContext && n.getNodeType() == NodeType.PARFOR) {
            ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                    .getMappedProg(n.getID())[1];
            cand.add(pfpb);
        }
    }

    /**
     * 
     * @param n
     * @param parfor
     * @param recPBs
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     * @throws HopsException
     * @throws LanguageException
     */
    protected void rFindAndUnfoldRecursiveFunction(OptNode n, ParForProgramBlock parfor,
            HashSet<ParForProgramBlock> recPBs, LocalVariableMap vars)
            throws DMLRuntimeException, DMLUnsupportedOperationException, HopsException, LanguageException {
        //unfold if found
        if (n.getNodeType() == NodeType.FUNCCALL && n.isRecursive()) {
            boolean exists = rContainsNode(n, parfor);
            if (exists) {
                String fnameKey = n.getParam(ParamType.OPSTRING);
                String[] names = fnameKey.split(Program.KEY_DELIM);
                String fnamespace = names[0];
                String fname = names[1];
                String fnameNew = FUNCTION_UNFOLD_NAMEPREFIX + fname;

                //unfold function
                FunctionOp fop = (FunctionOp) OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());
                Program prog = parfor.getProgram();
                DMLProgram dmlprog = parfor.getStatementBlock().getDMLProg();
                FunctionProgramBlock fpb = prog.getFunctionProgramBlock(fnamespace, fname);
                FunctionProgramBlock copyfpb = ProgramConverter.createDeepCopyFunctionProgramBlock(fpb,
                        new HashSet<String>(), new HashSet<String>());
                prog.addFunctionProgramBlock(fnamespace, fnameNew, copyfpb);
                dmlprog.addFunctionStatementBlock(fnamespace, fnameNew,
                        (FunctionStatementBlock) copyfpb.getStatementBlock());

                //replace function names in old subtree (link to new function)
                rReplaceFunctionNames(n, fname, fnameNew);

                //recreate sub opttree
                String fnameNewKey = fnamespace + Program.KEY_DELIM + fnameNew;
                OptNode nNew = new OptNode(NodeType.FUNCCALL);
                OptTreeConverter.getAbstractPlanMapping().putHopMapping(fop, nNew);
                nNew.setExecType(ExecType.CP);
                nNew.addParam(ParamType.OPSTRING, fnameNewKey);
                long parentID = OptTreeConverter.getAbstractPlanMapping().getMappedParentID(n.getID());
                OptTreeConverter.getAbstractPlanMapping().getOptNode(parentID).exchangeChild(n, nNew);
                HashSet<String> memo = new HashSet<String>();
                memo.add(fnameKey); //required if functionop not shared (because not replaced yet)
                memo.add(fnameNewKey); //requied if functionop shared (indirectly replaced)
                for (int i = 0; i < copyfpb.getChildBlocks().size() /*&& i<len*/; i++) {
                    ProgramBlock lpb = copyfpb.getChildBlocks().get(i);
                    StatementBlock lsb = lpb.getStatementBlock();
                    nNew.addChild(OptTreeConverter.rCreateAbstractOptNode(lsb, lpb, vars, false, memo));
                }

                //compute delta for recPB set (use for removing parfor)
                recPBs.removeAll(rGetAllParForPBs(n, new HashSet<ParForProgramBlock>()));
                recPBs.addAll(rGetAllParForPBs(nNew, new HashSet<ParForProgramBlock>()));

                //replace function names in new subtree (recursive link to new function)
                rReplaceFunctionNames(nNew, fname, fnameNew);

            }
            //else, we can return anyway because we will not find that parfor

            return;
        }

        //recursive invocation (only for non-recursive functions)
        if (!n.isLeaf())
            for (OptNode c : n.getChilds())
                rFindAndUnfoldRecursiveFunction(c, parfor, recPBs, vars);
    }

    /**
     * 
     * @param n
     * @param parfor
     * @return
     */
    protected boolean rContainsNode(OptNode n, ParForProgramBlock parfor) {
        boolean ret = false;

        if (n.getNodeType() == NodeType.PARFOR) {
            ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                    .getMappedProg(n.getID())[1];
            ret = (parfor == pfpb);
        }

        if (!ret && !n.isLeaf())
            for (OptNode c : n.getChilds()) {
                ret |= rContainsNode(c, parfor);
                if (ret)
                    break; //early abort
            }

        return ret;
    }

    /**
     * 
     * @param n
     * @param pbs
     * @return
     */
    protected HashSet<ParForProgramBlock> rGetAllParForPBs(OptNode n, HashSet<ParForProgramBlock> pbs) {
        //collect parfor
        if (n.getNodeType() == NodeType.PARFOR) {
            ParForProgramBlock pfpb = (ParForProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                    .getMappedProg(n.getID())[1];
            pbs.add(pfpb);
        }

        //recursive invocation
        if (!n.isLeaf())
            for (OptNode c : n.getChilds())
                rGetAllParForPBs(c, pbs);

        return pbs;
    }

    /**
     * 
     * @param n
     * @param oldName
     * @param newName
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     * @throws HopsException 
     */
    protected void rReplaceFunctionNames(OptNode n, String oldName, String newName)
            throws DMLRuntimeException, DMLUnsupportedOperationException, HopsException {
        if (n.getNodeType() == NodeType.FUNCCALL) {
            FunctionOp fop = (FunctionOp) OptTreeConverter.getAbstractPlanMapping().getMappedHop(n.getID());

            String[] names = n.getParam(ParamType.OPSTRING).split(Program.KEY_DELIM);
            String fnamespace = names[0];
            String fname = names[1];

            if (fname.equals(oldName) || fname.equals(newName)) //newName if shared hop
            {
                //set opttree function name
                n.addParam(ParamType.OPSTRING, DMLProgram.constructFunctionKey(fnamespace, newName));

                //set instruction function name
                long parentID = OptTreeConverter.getAbstractPlanMapping().getMappedParentID(n.getID());
                ProgramBlock pb = (ProgramBlock) OptTreeConverter.getAbstractPlanMapping()
                        .getMappedProg(parentID)[1];
                ArrayList<Instruction> instArr = pb.getInstructions();
                for (int i = 0; i < instArr.size(); i++) {
                    Instruction inst = instArr.get(i);
                    if (inst instanceof FunctionCallCPInstruction) {
                        FunctionCallCPInstruction fci = (FunctionCallCPInstruction) inst;
                        if (oldName.equals(fci.getFunctionName()))
                            instArr.set(i, FunctionCallCPInstruction
                                    .parseInstruction(fci.toString().replaceAll(oldName, newName)));
                    }
                }

                //set hop name (for recompile)
                if (fop.getFunctionName().equals(oldName))
                    fop.setFunctionName(newName);
            }
        }

        //recursive invocation
        if (!n.isLeaf())
            for (OptNode c : n.getChilds())
                rReplaceFunctionNames(c, oldName, newName);
    }

    /**
     * 
     * @param n
     * @param recPBs
     * @return
     * @throws DMLUnsupportedOperationException
     * @throws DMLRuntimeException
     */
    protected int removeRecursiveParFor(OptNode n, HashSet<ParForProgramBlock> recPBs)
            throws DMLUnsupportedOperationException, DMLRuntimeException {
        int count = 0;

        if (!n.isLeaf()) {
            for (OptNode sub : n.getChilds()) {
                if (sub.getNodeType() == NodeType.PARFOR) {
                    long id = sub.getID();
                    Object[] progobj = OptTreeConverter.getAbstractPlanMapping().getMappedProg(id);
                    ParForStatementBlock pfsb = (ParForStatementBlock) progobj[0];
                    ParForProgramBlock pfpb = (ParForProgramBlock) progobj[1];

                    if (recPBs.contains(pfpb)) {
                        //create for pb as replacement
                        Program prog = pfpb.getProgram();
                        ForProgramBlock fpb = ProgramConverter.createShallowCopyForProgramBlock(pfpb, prog);

                        //replace parfor with for, and update objectmapping
                        OptTreeConverter.replaceProgramBlock(n, sub, pfpb, fpb, false);
                        //update link to statement block
                        fpb.setStatementBlock(pfsb);

                        //update node
                        sub.setNodeType(NodeType.FOR);
                        sub.setK(1);

                        count++;
                    }
                }

                count += removeRecursiveParFor(sub, recPBs);
            }
        }

        return count;
    }

    ///////
    //REWRITE remove unnecessary parfor
    ///

    /**
     * 
     * @param n
     * @throws DMLRuntimeException
     * @throws DMLUnsupportedOperationException
     */
    protected void rewriteRemoveUnnecessaryParFor(OptNode n)
            throws DMLRuntimeException, DMLUnsupportedOperationException {
        int count = removeUnnecessaryParFor(n);

        _numEvaluatedPlans++;
        LOG.debug(getOptMode() + " OPT: rewrite 'remove unnecessary parfor' - result=" + count);
    }

    /**
     * 
     * @param n
     * @return
     * @throws DMLUnsupportedOperationException
     * @throws DMLRuntimeException
     */
    protected int removeUnnecessaryParFor(OptNode n) throws DMLUnsupportedOperationException, DMLRuntimeException {
        int count = 0;

        if (!n.isLeaf()) {
            for (OptNode sub : n.getChilds()) {
                if (sub.getNodeType() == NodeType.PARFOR && sub.getK() == 1) {
                    long id = sub.getID();
                    Object[] progobj = OptTreeConverter.getAbstractPlanMapping().getMappedProg(id);
                    ParForStatementBlock pfsb = (ParForStatementBlock) progobj[0];
                    ParForProgramBlock pfpb = (ParForProgramBlock) progobj[1];

                    //create for pb as replacement
                    Program prog = pfpb.getProgram();
                    ForProgramBlock fpb = ProgramConverter.createShallowCopyForProgramBlock(pfpb, prog);

                    //replace parfor with for, and update objectmapping
                    OptTreeConverter.replaceProgramBlock(n, sub, pfpb, fpb, false);
                    //update link to statement block
                    fpb.setStatementBlock(pfsb);

                    //update node
                    sub.setNodeType(NodeType.FOR);
                    sub.setK(1);

                    count++;
                }

                count += removeUnnecessaryParFor(sub);
            }
        }

        return count;
    }

    ////////////////////////
    //   Helper methods   //
    ////////////////////////

    public static String toMB(double inB) {
        return OptimizerUtils.toMB(inB) + "MB";
    }

}